Skip to content

Agent Reads and Writes

This example walks through the core SDK workflow: creating a repo, writing files, reading them back with metadata, and using transactions with expectedHead for safe concurrent updates.

import {
Client,
StaleHeadError,
isStaleHeadError,
} from "@githosted/sdk";
const client = new Client({
token: process.env.GITHOSTED_TOKEN,
// baseUrl defaults to https://api.githosted.dev
});
async function main() {
// 1. Create a new repo
const repo = await client.createRepo("agent-workspace");
console.log(`Created repo: ${repo.info?.slug} (${repo.id})`);
// 2. Write a config file
const writeResult = await repo.write(
"config.json",
JSON.stringify({ model: "claude-3", temperature: 0.7 }, null, 2),
{ message: "Initialize config" },
);
console.log(`Commit: ${writeResult.commitSha}`);
// 3. Read the file back -- returns content + metadata for concurrency control
const file = await repo.read("config.json");
console.log(`Content: ${file.content}`);
console.log(`Head SHA: ${file.headSha}`);
console.log(`Blob SHA: ${file.blobSha}`);
// 4. Write again with expectedHead for optimistic concurrency.
// If another writer committed between our read and this write,
// the server rejects with StaleHeadError instead of silently overwriting.
const config = JSON.parse(file.content);
config.temperature = 0.5;
await repo.write(
"config.json",
JSON.stringify(config, null, 2),
{
message: "Lower temperature",
expectedHead: file.headSha,
},
);
// 5. Use a transaction to write multiple files atomically
await repo.transaction(
"Add prompt and context files",
async (tx) => {
await tx.write("prompts/system.txt", "You are a helpful assistant.");
await tx.write("prompts/user.txt", "Summarize the following document.");
await tx.write(
"context/metadata.json",
JSON.stringify({ createdAt: new Date().toISOString() }),
);
},
);
// 6. List files to verify
const rootEntries = await repo.ls();
console.log("Root:", rootEntries);
// [
// { name: "config.json", type: "file" },
// { name: "context", type: "directory" },
// { name: "prompts", type: "directory" },
// ]
const promptEntries = await repo.ls("prompts");
console.log("Prompts:", promptEntries);
// [
// { name: "system.txt", type: "file" },
// { name: "user.txt", type: "file" },
// ]
// 7. View the commit log
const commits = await repo.log({ limit: 5 });
for (const c of commits) {
console.log(`${c.hash.slice(0, 7)} ${c.subject} (${c.authorName})`);
}
// 8. Diff between two commits
if (commits.length >= 2) {
const diff = await repo.diff(commits[1].hash, commits[0].hash);
console.log(diff.patch);
}
}
main().catch(console.error);

When multiple agents or processes write to the same repo, expectedHead prevents lost updates. If the branch has moved, the SDK throws StaleHeadError with the actual head SHA so you can re-read and retry:

async function safeUpdate(repo, path: string, transform: (content: string) => string) {
for (let attempt = 0; attempt < 3; attempt++) {
const file = await repo.read(path);
const updated = transform(file.content);
try {
return await repo.write(path, updated, {
message: `Update ${path}`,
expectedHead: file.headSha,
});
} catch (err) {
if (isStaleHeadError(err)) {
console.log(`Stale head (attempt ${attempt + 1}), re-reading...`);
console.log(`Expected: ${err.expectedHead}, actual: ${err.actualHead}`);
continue; // re-read and retry
}
throw err;
}
}
throw new Error(`Failed to update ${path} after 3 attempts`);
}

Transactions also accept expectedHead to guard the entire batch of changes:

const file = await repo.read("state.json");
await repo.transaction(
"Update state and log",
async (tx) => {
const state = JSON.parse(file.content);
state.step = state.step + 1;
await tx.write("state.json", JSON.stringify(state));
await tx.write("logs/step-" + state.step + ".txt", "Completed step");
await tx.delete("logs/step-" + (state.step - 10) + ".txt");
},
{ expectedHead: file.headSha },
);
  • repo.read() returns headSha — pass it as expectedHead on the next write to enable optimistic concurrency.
  • StaleHeadError is never auto-retried — you must handle it explicitly, since the SDK cannot know how to merge your intent with the intervening changes.
  • RepoBusyError is auto-retried — the SDK retries with exponential backoff (up to 3 times by default) when the repo is temporarily locked by another in-progress write.
  • Transactions commit multiple file changes under a single commit message. They accept the same ref and expectedHead options as individual writes.