@akira-tl/forgerelay 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/capabilities/workspace/managed-worktrees/GUIDE.md +23 -1
- package/capabilities/workspace/workspace-checkpoints/GUIDE.md +75 -0
- package/dist/mcp/server/core/capabilities/workspace-checkpoint.js +20 -0
- package/dist/mcp/server/core/capabilities.js +8 -0
- package/dist/mcp/server/core/capability-registry.js +41 -0
- package/dist/mcp/server/core/capability-support.js +17 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +54 -1
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +4 -1
- package/dist/server.js +31 -1
- package/dist/workspaces/git/git-worktrees.js +1 -1
- package/dist/workspaces/git/worktree-recovery.js +330 -1
- package/dist/workspaces/sessions.js +93 -0
- package/dist/workspaces/state/workspace-checkpoints.js +408 -0
- package/dist/workspaces.js +3 -0
- package/package.json +2 -2
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtemp, realpath, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import * as z from "zod/v4";
|
|
7
|
+
import { getGitEligibility, git, safeWorkspaceRefSegment } from "../git/git.js";
|
|
8
|
+
const CHECKPOINT_STATE_VERSION = 1;
|
|
9
|
+
const CHECKPOINT_REF_PREFIX = "refs/forgerelay/checkpoints";
|
|
10
|
+
const MAX_CHECKPOINTS = 500;
|
|
11
|
+
const MAX_CHECKPOINT_NAME_LENGTH = 120;
|
|
12
|
+
const MAX_CHECKPOINT_STATE_BYTES = 1024 * 1024;
|
|
13
|
+
const checkpointSchema = z.object({
|
|
14
|
+
id: z.string().regex(/^cp_[a-f0-9]{10}$/),
|
|
15
|
+
name: z.string().min(1).max(MAX_CHECKPOINT_NAME_LENGTH),
|
|
16
|
+
createdAt: z.string().min(1),
|
|
17
|
+
commit: z.string().regex(/^[a-f0-9]{40,64}$/),
|
|
18
|
+
baseHead: z.string().regex(/^[a-f0-9]{40,64}$/),
|
|
19
|
+
summary: z.object({
|
|
20
|
+
files: z.number().int().nonnegative(),
|
|
21
|
+
additions: z.number().int().nonnegative(),
|
|
22
|
+
removals: z.number().int().nonnegative(),
|
|
23
|
+
}).strict(),
|
|
24
|
+
}).strict();
|
|
25
|
+
const checkpointStateSchema = z.object({
|
|
26
|
+
version: z.literal(CHECKPOINT_STATE_VERSION),
|
|
27
|
+
revision: z.number().int().nonnegative(),
|
|
28
|
+
gitCommonDir: z.string().min(1),
|
|
29
|
+
checkpoints: z.array(checkpointSchema).max(MAX_CHECKPOINTS),
|
|
30
|
+
}).strict().superRefine((state, context) => {
|
|
31
|
+
const ids = new Set();
|
|
32
|
+
for (const [index, checkpoint] of state.checkpoints.entries()) {
|
|
33
|
+
if (ids.has(checkpoint.id)) {
|
|
34
|
+
context.addIssue({
|
|
35
|
+
code: "custom",
|
|
36
|
+
path: ["checkpoints", index, "id"],
|
|
37
|
+
message: `Duplicate checkpoint id ${checkpoint.id}.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
ids.add(checkpoint.id);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
export class WorkspaceCheckpointStore {
|
|
44
|
+
stateDir;
|
|
45
|
+
now;
|
|
46
|
+
mutationChains = new Map();
|
|
47
|
+
constructor(stateDir, now = () => new Date()) {
|
|
48
|
+
this.stateDir = stateDir;
|
|
49
|
+
this.now = now;
|
|
50
|
+
}
|
|
51
|
+
async create(workspaceId, workspaceRoot, name) {
|
|
52
|
+
const id = normalizeWorkspaceId(workspaceId);
|
|
53
|
+
const checkpointName = normalizeCheckpointName(name);
|
|
54
|
+
return this.runMutation(id, async () => {
|
|
55
|
+
const repository = await resolveRepository(workspaceRoot);
|
|
56
|
+
const loaded = this.tryReadState(id);
|
|
57
|
+
if (loaded)
|
|
58
|
+
await assertSameRepository(loaded.gitCommonDir, repository.gitCommonDir, id);
|
|
59
|
+
if ((loaded?.checkpoints.length ?? 0) >= MAX_CHECKPOINTS) {
|
|
60
|
+
throw new Error(`Workspace checkpoint limit is ${MAX_CHECKPOINTS}. Delete an older checkpoint first.`);
|
|
61
|
+
}
|
|
62
|
+
const checkpointId = `cp_${randomBytes(5).toString("hex")}`;
|
|
63
|
+
const ref = checkpointRef(id, checkpointId);
|
|
64
|
+
const snapshot = await createWorkingTreeSnapshot(repository.gitRoot);
|
|
65
|
+
const checkpoint = {
|
|
66
|
+
id: checkpointId,
|
|
67
|
+
name: checkpointName,
|
|
68
|
+
createdAt: this.now().toISOString(),
|
|
69
|
+
commit: snapshot.commit,
|
|
70
|
+
baseHead: snapshot.baseHead,
|
|
71
|
+
summary: snapshot.summary,
|
|
72
|
+
};
|
|
73
|
+
await updateRef(repository.gitCommonDir, ref, checkpoint.commit, zeroOid(checkpoint.commit.length));
|
|
74
|
+
try {
|
|
75
|
+
this.writeState(id, {
|
|
76
|
+
version: CHECKPOINT_STATE_VERSION,
|
|
77
|
+
revision: (loaded?.revision ?? 0) + 1,
|
|
78
|
+
gitCommonDir: repository.gitCommonDir,
|
|
79
|
+
checkpoints: [...(loaded?.checkpoints ?? []), checkpoint],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
await deleteRef(repository.gitCommonDir, ref, checkpoint.commit).catch(() => undefined);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
return { ...checkpoint, summary: { ...checkpoint.summary } };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async list(workspaceId, workspaceRoot, input = {}) {
|
|
90
|
+
const id = normalizeWorkspaceId(workspaceId);
|
|
91
|
+
const repository = await resolveRepository(workspaceRoot);
|
|
92
|
+
const state = this.tryReadState(id);
|
|
93
|
+
if (state)
|
|
94
|
+
await assertSameRepository(state.gitCommonDir, repository.gitCommonDir, id);
|
|
95
|
+
const offset = normalizeOffset(input.offset);
|
|
96
|
+
const limit = normalizeLimit(input.limit);
|
|
97
|
+
const checkpoints = state?.checkpoints ?? [];
|
|
98
|
+
return {
|
|
99
|
+
workspaceId: id,
|
|
100
|
+
checkpoints: checkpoints.slice(offset, offset + limit).map(cloneCheckpoint),
|
|
101
|
+
page: {
|
|
102
|
+
offset,
|
|
103
|
+
limit,
|
|
104
|
+
total: checkpoints.length,
|
|
105
|
+
hasMore: offset + limit < checkpoints.length,
|
|
106
|
+
},
|
|
107
|
+
ignoredFilesIncluded: false,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async inspect(workspaceId, workspaceRoot, checkpointId) {
|
|
111
|
+
const id = normalizeWorkspaceId(workspaceId);
|
|
112
|
+
const cpId = normalizeCheckpointId(checkpointId);
|
|
113
|
+
const repository = await resolveRepository(workspaceRoot);
|
|
114
|
+
const state = this.requireState(id);
|
|
115
|
+
await assertSameRepository(state.gitCommonDir, repository.gitCommonDir, id);
|
|
116
|
+
const checkpoint = requireCheckpoint(state, cpId);
|
|
117
|
+
await assertCheckpointRef(state.gitCommonDir, id, checkpoint);
|
|
118
|
+
return { workspaceId: id, checkpoint: cloneCheckpoint(checkpoint), ignoredFilesIncluded: false };
|
|
119
|
+
}
|
|
120
|
+
async delete(workspaceId, workspaceRoot, checkpointId) {
|
|
121
|
+
const id = normalizeWorkspaceId(workspaceId);
|
|
122
|
+
const cpId = normalizeCheckpointId(checkpointId);
|
|
123
|
+
return this.runMutation(id, async () => {
|
|
124
|
+
const repository = await resolveRepository(workspaceRoot);
|
|
125
|
+
const state = this.requireState(id);
|
|
126
|
+
await assertSameRepository(state.gitCommonDir, repository.gitCommonDir, id);
|
|
127
|
+
const checkpoint = requireCheckpoint(state, cpId);
|
|
128
|
+
const ref = checkpointRef(id, cpId);
|
|
129
|
+
await deleteRef(state.gitCommonDir, ref, checkpoint.commit);
|
|
130
|
+
try {
|
|
131
|
+
this.writeState(id, {
|
|
132
|
+
...state,
|
|
133
|
+
revision: state.revision + 1,
|
|
134
|
+
checkpoints: state.checkpoints.filter((candidate) => candidate.id !== cpId),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
await updateRef(state.gitCommonDir, ref, checkpoint.commit, zeroOid(checkpoint.commit.length)).catch(() => undefined);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
return { workspaceId: id, checkpointId: cpId, deleted: true };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async deleteWorkspace(workspaceId) {
|
|
145
|
+
const id = normalizeWorkspaceId(workspaceId);
|
|
146
|
+
await this.runMutation(id, async () => {
|
|
147
|
+
const state = this.tryReadState(id);
|
|
148
|
+
if (!state)
|
|
149
|
+
return;
|
|
150
|
+
for (const checkpoint of state.checkpoints) {
|
|
151
|
+
await deleteRef(state.gitCommonDir, checkpointRef(id, checkpoint.id), checkpoint.commit);
|
|
152
|
+
}
|
|
153
|
+
rmSync(this.statePath(id), { force: true });
|
|
154
|
+
try {
|
|
155
|
+
rmdirSync(this.workspaceStateDir(id));
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (!isErrno(error, "ENOENT") && !isErrno(error, "ENOTEMPTY") && !isErrno(error, "EEXIST")) {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async runMutation(workspaceId, operation) {
|
|
165
|
+
const previous = this.mutationChains.get(workspaceId) ?? Promise.resolve();
|
|
166
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
167
|
+
this.mutationChains.set(workspaceId, current);
|
|
168
|
+
try {
|
|
169
|
+
return await current;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
if (this.mutationChains.get(workspaceId) === current)
|
|
173
|
+
this.mutationChains.delete(workspaceId);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
requireState(workspaceId) {
|
|
177
|
+
const state = this.tryReadState(workspaceId);
|
|
178
|
+
if (!state)
|
|
179
|
+
throw new Error(`Workspace ${workspaceId} has no checkpoints.`);
|
|
180
|
+
return state;
|
|
181
|
+
}
|
|
182
|
+
tryReadState(workspaceId) {
|
|
183
|
+
let raw;
|
|
184
|
+
try {
|
|
185
|
+
raw = readFileSync(this.statePath(workspaceId));
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (isErrno(error, "ENOENT"))
|
|
189
|
+
return undefined;
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
if (raw.byteLength > MAX_CHECKPOINT_STATE_BYTES) {
|
|
193
|
+
throw new Error(`Workspace checkpoint state exceeds ${MAX_CHECKPOINT_STATE_BYTES} bytes.`);
|
|
194
|
+
}
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(raw.toString("utf8"));
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
throw new Error(`Workspace checkpoint state is not valid JSON: ${errorMessage(error)}`);
|
|
201
|
+
}
|
|
202
|
+
const validated = checkpointStateSchema.safeParse(parsed);
|
|
203
|
+
if (!validated.success) {
|
|
204
|
+
const details = validated.error.issues
|
|
205
|
+
.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "state"}: ${issue.message}`)
|
|
206
|
+
.join("; ");
|
|
207
|
+
throw new Error(`Workspace checkpoint state has an unsupported or invalid format: ${details}`);
|
|
208
|
+
}
|
|
209
|
+
return cloneState(validated.data);
|
|
210
|
+
}
|
|
211
|
+
writeState(workspaceId, state) {
|
|
212
|
+
const validated = checkpointStateSchema.parse(state);
|
|
213
|
+
const serialized = `${JSON.stringify(validated, null, 2)}\n`;
|
|
214
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_CHECKPOINT_STATE_BYTES) {
|
|
215
|
+
throw new Error(`Workspace checkpoint state exceeds ${MAX_CHECKPOINT_STATE_BYTES} bytes.`);
|
|
216
|
+
}
|
|
217
|
+
const workspaceDir = this.workspaceStateDir(workspaceId);
|
|
218
|
+
mkdirSync(workspaceDir, { recursive: true, mode: 0o700 });
|
|
219
|
+
const statePath = this.statePath(workspaceId);
|
|
220
|
+
const tempPath = `${statePath}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
221
|
+
try {
|
|
222
|
+
writeFileSync(tempPath, serialized, { mode: 0o600 });
|
|
223
|
+
renameSync(tempPath, statePath);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
rmSync(tempPath, { force: true });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
workspaceStateDir(workspaceId) {
|
|
230
|
+
return join(this.stateDir, "workspaces", workspaceId);
|
|
231
|
+
}
|
|
232
|
+
statePath(workspaceId) {
|
|
233
|
+
return join(this.workspaceStateDir(workspaceId), "checkpoints.json");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function resolveRepository(workspaceRoot) {
|
|
237
|
+
const eligibility = await getGitEligibility(workspaceRoot);
|
|
238
|
+
if (!eligibility.ok || !eligibility.gitRoot) {
|
|
239
|
+
throw new Error(eligibility.message ?? "workspace.checkpoint requires a Git workspace with a HEAD commit.");
|
|
240
|
+
}
|
|
241
|
+
const commonDirRaw = (await git(eligibility.gitRoot, [
|
|
242
|
+
"rev-parse",
|
|
243
|
+
"--path-format=absolute",
|
|
244
|
+
"--git-common-dir",
|
|
245
|
+
])).stdout.trim();
|
|
246
|
+
const commonDir = await canonicalExistingPath(commonDirRaw);
|
|
247
|
+
return { gitRoot: eligibility.gitRoot, gitCommonDir: commonDir };
|
|
248
|
+
}
|
|
249
|
+
async function createWorkingTreeSnapshot(gitRoot) {
|
|
250
|
+
const tempDir = await mkdtemp(join(tmpdir(), "forgerelay-checkpoint-index-"));
|
|
251
|
+
const indexPath = join(tempDir, "index");
|
|
252
|
+
const env = checkpointEnv(indexPath);
|
|
253
|
+
try {
|
|
254
|
+
await git(gitRoot, ["read-tree", "HEAD"], { env });
|
|
255
|
+
await git(gitRoot, ["add", "-A", "--", "."], { env });
|
|
256
|
+
const tree = (await git(gitRoot, ["write-tree"], { env })).stdout.trim();
|
|
257
|
+
const baseHead = (await git(gitRoot, ["rev-parse", "--verify", "HEAD^{commit}"])).stdout.trim();
|
|
258
|
+
const commit = (await git(gitRoot, [
|
|
259
|
+
"commit-tree",
|
|
260
|
+
tree,
|
|
261
|
+
"-p",
|
|
262
|
+
baseHead,
|
|
263
|
+
"-m",
|
|
264
|
+
"ForgeRelay persistent workspace checkpoint",
|
|
265
|
+
], { env })).stdout.trim();
|
|
266
|
+
const numstat = (await git(gitRoot, ["diff", "--numstat", "-z", baseHead, commit], {
|
|
267
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
268
|
+
})).stdout;
|
|
269
|
+
return { commit, baseHead, summary: summarizeNumstat(numstat) };
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function summarizeNumstat(output) {
|
|
276
|
+
const fields = output.split("\0").filter((field) => field.length > 0);
|
|
277
|
+
let files = 0;
|
|
278
|
+
let additions = 0;
|
|
279
|
+
let removals = 0;
|
|
280
|
+
for (let index = 0; index < fields.length;) {
|
|
281
|
+
const header = fields[index++] ?? "";
|
|
282
|
+
const parts = header.split("\t");
|
|
283
|
+
additions += parseStatNumber(parts[0]);
|
|
284
|
+
removals += parseStatNumber(parts[1]);
|
|
285
|
+
files += 1;
|
|
286
|
+
if (parts.length < 3)
|
|
287
|
+
index += 2;
|
|
288
|
+
}
|
|
289
|
+
return { files, additions, removals };
|
|
290
|
+
}
|
|
291
|
+
function parseStatNumber(value) {
|
|
292
|
+
if (!value || value === "-")
|
|
293
|
+
return 0;
|
|
294
|
+
const parsed = Number(value);
|
|
295
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
296
|
+
}
|
|
297
|
+
function checkpointEnv(indexPath) {
|
|
298
|
+
return {
|
|
299
|
+
GIT_INDEX_FILE: indexPath,
|
|
300
|
+
GIT_AUTHOR_NAME: "ForgeRelay",
|
|
301
|
+
GIT_AUTHOR_EMAIL: "forgerelay@users.noreply.local",
|
|
302
|
+
GIT_COMMITTER_NAME: "ForgeRelay",
|
|
303
|
+
GIT_COMMITTER_EMAIL: "forgerelay@users.noreply.local",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async function updateRef(commonDir, ref, commit, oldValue) {
|
|
307
|
+
await git(commonDir, ["--git-dir", commonDir, "update-ref", ref, commit, oldValue]);
|
|
308
|
+
}
|
|
309
|
+
async function deleteRef(commonDir, ref, expectedCommit) {
|
|
310
|
+
await assertCheckpointRefCommit(commonDir, ref, expectedCommit);
|
|
311
|
+
await git(commonDir, ["--git-dir", commonDir, "update-ref", "-d", ref, expectedCommit]);
|
|
312
|
+
}
|
|
313
|
+
async function assertCheckpointRef(commonDir, workspaceId, checkpoint) {
|
|
314
|
+
await assertCheckpointRefCommit(commonDir, checkpointRef(workspaceId, checkpoint.id), checkpoint.commit);
|
|
315
|
+
}
|
|
316
|
+
async function assertCheckpointRefCommit(commonDir, ref, expectedCommit) {
|
|
317
|
+
let actual;
|
|
318
|
+
try {
|
|
319
|
+
actual = (await git(commonDir, ["--git-dir", commonDir, "rev-parse", "--verify", `${ref}^{commit}`])).stdout.trim();
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
throw new Error(`Checkpoint Git ref ${ref} is missing; refusing to mutate inconsistent checkpoint state.`);
|
|
323
|
+
}
|
|
324
|
+
if (actual !== expectedCommit) {
|
|
325
|
+
throw new Error(`Checkpoint Git ref ${ref} no longer matches its immutable checkpoint commit.`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function checkpointRef(workspaceId, checkpointId) {
|
|
329
|
+
return `${CHECKPOINT_REF_PREFIX}/${safeWorkspaceRefSegment(workspaceId)}/${checkpointId}`;
|
|
330
|
+
}
|
|
331
|
+
async function assertSameRepository(stored, current, workspaceId) {
|
|
332
|
+
const [storedCanonical, currentCanonical] = await Promise.all([
|
|
333
|
+
canonicalExistingPath(stored),
|
|
334
|
+
canonicalExistingPath(current),
|
|
335
|
+
]);
|
|
336
|
+
if (storedCanonical !== currentCanonical) {
|
|
337
|
+
throw new Error(`Workspace checkpoint repository mismatch for ${workspaceId}.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function canonicalExistingPath(path) {
|
|
341
|
+
try {
|
|
342
|
+
return await realpath(path);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return resolve(path);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function normalizeWorkspaceId(workspaceId) {
|
|
349
|
+
const value = workspaceId.trim();
|
|
350
|
+
if (!/^[a-z][a-z0-9_-]{1,127}$/.test(value)) {
|
|
351
|
+
throw new Error("Workspace ID is not valid for checkpoint state.");
|
|
352
|
+
}
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
function normalizeCheckpointId(checkpointId) {
|
|
356
|
+
const value = checkpointId.trim();
|
|
357
|
+
if (!/^cp_[a-f0-9]{10}$/.test(value))
|
|
358
|
+
throw new Error(`Invalid checkpoint id ${checkpointId}.`);
|
|
359
|
+
return value;
|
|
360
|
+
}
|
|
361
|
+
function normalizeCheckpointName(name) {
|
|
362
|
+
const value = name.trim();
|
|
363
|
+
if (!value)
|
|
364
|
+
throw new Error("Checkpoint name must not be empty.");
|
|
365
|
+
if (value.length > MAX_CHECKPOINT_NAME_LENGTH) {
|
|
366
|
+
throw new Error(`Checkpoint name must be at most ${MAX_CHECKPOINT_NAME_LENGTH} characters.`);
|
|
367
|
+
}
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
function normalizeOffset(offset) {
|
|
371
|
+
if (offset === undefined)
|
|
372
|
+
return 0;
|
|
373
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
374
|
+
throw new Error("Checkpoint list offset must be a non-negative integer.");
|
|
375
|
+
return offset;
|
|
376
|
+
}
|
|
377
|
+
function normalizeLimit(limit) {
|
|
378
|
+
if (limit === undefined)
|
|
379
|
+
return 50;
|
|
380
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
381
|
+
throw new Error("Checkpoint list limit must be an integer between 1 and 100.");
|
|
382
|
+
}
|
|
383
|
+
return limit;
|
|
384
|
+
}
|
|
385
|
+
function requireCheckpoint(state, checkpointId) {
|
|
386
|
+
const checkpoint = state.checkpoints.find((candidate) => candidate.id === checkpointId);
|
|
387
|
+
if (!checkpoint)
|
|
388
|
+
throw new Error(`Unknown Workspace checkpoint ${checkpointId}.`);
|
|
389
|
+
return checkpoint;
|
|
390
|
+
}
|
|
391
|
+
function cloneCheckpoint(checkpoint) {
|
|
392
|
+
return { ...checkpoint, summary: { ...checkpoint.summary } };
|
|
393
|
+
}
|
|
394
|
+
function cloneState(state) {
|
|
395
|
+
return {
|
|
396
|
+
...state,
|
|
397
|
+
checkpoints: state.checkpoints.map(cloneCheckpoint),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function zeroOid(length) {
|
|
401
|
+
return "0".repeat(length);
|
|
402
|
+
}
|
|
403
|
+
function isErrno(error, code) {
|
|
404
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
405
|
+
}
|
|
406
|
+
function errorMessage(error) {
|
|
407
|
+
return error instanceof Error ? error.message : String(error);
|
|
408
|
+
}
|
package/dist/workspaces.js
CHANGED
|
@@ -316,6 +316,9 @@ export class WorkspaceRegistry {
|
|
|
316
316
|
getWorkspace(workspaceId) {
|
|
317
317
|
return this.sessions.getWorkspace(workspaceId);
|
|
318
318
|
}
|
|
319
|
+
runManagedWorktreeRecovery(workspaceId, operation) {
|
|
320
|
+
return this.sessions.runManagedWorktreeRecovery(workspaceId, operation);
|
|
321
|
+
}
|
|
319
322
|
fileToolRoots(workspace) {
|
|
320
323
|
return this.context.fileToolRoots(workspace);
|
|
321
324
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Local development control plane for MCP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Akira-TL/forgerelay#readme",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"release:push-ready": "node scripts/release/push-ready.mjs",
|
|
53
53
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
54
54
|
"start": "node dist/cli.js serve",
|
|
55
|
-
"test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/runtime/config/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/cli.test.ts",
|
|
55
|
+
"test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/runtime/config/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/cli.test.ts",
|
|
56
56
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
57
57
|
"release:check": "node scripts/release-version.mjs check",
|
|
58
58
|
"release:tag-check": "node scripts/release-version.mjs tag",
|