agentcache 0.4.2 → 0.5.0-beta.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/README.md +282 -151
- package/dist/{chunk-T4COG3XD.js → chunk-R5I6WWSD.js} +31 -14
- package/dist/chunk-RXGW4Q3G.js +109 -0
- package/dist/chunk-XRJ6QW6N.js +92 -0
- package/dist/chunk-YKG6CDGT.js +1818 -0
- package/dist/chunk-YY7QXBG5.js +6610 -0
- package/dist/cli.js +2535 -292
- package/dist/device-id-RV7RO5RB.js +7 -0
- package/dist/ide-detector-ETGAVVXO.js +8 -0
- package/dist/mcp.d.ts +734 -2
- package/dist/mcp.js +1126 -446
- package/dist/{paths-5LZRKNYY.js → paths-NTZ2357O.js} +3 -2
- package/dist/postinstall.js +1 -65
- package/dist/setup-7JJPW3VG.js +48 -0
- package/docs/compatibility.md +152 -0
- package/docs/demo-script.md +121 -0
- package/docs/launch-copy.md +125 -0
- package/docs/privacy.md +173 -0
- package/docs/troubleshooting.md +209 -0
- package/package.json +32 -14
- package/dist/3-canonicalizer-HIN2F7SZ.js +0 -11
- package/dist/chunk-5UO7NJPQ.js +0 -71
- package/dist/chunk-CUBZRYS5.js +0 -580
- package/dist/chunk-GGAATZKM.js +0 -120
- package/dist/chunk-JUDLOBOC.js +0 -77
- package/dist/chunk-KFQGP6VL.js +0 -33
- package/dist/chunk-PSASDZQE.js +0 -490
- package/dist/chunk-SLRKWMSE.js +0 -202
- package/dist/chunk-T7BJPANN.js +0 -45
- package/dist/chunk-WTXSZBQE.js +0 -388
- package/dist/compile-all-PTWTZVP5.js +0 -495
- package/dist/ide-detector-5TRCR4F5.js +0 -7
- package/dist/pre-tool-use-A4AJHZOJ.js +0 -30
- package/dist/session-start-DGMGEAJU.js +0 -78
- package/dist/setup-CVG35TUZ.js +0 -51
- package/dist/sqlite-NM2BVHUY.js +0 -7
- package/dist/stop-WGGRX6TQ.js +0 -38
- package/dist/transcript-JWSGSDSF.js +0 -24
package/dist/cli.js
CHANGED
|
@@ -1,366 +1,2609 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_READ_BUDGET,
|
|
4
|
+
PORTABLE_CLI_ADAPTER_ID,
|
|
5
|
+
SESSION_SCHEMA_VERSION,
|
|
6
|
+
SessionOperationError,
|
|
7
|
+
SessionService,
|
|
8
|
+
SqliteSessionRepository,
|
|
9
|
+
createId,
|
|
10
|
+
listAdapters,
|
|
11
|
+
redactPortableText,
|
|
12
|
+
resolveWorkspace,
|
|
13
|
+
serializePublicResumeCapsule,
|
|
14
|
+
validateReadBudget
|
|
15
|
+
} from "./chunk-YY7QXBG5.js";
|
|
16
|
+
import {
|
|
17
|
+
loadOrCreateDeviceId
|
|
18
|
+
} from "./chunk-XRJ6QW6N.js";
|
|
19
|
+
import {
|
|
20
|
+
configureMcpRegistrationEntrypoint,
|
|
21
|
+
inspectLegacyClaudeHookPresence,
|
|
22
|
+
isMcpServerRegistered
|
|
23
|
+
} from "./chunk-YKG6CDGT.js";
|
|
24
|
+
import "./chunk-RXGW4Q3G.js";
|
|
25
|
+
import {
|
|
26
|
+
getSessionDbPath
|
|
27
|
+
} from "./chunk-R5I6WWSD.js";
|
|
2
28
|
|
|
3
29
|
// src/cli.ts
|
|
30
|
+
import { Command as Command3 } from "commander";
|
|
31
|
+
import {
|
|
32
|
+
closeSync as closeSync2,
|
|
33
|
+
constants as fsConstants,
|
|
34
|
+
fstatSync as fstatSync2,
|
|
35
|
+
lstatSync as lstatFileSync,
|
|
36
|
+
openSync as openSync2,
|
|
37
|
+
readFileSync as readPackageFileSync,
|
|
38
|
+
readSync as readSync2
|
|
39
|
+
} from "fs";
|
|
40
|
+
import { fileURLToPath } from "url";
|
|
41
|
+
|
|
42
|
+
// src/commands/adapter.ts
|
|
4
43
|
import { Command } from "commander";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
await
|
|
44
|
+
async function startMcpForAdapter(adapterId, dependencies = {}) {
|
|
45
|
+
const adapters = dependencies.adapters ?? listAdapters();
|
|
46
|
+
const adapter = requireAdapter(
|
|
47
|
+
new Map(adapters.map((candidate) => [candidate.id, candidate])),
|
|
48
|
+
adapterId
|
|
49
|
+
);
|
|
50
|
+
const loadDeviceId = dependencies.loadDeviceId ?? (await import("./device-id-RV7RO5RB.js")).loadOrCreateDeviceId;
|
|
51
|
+
const startServer = dependencies.startServer ?? (await import("./mcp.js")).startMcpServer;
|
|
52
|
+
await startServer({ adapterId: adapter.id, deviceId: loadDeviceId() });
|
|
53
|
+
}
|
|
54
|
+
function createAdapterCommand(dependencies = {}) {
|
|
55
|
+
const adapters = dependencies.adapters ?? listAdapters();
|
|
56
|
+
const write = dependencies.write ?? ((value) => process.stdout.write(value));
|
|
57
|
+
const byId = new Map(adapters.map((adapter) => [adapter.id, adapter]));
|
|
58
|
+
const command = new Command("adapter").description("Inspect and configure agent adapters");
|
|
59
|
+
command.command("list").description("List supported agent adapters").option("--json", "Print machine-readable JSON").action((options) => {
|
|
60
|
+
const result = {
|
|
61
|
+
adapters: adapters.map((adapter) => {
|
|
62
|
+
const descriptor = adapter.descriptor();
|
|
63
|
+
return {
|
|
64
|
+
id: descriptor.id,
|
|
65
|
+
displayName: publicDiagnostic(descriptor.displayName),
|
|
66
|
+
capabilities: publicCapabilities(descriptor.capabilities)
|
|
67
|
+
};
|
|
68
|
+
})
|
|
69
|
+
};
|
|
70
|
+
if (options.json) return writeJson(write, result);
|
|
71
|
+
for (const adapter of result.adapters) {
|
|
72
|
+
write(`${terminalText(adapter.id)} ${terminalText(adapter.displayName)}
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
command.command("doctor <adapter-id>").description("Probe an adapter and report capability evidence").option("--json", "Print machine-readable JSON").action(async (adapterId, options) => {
|
|
77
|
+
const adapter = requireAdapter(byId, adapterId);
|
|
78
|
+
const probe = await adapter.probe();
|
|
79
|
+
const result = {
|
|
80
|
+
adapterId: adapter.id,
|
|
81
|
+
available: probe.available,
|
|
82
|
+
checkedAt: publicDiagnostic(probe.checkedAt),
|
|
83
|
+
...probe.clientVersion === void 0 ? {} : { clientVersion: publicDiagnostic(probe.clientVersion) },
|
|
84
|
+
...probe.sourceFormatVersion === void 0 ? {} : { sourceFormatVersion: publicDiagnostic(probe.sourceFormatVersion) },
|
|
85
|
+
diagnostics: probe.diagnostics.slice(0, 50).map(publicDiagnostic),
|
|
86
|
+
capabilities: publicCapabilities(probe.capabilities)
|
|
87
|
+
};
|
|
88
|
+
if (options.json) return writeJson(write, result);
|
|
89
|
+
write(
|
|
90
|
+
`${publicDiagnostic(adapter.descriptor().displayName)}: ${probe.available ? "available" : "unavailable"}
|
|
91
|
+
`
|
|
92
|
+
);
|
|
93
|
+
for (const [name, capability] of Object.entries(result.capabilities)) {
|
|
94
|
+
const evidence = capability.evidence ?? capability.reason ?? "No evidence reported";
|
|
95
|
+
write(
|
|
96
|
+
` ${terminalText(name)}: ${terminalText(capability.level)} (${terminalText(capability.mode)}) \u2014 ${terminalText(evidence)}
|
|
97
|
+
`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
for (const diagnostic of result.diagnostics) {
|
|
101
|
+
write(` diagnostic: ${terminalText(diagnostic)}
|
|
102
|
+
`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
command.command("register <adapter-id>").description("Register AgentCache with one adapter").action(async (adapterId) => {
|
|
106
|
+
const adapter = requireAdapter(byId, adapterId);
|
|
107
|
+
const result = await adapter.register();
|
|
108
|
+
const state = result.registered ? "registered" : "not registered";
|
|
109
|
+
write(
|
|
110
|
+
`${publicDiagnostic(adapter.descriptor().displayName)}: ${state}${result.reason ? ` \u2014 ${publicDiagnostic(result.reason)}` : ""}
|
|
111
|
+
`
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
command.command("unregister <adapter-id>").description("Remove AgentCache registration from one adapter").action(async (adapterId) => {
|
|
115
|
+
const adapter = requireAdapter(byId, adapterId);
|
|
116
|
+
const result = await adapter.unregister();
|
|
117
|
+
const state = result.registered ? "still registered" : "unregistered";
|
|
118
|
+
write(
|
|
119
|
+
`${publicDiagnostic(adapter.descriptor().displayName)}: ${state}${result.reason ? ` \u2014 ${publicDiagnostic(result.reason)}` : ""}
|
|
120
|
+
`
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
return command;
|
|
124
|
+
}
|
|
125
|
+
function registerAdapterCommands(program2, dependencies = {}) {
|
|
126
|
+
program2.addCommand(createAdapterCommand(dependencies));
|
|
127
|
+
}
|
|
128
|
+
function requireAdapter(adapters, adapterId) {
|
|
129
|
+
const adapter = adapters.get(adapterId);
|
|
130
|
+
if (!adapter) {
|
|
131
|
+
throw new SessionOperationError(
|
|
132
|
+
"invalid_input",
|
|
133
|
+
`Unknown adapter: ${adapterId}`,
|
|
134
|
+
["setup", "cancel"]
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return adapter;
|
|
138
|
+
}
|
|
139
|
+
function publicCapabilities(capabilities) {
|
|
140
|
+
return {
|
|
141
|
+
registration: publicCapability(capabilities.registration),
|
|
142
|
+
history: publicCapability(capabilities.history),
|
|
143
|
+
nativeResume: publicCapability(capabilities.nativeResume),
|
|
144
|
+
append: publicCapability(capabilities.append),
|
|
145
|
+
checkpoint: publicCapability(capabilities.checkpoint),
|
|
146
|
+
fork: publicCapability(capabilities.fork),
|
|
147
|
+
launch: publicCapability(capabilities.launch)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function publicCapability(capability) {
|
|
151
|
+
return {
|
|
152
|
+
level: capability.level,
|
|
153
|
+
mode: capability.mode,
|
|
154
|
+
...capability.reason === void 0 ? {} : { reason: publicDiagnostic(capability.reason) },
|
|
155
|
+
...capability.evidence === void 0 ? {} : { evidence: publicDiagnostic(capability.evidence) },
|
|
156
|
+
...capability.verifiedAt === void 0 ? {} : { verifiedAt: publicDiagnostic(capability.verifiedAt) }
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function writeJson(write, value) {
|
|
160
|
+
write(`${JSON.stringify(value)}
|
|
161
|
+
`);
|
|
162
|
+
}
|
|
163
|
+
function terminalText(value) {
|
|
164
|
+
return value.replace(
|
|
165
|
+
/[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g,
|
|
166
|
+
"\uFFFD"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
function publicDiagnostic(value) {
|
|
170
|
+
const boundedSource = value.slice(0, 4096);
|
|
171
|
+
return terminalText(redactPortableText(terminalText(boundedSource))).slice(0, 1024);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/commands/session.ts
|
|
175
|
+
import { randomUUID } from "crypto";
|
|
176
|
+
import {
|
|
177
|
+
closeSync,
|
|
178
|
+
constants,
|
|
179
|
+
fstatSync,
|
|
180
|
+
lstatSync,
|
|
181
|
+
openSync,
|
|
182
|
+
readSync
|
|
183
|
+
} from "fs";
|
|
184
|
+
import { basename } from "path";
|
|
185
|
+
import { createInterface } from "readline/promises";
|
|
186
|
+
import { TextDecoder } from "util";
|
|
187
|
+
import { Command as Command2, CommanderError } from "commander";
|
|
188
|
+
|
|
189
|
+
// src/session/ingestion.ts
|
|
190
|
+
import { createHash } from "crypto";
|
|
191
|
+
import { isAbsolute } from "path";
|
|
192
|
+
var SESSION_TITLE_MAX_CHARS = 120;
|
|
193
|
+
var METADATA_KEY_MAX_CHARS = 256;
|
|
194
|
+
var METADATA_STRING_MAX_CHARS = 4096;
|
|
195
|
+
var OPERATION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
196
|
+
"not_found",
|
|
197
|
+
"adapter_unavailable",
|
|
198
|
+
"unsupported_version",
|
|
199
|
+
"malformed_source",
|
|
200
|
+
"workspace_mismatch",
|
|
201
|
+
"head_changed",
|
|
202
|
+
"handoff_expired",
|
|
203
|
+
"handoff_consumed",
|
|
204
|
+
"wrong_destination",
|
|
205
|
+
"lease_conflict",
|
|
206
|
+
"invalid_token",
|
|
207
|
+
"out_of_sync",
|
|
208
|
+
"invalid_input"
|
|
209
|
+
]);
|
|
210
|
+
var OPERATION_ALTERNATIVES = /* @__PURE__ */ new Set([
|
|
211
|
+
"refresh",
|
|
212
|
+
"fork",
|
|
213
|
+
"cancel",
|
|
214
|
+
"wait",
|
|
215
|
+
"retry",
|
|
216
|
+
"setup",
|
|
217
|
+
"diagnose"
|
|
218
|
+
]);
|
|
219
|
+
async function discoverSessions(binding, adapters, budget) {
|
|
220
|
+
const validatedBudget = validateReadBudget(budget);
|
|
221
|
+
const uniqueAdapters = /* @__PURE__ */ new Map();
|
|
222
|
+
for (const adapter of adapters) {
|
|
223
|
+
if (!uniqueAdapters.has(adapter.id)) uniqueAdapters.set(adapter.id, adapter);
|
|
224
|
+
}
|
|
225
|
+
const diagnostics = [];
|
|
226
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
227
|
+
const ambiguousIdentities = /* @__PURE__ */ new Set();
|
|
228
|
+
for (const adapter of uniqueAdapters.values()) {
|
|
229
|
+
const descriptor = adapter.descriptor();
|
|
230
|
+
if (descriptor.capabilities.history.level === "unsupported" || !adapter.discover) {
|
|
231
|
+
diagnostics.push({
|
|
232
|
+
adapterId: adapter.id,
|
|
233
|
+
code: "history_unsupported",
|
|
234
|
+
message: safeErrorMessage(descriptor.capabilities.history.reason ?? `${descriptor.displayName} does not support local history discovery`)
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
let discovered;
|
|
239
|
+
try {
|
|
240
|
+
discovered = await adapter.discover({
|
|
241
|
+
workspaceRoot: binding.rootPath,
|
|
242
|
+
budget: validatedBudget
|
|
243
|
+
});
|
|
244
|
+
} catch (error) {
|
|
245
|
+
diagnostics.push({
|
|
246
|
+
adapterId: adapter.id,
|
|
247
|
+
code: "discovery_failed",
|
|
248
|
+
message: safeErrorMessage(error)
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
for (const candidate of discovered) {
|
|
253
|
+
const reference = candidate?.reference;
|
|
254
|
+
if (!reference || reference.adapterId !== adapter.id || !nonEmpty(reference.nativeSessionId) || !nonEmpty(reference.locator)) {
|
|
255
|
+
diagnostics.push({
|
|
256
|
+
adapterId: adapter.id,
|
|
257
|
+
code: "invalid_reference",
|
|
258
|
+
message: "Adapter discovery returned an invalid or mismatched native session reference"
|
|
259
|
+
});
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const evidence = workspaceEvidence(candidate);
|
|
263
|
+
if (evidence.status === "verified" && (evidence.root !== binding.rootPath || reference.workspaceRoot !== evidence.root)) {
|
|
264
|
+
diagnostics.push({
|
|
265
|
+
adapterId: adapter.id,
|
|
266
|
+
code: "workspace_unverified",
|
|
267
|
+
message: "Native session workspace could not be verified for the active root"
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
let boundCandidate;
|
|
272
|
+
try {
|
|
273
|
+
boundCandidate = evidence.status === "verified" ? sanitizeDiscoveredSession(candidate, evidence) : unassignedDiscoveryChoice(candidate, evidence);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
diagnostics.push({
|
|
276
|
+
adapterId: adapter.id,
|
|
277
|
+
code: "invalid_reference",
|
|
278
|
+
message: safeErrorMessage(error)
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (evidence.status !== "verified") {
|
|
283
|
+
diagnostics.push({
|
|
284
|
+
adapterId: adapter.id,
|
|
285
|
+
code: "workspace_unverified",
|
|
286
|
+
message: "Native session is unassigned and requires an explicit attach target"
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const identity = nativeIdentity(adapter.id, reference.nativeSessionId);
|
|
290
|
+
if (ambiguousIdentities.has(identity)) continue;
|
|
291
|
+
const existing = candidates.get(identity);
|
|
292
|
+
if (existing && existing.reference.locator !== boundCandidate.reference.locator) {
|
|
293
|
+
candidates.delete(identity);
|
|
294
|
+
ambiguousIdentities.add(identity);
|
|
295
|
+
diagnostics.push({
|
|
296
|
+
adapterId: adapter.id,
|
|
297
|
+
code: "ambiguous_reference",
|
|
298
|
+
message: "Native session ID was discovered at multiple source locations"
|
|
299
|
+
});
|
|
300
|
+
} else if (!existing || compareDiscovery(boundCandidate, existing) < 0) {
|
|
301
|
+
candidates.set(identity, boundCandidate);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const sessions = [...candidates.values()].sort(compareDiscovery);
|
|
306
|
+
return {
|
|
307
|
+
binding,
|
|
308
|
+
searchedAdapters: [...uniqueAdapters.keys()],
|
|
309
|
+
sessions,
|
|
310
|
+
diagnostics: diagnostics.map((diagnostic) => ({
|
|
311
|
+
...diagnostic,
|
|
312
|
+
message: safeErrorMessage(diagnostic.message)
|
|
313
|
+
}))
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
var SessionIngestionService = class {
|
|
317
|
+
#repository;
|
|
318
|
+
#adapters;
|
|
319
|
+
#binding;
|
|
320
|
+
#deviceId;
|
|
321
|
+
#rootScopeId;
|
|
322
|
+
#budget;
|
|
323
|
+
#now;
|
|
324
|
+
#pendingByNativeIdentity = /* @__PURE__ */ new Map();
|
|
325
|
+
constructor(options) {
|
|
326
|
+
if (!nonEmpty(options.deviceId)) {
|
|
327
|
+
throw invalidInput("Device ID must be non-empty");
|
|
328
|
+
}
|
|
329
|
+
const adapters = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const adapter of options.adapters) {
|
|
331
|
+
if (adapters.has(adapter.id)) {
|
|
332
|
+
throw invalidInput(`Adapter ${adapter.id} was registered more than once`);
|
|
333
|
+
}
|
|
334
|
+
adapters.set(adapter.id, adapter);
|
|
335
|
+
}
|
|
336
|
+
this.#repository = options.repository;
|
|
337
|
+
this.#adapters = adapters;
|
|
338
|
+
this.#binding = options.binding;
|
|
339
|
+
this.#deviceId = options.deviceId;
|
|
340
|
+
this.#rootScopeId = rootScopeId(options.binding, options.deviceId);
|
|
341
|
+
this.#budget = { ...validateReadBudget(options.budget) };
|
|
342
|
+
this.#now = options.now ?? Date.now;
|
|
343
|
+
}
|
|
344
|
+
discoverSessions(budget = this.#budget) {
|
|
345
|
+
return discoverSessions(this.#binding, [...this.#adapters.values()], budget);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Imports a native transcript or explicitly attaches it to a selected
|
|
349
|
+
* portable session. Supplying a target is the only way to join timelines.
|
|
350
|
+
*/
|
|
351
|
+
ingestNativeSession(input, target, mode = target ? "attach" : "import") {
|
|
352
|
+
const native = normalizeNativeInput(input);
|
|
353
|
+
const key = nativeIdentity(native.reference.adapterId, native.reference.nativeSessionId);
|
|
354
|
+
const previous = this.#pendingByNativeIdentity.get(key);
|
|
355
|
+
const operation = (previous ? previous.catch(() => void 0) : Promise.resolve()).then(() => this.#ingestNativeSession(native, target, mode)).catch((error) => {
|
|
356
|
+
throw ingestionOperationFailure(error);
|
|
357
|
+
});
|
|
358
|
+
this.#pendingByNativeIdentity.set(key, operation);
|
|
359
|
+
void operation.finally(() => {
|
|
360
|
+
if (this.#pendingByNativeIdentity.get(key) === operation) {
|
|
361
|
+
this.#pendingByNativeIdentity.delete(key);
|
|
362
|
+
}
|
|
363
|
+
}).catch(() => void 0);
|
|
364
|
+
return operation;
|
|
365
|
+
}
|
|
366
|
+
/** Refreshes a leg from the exact source locator persisted at import time. */
|
|
367
|
+
async refreshClientLeg(legId) {
|
|
368
|
+
const leg = this.#repository.getLeg(legId);
|
|
369
|
+
if (!leg) {
|
|
370
|
+
throw new SessionOperationError("not_found", `Client leg ${legId} was not found`, ["refresh"]);
|
|
371
|
+
}
|
|
372
|
+
if (!leg.nativeSessionId) {
|
|
373
|
+
throw invalidInput("A client leg without a native session ID cannot be refreshed");
|
|
374
|
+
}
|
|
375
|
+
const session = this.#repository.getSession(leg.sessionId);
|
|
376
|
+
if (!session) {
|
|
377
|
+
throw new SessionOperationError(
|
|
378
|
+
"not_found",
|
|
379
|
+
`Portable session ${leg.sessionId} was not found`,
|
|
380
|
+
["diagnose"]
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
this.#assertSessionWorkspace(session, leg);
|
|
384
|
+
const adapter = this.#getAdapter(leg.adapterId);
|
|
385
|
+
const sourceLocator = stringMetadata(leg.metadata, "sourceLocator");
|
|
386
|
+
let reference;
|
|
387
|
+
if (sourceLocator) {
|
|
388
|
+
reference = {
|
|
389
|
+
adapterId: leg.adapterId,
|
|
390
|
+
nativeSessionId: leg.nativeSessionId,
|
|
391
|
+
locator: sourceLocator,
|
|
392
|
+
workspaceRoot: this.#binding.rootPath
|
|
393
|
+
};
|
|
394
|
+
} else if (adapter.resolveNativeReference) {
|
|
395
|
+
try {
|
|
396
|
+
reference = await adapter.resolveNativeReference({
|
|
397
|
+
nativeSessionId: leg.nativeSessionId,
|
|
398
|
+
workspaceRoot: this.#binding.rootPath
|
|
399
|
+
});
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (error instanceof SessionOperationError) throw sanitizedOperationError(error);
|
|
402
|
+
throw initialSourceFailure("validate", adapter.id, error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (!reference) {
|
|
406
|
+
throw new SessionOperationError(
|
|
407
|
+
"not_found",
|
|
408
|
+
`The native source for client leg ${legId} could not be resolved`,
|
|
409
|
+
["diagnose", "cancel"]
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const sourceStartedAt = timestampMetadata(leg.metadata, "sourceStartedAt");
|
|
413
|
+
const sourceActivityAt2 = timestampMetadata(leg.metadata, "sourceActivityAt") ?? normalizeStoredTimestamp(leg.startedAt, "Stored leg start timestamp");
|
|
414
|
+
const ingestionMode = stringMetadata(leg.metadata, "ingestionMode");
|
|
415
|
+
if (ingestionMode !== "import" && ingestionMode !== "attach") {
|
|
416
|
+
throw new SessionOperationError(
|
|
417
|
+
"out_of_sync",
|
|
418
|
+
"Client leg is missing trusted ingestion provenance",
|
|
419
|
+
["diagnose", "cancel"]
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const target = ingestionMode === "attach" ? {
|
|
423
|
+
sessionId: leg.sessionId,
|
|
424
|
+
resumedFromCheckpointId: leg.resumedFromCheckpointId
|
|
425
|
+
} : void 0;
|
|
426
|
+
return this.ingestNativeSession({
|
|
427
|
+
reference,
|
|
428
|
+
...stringMetadata(leg.metadata, "sourceTitle") ? { title: stringMetadata(leg.metadata, "sourceTitle") } : {},
|
|
429
|
+
...sourceStartedAt !== void 0 ? { startedAt: sourceStartedAt } : {},
|
|
430
|
+
updatedAt: sourceActivityAt2,
|
|
431
|
+
...leg.clientVersion ? { clientVersion: leg.clientVersion } : {},
|
|
432
|
+
...leg.model ? { model: leg.model } : {},
|
|
433
|
+
...stringMetadata(leg.metadata, "gitBranch") ? { gitBranch: stringMetadata(leg.metadata, "gitBranch") } : {},
|
|
434
|
+
...stringMetadata(leg.metadata, "gitCommit") ? { gitCommit: stringMetadata(leg.metadata, "gitCommit") } : {}
|
|
435
|
+
}, target, ingestionMode);
|
|
436
|
+
}
|
|
437
|
+
async #ingestNativeSession(native, target, mode) {
|
|
438
|
+
this.#validateMode(target, mode);
|
|
439
|
+
native = normalizeNativeTimestamps(native);
|
|
440
|
+
this.#assertNativeWorkspaceEvidence(native, mode);
|
|
441
|
+
const adapter = this.#getAdapter(native.reference.adapterId);
|
|
442
|
+
if (adapter.descriptor().capabilities.history.level === "unsupported" || !adapter.readSnapshot) {
|
|
443
|
+
throw new SessionOperationError(
|
|
444
|
+
"adapter_unavailable",
|
|
445
|
+
`${adapter.descriptor().displayName} does not support native history reads`,
|
|
446
|
+
["diagnose", "cancel"]
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
let reference;
|
|
450
|
+
try {
|
|
451
|
+
reference = await this.#resolveReference(adapter, native.reference, mode === "attach");
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (error instanceof SessionOperationError) throw sanitizedOperationError(error);
|
|
454
|
+
throw initialSourceFailure("validate", adapter.id, error);
|
|
455
|
+
}
|
|
456
|
+
native = { ...native, reference };
|
|
457
|
+
const existingLeg = this.#repository.findLeg(
|
|
458
|
+
this.#deviceId,
|
|
459
|
+
reference.adapterId,
|
|
460
|
+
reference.nativeSessionId
|
|
461
|
+
);
|
|
462
|
+
if (mode === "import" && !existingLeg && native.workspaceEvidence?.status !== "verified") {
|
|
463
|
+
throw new SessionOperationError(
|
|
464
|
+
"workspace_mismatch",
|
|
465
|
+
"A new native import requires verified workspace provenance",
|
|
466
|
+
["diagnose", "cancel"]
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (existingLeg) {
|
|
470
|
+
const existingLocator = stringMetadata(existingLeg.metadata, "sourceLocator");
|
|
471
|
+
if (existingLocator && existingLocator !== reference.locator) {
|
|
472
|
+
const durableCursor = this.#repository.findSourceCursor(
|
|
473
|
+
this.#deviceId,
|
|
474
|
+
reference.adapterId,
|
|
475
|
+
existingLocator
|
|
476
|
+
);
|
|
477
|
+
const message = "Native session ID was reused at a different source locator; refusing to merge timelines";
|
|
478
|
+
if (durableCursor) this.#markCursor(durableCursor, "out-of-sync", message);
|
|
479
|
+
throw new SessionOperationError(
|
|
480
|
+
"out_of_sync",
|
|
481
|
+
message,
|
|
482
|
+
["diagnose", "cancel"],
|
|
483
|
+
{ adapterId: reference.adapterId, nativeSessionId: reference.nativeSessionId }
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (target && existingLeg.sessionId !== target.sessionId) {
|
|
487
|
+
throw invalidInput(
|
|
488
|
+
"A native session already belongs to another portable session and cannot be merged"
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const session2 = this.#repository.getSession(existingLeg.sessionId);
|
|
492
|
+
if (!session2) {
|
|
493
|
+
throw new SessionOperationError(
|
|
494
|
+
"not_found",
|
|
495
|
+
`Portable session ${existingLeg.sessionId} was not found`,
|
|
496
|
+
["diagnose"]
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
this.#assertSessionWorkspace(session2, existingLeg);
|
|
500
|
+
this.#assertExistingClaim(existingLeg, target, mode);
|
|
501
|
+
}
|
|
502
|
+
const targetSession = !existingLeg && target ? this.#resolveTarget(target) : null;
|
|
503
|
+
const session = existingLeg ? this.#repository.getSession(existingLeg.sessionId) : targetSession;
|
|
504
|
+
const existingCursor = existingLeg ? this.#repository.findSourceCursor(
|
|
505
|
+
this.#deviceId,
|
|
506
|
+
reference.adapterId,
|
|
507
|
+
reference.locator
|
|
508
|
+
) : null;
|
|
509
|
+
if (existingCursor && existingCursor.legId !== existingLeg?.legId) {
|
|
510
|
+
throw new SessionOperationError(
|
|
511
|
+
"out_of_sync",
|
|
512
|
+
"The native source locator belongs to a different client leg",
|
|
513
|
+
["diagnose", "cancel"]
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
let snapshot;
|
|
517
|
+
try {
|
|
518
|
+
snapshot = await this.#readSnapshot(adapter, reference, existingCursor);
|
|
519
|
+
this.#assertSnapshotReference(snapshot, reference);
|
|
520
|
+
this.#assertSnapshotWorkspaceEvidence(snapshot, mode);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
if (!existingCursor) {
|
|
523
|
+
if (error instanceof SessionOperationError) throw sanitizedOperationError(error);
|
|
524
|
+
throw initialSourceFailure("read", adapter.id, error);
|
|
525
|
+
}
|
|
526
|
+
const message = safeErrorMessage(error);
|
|
527
|
+
this.#markCursor(existingCursor, "out-of-sync", message);
|
|
528
|
+
throw new SessionOperationError(
|
|
529
|
+
"out_of_sync",
|
|
530
|
+
`Native transcript could not continue from its durable cursor: ${message}`,
|
|
531
|
+
["retry", "diagnose"],
|
|
532
|
+
{ adapterId: adapter.id, nativeSessionId: native.reference.nativeSessionId }
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (existingCursor && existingCursor.sourceFingerprint !== snapshot.sourceFingerprint) {
|
|
536
|
+
const message = "Native transcript identity changed; the previous cursor was retained";
|
|
537
|
+
this.#markCursor(existingCursor, "out-of-sync", message);
|
|
538
|
+
throw new SessionOperationError(
|
|
539
|
+
"out_of_sync",
|
|
540
|
+
"Native transcript was truncated or replaced; refusing to merge it into the existing timeline",
|
|
541
|
+
["diagnose", "cancel"],
|
|
542
|
+
{ adapterId: adapter.id, nativeSessionId: native.reference.nativeSessionId }
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
const now = normalizeClockTimestamp(this.#now());
|
|
546
|
+
const parseState = parseStateFor(snapshot);
|
|
547
|
+
const derivedTitle = sessionTitle(native, snapshot);
|
|
548
|
+
let events;
|
|
549
|
+
try {
|
|
550
|
+
events = snapshot.events.map(normalizeEvent);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (existingCursor) this.#markCursor(existingCursor, "out-of-sync", safeErrorMessage(error));
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
const activityAt = sourceActivityAt(native, events, now);
|
|
556
|
+
const startedAt = native.startedAt ?? firstEventAt(events) ?? activityAt;
|
|
557
|
+
const proposedSession = session ? null : {
|
|
558
|
+
sessionId: createId("ses", startedAt),
|
|
559
|
+
workspaceId: this.#binding.status === "bound" ? this.#binding.workspaceId : null,
|
|
560
|
+
title: derivedTitle.title,
|
|
561
|
+
status: "active",
|
|
562
|
+
createdAt: startedAt,
|
|
563
|
+
updatedAt: activityAt,
|
|
564
|
+
headEventId: null,
|
|
565
|
+
headSequence: 0,
|
|
566
|
+
headCheckpointId: null,
|
|
567
|
+
metadata: {
|
|
568
|
+
...initialSessionMetadata(native, activityAt, parseState, derivedTitle.source),
|
|
569
|
+
rootScopeId: this.#rootScopeId
|
|
570
|
+
},
|
|
571
|
+
schemaVersion: SESSION_SCHEMA_VERSION
|
|
572
|
+
};
|
|
573
|
+
const sessionId = existingLeg?.sessionId ?? targetSession?.sessionId ?? proposedSession.sessionId;
|
|
574
|
+
const baseLegMetadata = existingLeg?.metadata ?? initialLegMetadata(native, activityAt, parseState);
|
|
575
|
+
const proposedLeg = existingLeg ?? {
|
|
576
|
+
legId: createId("leg", startedAt),
|
|
577
|
+
sessionId,
|
|
578
|
+
deviceId: this.#deviceId,
|
|
579
|
+
adapterId: reference.adapterId,
|
|
580
|
+
nativeSessionId: reference.nativeSessionId,
|
|
581
|
+
clientVersion: portableScalar(native.clientVersion),
|
|
582
|
+
model: portableScalar(native.model),
|
|
583
|
+
workspaceLocationId: this.#binding.status === "bound" ? this.#binding.locationId : null,
|
|
584
|
+
resumedFromCheckpointId: target?.resumedFromCheckpointId ?? null,
|
|
585
|
+
state: "attached",
|
|
586
|
+
startedAt,
|
|
587
|
+
endedAt: null,
|
|
588
|
+
metadata: {
|
|
589
|
+
...baseLegMetadata,
|
|
590
|
+
rootScopeId: this.#rootScopeId,
|
|
591
|
+
ingestionMode: mode,
|
|
592
|
+
attachedSessionId: mode === "attach" ? sessionId : null,
|
|
593
|
+
attachedCheckpointId: mode === "attach" ? target?.resumedFromCheckpointId ?? null : null
|
|
594
|
+
},
|
|
595
|
+
schemaVersion: SESSION_SCHEMA_VERSION
|
|
596
|
+
};
|
|
597
|
+
const cursorStatus = cursorStatusFor(snapshot);
|
|
598
|
+
const nextCursor = {
|
|
599
|
+
sourceId: existingCursor?.sourceId ?? createId("src", now),
|
|
600
|
+
legId: proposedLeg.legId,
|
|
601
|
+
deviceId: this.#deviceId,
|
|
602
|
+
adapterId: native.reference.adapterId,
|
|
603
|
+
locator: native.reference.locator,
|
|
604
|
+
sourceFingerprint: snapshot.sourceFingerprint,
|
|
605
|
+
cursorKind: snapshot.nextCursor.kind,
|
|
606
|
+
cursorValue: snapshot.nextCursor.value,
|
|
607
|
+
lastSourceEventId: snapshot.nextCursor.lastSourceEventId ?? existingCursor?.lastSourceEventId ?? null,
|
|
608
|
+
parserVersion: snapshot.parserVersion,
|
|
609
|
+
lastSeenAt: monotonicSeenAt(existingCursor, now),
|
|
610
|
+
status: cursorStatus,
|
|
611
|
+
lastError: cursorError(snapshot),
|
|
612
|
+
schemaVersion: SESSION_SCHEMA_VERSION
|
|
613
|
+
};
|
|
614
|
+
const baseSessionMetadata = session?.metadata ?? proposedSession.metadata;
|
|
615
|
+
const sessionMetadata = {
|
|
616
|
+
...mergeSessionMetadata(
|
|
617
|
+
baseSessionMetadata,
|
|
618
|
+
native,
|
|
619
|
+
activityAt,
|
|
620
|
+
parseState,
|
|
621
|
+
session?.headSequence ?? 0,
|
|
622
|
+
snapshot,
|
|
623
|
+
!session
|
|
624
|
+
),
|
|
625
|
+
rootScopeId: this.#rootScopeId
|
|
626
|
+
};
|
|
627
|
+
const legMetadata = {
|
|
628
|
+
...mergeLegMetadata(baseLegMetadata, native, activityAt, parseState, 0, snapshot),
|
|
629
|
+
rootScopeId: this.#rootScopeId,
|
|
630
|
+
ingestionMode: mode,
|
|
631
|
+
attachedSessionId: mode === "attach" ? sessionId : null,
|
|
632
|
+
attachedCheckpointId: mode === "attach" ? target?.resumedFromCheckpointId ?? null : null
|
|
633
|
+
};
|
|
634
|
+
let persisted;
|
|
635
|
+
try {
|
|
636
|
+
persisted = this.#repository.ingestTranscriptPage({
|
|
637
|
+
mode,
|
|
638
|
+
proposedSession,
|
|
639
|
+
proposedLeg,
|
|
640
|
+
targetSessionId: target?.sessionId ?? null,
|
|
641
|
+
rootScopeId: this.#rootScopeId,
|
|
642
|
+
events,
|
|
643
|
+
expectedHeadSequence: session?.headSequence ?? 0,
|
|
644
|
+
expectedCursor: existingCursor,
|
|
645
|
+
nextCursor,
|
|
646
|
+
titleCandidate: derivedTitle,
|
|
647
|
+
sessionMetadata,
|
|
648
|
+
legMetadata,
|
|
649
|
+
updatedAt: Math.max(activityAt, now)
|
|
650
|
+
});
|
|
651
|
+
} catch (error) {
|
|
652
|
+
if (existingCursor && !isHeadChanged(error)) {
|
|
653
|
+
this.#markCursor(existingCursor, "out-of-sync", safeErrorMessage(error));
|
|
654
|
+
}
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
session: persisted.session,
|
|
659
|
+
leg: persisted.leg,
|
|
660
|
+
cursor: persisted.cursor,
|
|
661
|
+
inserted: persisted.inserted,
|
|
662
|
+
createdSession: persisted.createdSession,
|
|
663
|
+
attached: mode === "attach",
|
|
664
|
+
truncated: snapshot.truncated,
|
|
665
|
+
diagnostics: sanitizeDiagnostics(snapshot.diagnostics)
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
async #readSnapshot(adapter, reference, cursor) {
|
|
669
|
+
return adapter.readSnapshot({
|
|
670
|
+
reference,
|
|
671
|
+
budget: this.#budget,
|
|
672
|
+
...cursor ? {
|
|
673
|
+
cursor: {
|
|
674
|
+
kind: cursor.cursorKind,
|
|
675
|
+
value: cursor.cursorValue,
|
|
676
|
+
...cursor.lastSourceEventId ? { lastSourceEventId: cursor.lastSourceEventId } : {}
|
|
677
|
+
}
|
|
678
|
+
} : {}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async #resolveReference(adapter, reference, allowBindingAuthority) {
|
|
682
|
+
if (reference.adapterId !== adapter.id || !nonEmpty(reference.nativeSessionId) || !nonEmpty(reference.locator)) {
|
|
683
|
+
throw invalidInput("Native session reference is incomplete or belongs to another adapter");
|
|
684
|
+
}
|
|
685
|
+
if (reference.workspaceRoot && reference.workspaceRoot !== this.#binding.rootPath) {
|
|
686
|
+
throw new SessionOperationError(
|
|
687
|
+
"workspace_mismatch",
|
|
688
|
+
"Native session reference belongs to another workspace root",
|
|
689
|
+
["cancel"]
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
if (!reference.workspaceRoot && !allowBindingAuthority) {
|
|
693
|
+
throw new SessionOperationError(
|
|
694
|
+
"workspace_mismatch",
|
|
695
|
+
"Native session reference has no verified workspace provenance",
|
|
696
|
+
["diagnose", "cancel"]
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
if (!adapter.resolveNativeReference) {
|
|
700
|
+
return {
|
|
701
|
+
...reference,
|
|
702
|
+
workspaceRoot: reference.workspaceRoot ?? this.#binding.rootPath
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
const resolved = await adapter.resolveNativeReference({
|
|
706
|
+
nativeSessionId: reference.nativeSessionId,
|
|
707
|
+
locator: reference.locator,
|
|
708
|
+
workspaceRoot: this.#binding.rootPath
|
|
709
|
+
});
|
|
710
|
+
if (!resolved || resolved.adapterId !== adapter.id || resolved.nativeSessionId !== reference.nativeSessionId || resolved.locator !== reference.locator) {
|
|
711
|
+
throw new SessionOperationError(
|
|
712
|
+
"malformed_source",
|
|
713
|
+
"Adapter could not validate the native session reference inside its read-only roots",
|
|
714
|
+
["diagnose", "cancel"]
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
if (resolved.workspaceRoot && resolved.workspaceRoot !== this.#binding.rootPath) {
|
|
718
|
+
throw new SessionOperationError(
|
|
719
|
+
"workspace_mismatch",
|
|
720
|
+
"Resolved native session belongs to another workspace root",
|
|
721
|
+
["cancel"]
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
...resolved,
|
|
726
|
+
workspaceRoot: resolved.workspaceRoot ?? reference.workspaceRoot ?? this.#binding.rootPath
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
#validateMode(target, mode) {
|
|
730
|
+
if (mode !== "import" && mode !== "attach") {
|
|
731
|
+
throw invalidInput(`Unsupported ingestion mode: ${String(mode)}`);
|
|
732
|
+
}
|
|
733
|
+
if (mode === "attach" && !target) {
|
|
734
|
+
throw invalidInput("Attach mode requires an explicitly selected portable session");
|
|
735
|
+
}
|
|
736
|
+
if (mode === "import" && target) {
|
|
737
|
+
throw invalidInput("Import mode cannot silently attach to a portable session");
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
#assertNativeWorkspaceEvidence(native, mode) {
|
|
741
|
+
const evidence = native.workspaceEvidence;
|
|
742
|
+
if (!evidence) return;
|
|
743
|
+
if (evidence.status === "verified" && (evidence.root !== this.#binding.rootPath || native.reference.workspaceRoot !== evidence.root)) {
|
|
744
|
+
throw new SessionOperationError(
|
|
745
|
+
"workspace_mismatch",
|
|
746
|
+
"Native session workspace evidence does not match the active root",
|
|
747
|
+
["cancel"]
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
if (mode === "import" && evidence.status !== "verified") {
|
|
751
|
+
throw new SessionOperationError(
|
|
752
|
+
"workspace_mismatch",
|
|
753
|
+
"Native session workspace provenance is not verified for import",
|
|
754
|
+
["diagnose", "cancel"]
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
#resolveTarget(target) {
|
|
759
|
+
if (!nonEmpty(target.sessionId)) throw invalidInput("Target session ID must be non-empty");
|
|
760
|
+
if (this.#binding.status !== "bound") {
|
|
761
|
+
throw new SessionOperationError(
|
|
762
|
+
"workspace_mismatch",
|
|
763
|
+
"An ambiguous workspace cannot attach a native leg to an existing session",
|
|
764
|
+
["cancel"]
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const session = this.#repository.getSession(target.sessionId);
|
|
768
|
+
if (!session) {
|
|
769
|
+
throw new SessionOperationError(
|
|
770
|
+
"not_found",
|
|
771
|
+
`Portable session ${target.sessionId} was not found`,
|
|
772
|
+
["cancel"]
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
this.#assertSessionWorkspace(session);
|
|
776
|
+
const checkpointId = target.resumedFromCheckpointId ?? null;
|
|
777
|
+
if (checkpointId !== null) {
|
|
778
|
+
const checkpoint = this.#repository.getCheckpoint(checkpointId);
|
|
779
|
+
if (!checkpoint || checkpoint.sessionId !== session.sessionId) {
|
|
780
|
+
throw invalidInput("Attach checkpoint does not belong to the selected portable session");
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return session;
|
|
784
|
+
}
|
|
785
|
+
#assertSessionWorkspace(session, leg) {
|
|
786
|
+
const expectedWorkspaceId = this.#binding.status === "bound" ? this.#binding.workspaceId : null;
|
|
787
|
+
const persistedSessionScope = stringMetadata(session.metadata, "rootScopeId");
|
|
788
|
+
const sessionScopeMismatch = this.#binding.status === "unassigned" ? persistedSessionScope !== this.#rootScopeId : persistedSessionScope !== void 0 && persistedSessionScope !== this.#rootScopeId;
|
|
789
|
+
const legScopeMismatch = leg !== void 0 && stringMetadata(leg.metadata, "rootScopeId") !== this.#rootScopeId;
|
|
790
|
+
if (session.workspaceId !== expectedWorkspaceId || sessionScopeMismatch || legScopeMismatch) {
|
|
791
|
+
throw new SessionOperationError(
|
|
792
|
+
"workspace_mismatch",
|
|
793
|
+
"Portable session does not belong to the active workspace binding",
|
|
794
|
+
["cancel"],
|
|
795
|
+
{
|
|
796
|
+
expectedWorkspaceId,
|
|
797
|
+
actualWorkspaceId: session.workspaceId
|
|
798
|
+
}
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
#assertExistingClaim(leg, target, mode) {
|
|
803
|
+
const persistedMode = stringMetadata(leg.metadata, "ingestionMode");
|
|
804
|
+
if (persistedMode !== mode) {
|
|
805
|
+
throw invalidInput("Native leg ingestion mode does not match its persisted provenance");
|
|
806
|
+
}
|
|
807
|
+
if (mode === "import") {
|
|
808
|
+
if (target || leg.resumedFromCheckpointId !== null) {
|
|
809
|
+
throw invalidInput("Imported native leg cannot be reclassified or attached");
|
|
810
|
+
}
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const expectedCheckpointId = target?.resumedFromCheckpointId ?? null;
|
|
814
|
+
const metadataSessionId = stringMetadata(leg.metadata, "attachedSessionId");
|
|
815
|
+
const rawMetadataCheckpointId = leg.metadata.attachedCheckpointId;
|
|
816
|
+
const metadataCheckpointId = rawMetadataCheckpointId === null ? null : typeof rawMetadataCheckpointId === "string" ? rawMetadataCheckpointId : void 0;
|
|
817
|
+
if (!target || target.sessionId !== leg.sessionId || leg.resumedFromCheckpointId !== expectedCheckpointId || metadataSessionId !== leg.sessionId || metadataCheckpointId !== expectedCheckpointId) {
|
|
818
|
+
throw invalidInput("Explicit attach target does not match persisted resume provenance");
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
#assertSnapshotReference(snapshot, reference) {
|
|
822
|
+
if (snapshot.reference.adapterId !== reference.adapterId || snapshot.reference.nativeSessionId !== reference.nativeSessionId || snapshot.reference.locator !== reference.locator) {
|
|
823
|
+
throw new SessionOperationError(
|
|
824
|
+
"malformed_source",
|
|
825
|
+
"Adapter snapshot substituted a different native session reference",
|
|
826
|
+
["diagnose", "cancel"]
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
if (!nonEmpty(snapshot.sourceFingerprint) || Buffer.byteLength(snapshot.sourceFingerprint, "utf8") > 512 || /[\0-\x1f\x7f]/u.test(snapshot.sourceFingerprint)) {
|
|
830
|
+
throw new SessionOperationError(
|
|
831
|
+
"malformed_source",
|
|
832
|
+
"Adapter snapshot did not provide a valid bounded source fingerprint",
|
|
833
|
+
["diagnose", "cancel"]
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
#assertSnapshotWorkspaceEvidence(snapshot, mode) {
|
|
838
|
+
const evidence = normalizedWorkspaceEvidence(snapshot.workspaceEvidence);
|
|
839
|
+
if (evidence.status === "verified") {
|
|
840
|
+
if (evidence.root !== this.#binding.rootPath || snapshot.reference.workspaceRoot !== evidence.root) {
|
|
841
|
+
throw new SessionOperationError(
|
|
842
|
+
"workspace_mismatch",
|
|
843
|
+
"Snapshot workspace evidence does not match the active root",
|
|
844
|
+
["cancel"]
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (snapshot.reference.workspaceRoot !== void 0 || mode === "import") {
|
|
850
|
+
throw new SessionOperationError(
|
|
851
|
+
"workspace_mismatch",
|
|
852
|
+
"Snapshot workspace provenance is not verified for import",
|
|
853
|
+
["diagnose", "cancel"]
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
#getAdapter(adapterId) {
|
|
858
|
+
const adapter = this.#adapters.get(adapterId);
|
|
859
|
+
if (!adapter) {
|
|
860
|
+
throw new SessionOperationError(
|
|
861
|
+
"adapter_unavailable",
|
|
862
|
+
`Adapter ${adapterId} is not registered in this ingestion service`,
|
|
863
|
+
["setup", "cancel"]
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
return adapter;
|
|
867
|
+
}
|
|
868
|
+
#markCursor(cursor, status, message) {
|
|
869
|
+
try {
|
|
870
|
+
this.#repository.markIngestionSourceStatus({
|
|
871
|
+
expectedCursor: cursor,
|
|
872
|
+
rootScopeId: this.#rootScopeId,
|
|
873
|
+
status,
|
|
874
|
+
lastError: redactPortableText(message).slice(0, 2e3),
|
|
875
|
+
lastSeenAt: monotonicSeenAt(cursor, normalizeClockTimestamp(this.#now()))
|
|
876
|
+
});
|
|
877
|
+
} catch (error) {
|
|
878
|
+
if (isHeadChanged(error)) throw error;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
function normalizeNativeInput(input) {
|
|
883
|
+
if ("reference" in input) return { ...input, reference: { ...input.reference } };
|
|
884
|
+
return { reference: { ...input } };
|
|
885
|
+
}
|
|
886
|
+
function normalizeNativeTimestamps(native) {
|
|
887
|
+
return {
|
|
888
|
+
...native,
|
|
889
|
+
...native.startedAt === void 0 ? {} : { startedAt: normalizeSourceTimestamp(native.startedAt, "Native session start timestamp") },
|
|
890
|
+
...native.updatedAt === void 0 ? {} : { updatedAt: normalizeSourceTimestamp(native.updatedAt, "Native session activity timestamp") }
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function nativeIdentity(adapterId, nativeSessionId) {
|
|
894
|
+
return `${adapterId}\0${nativeSessionId}`;
|
|
895
|
+
}
|
|
896
|
+
function compareDiscovery(left, right) {
|
|
897
|
+
return right.updatedAt - left.updatedAt || left.reference.adapterId.localeCompare(right.reference.adapterId) || left.reference.nativeSessionId.localeCompare(right.reference.nativeSessionId) || left.reference.locator.localeCompare(right.reference.locator);
|
|
898
|
+
}
|
|
899
|
+
function workspaceEvidence(candidate) {
|
|
900
|
+
return normalizedWorkspaceEvidence(candidate.workspaceEvidence);
|
|
901
|
+
}
|
|
902
|
+
function normalizedWorkspaceEvidence(evidence) {
|
|
903
|
+
const candidate = evidence;
|
|
904
|
+
if (candidate?.status === "verified" && validWorkspaceRoot(candidate.root) && ["claude.cwd", "codex.session_meta.cwd", "continue.workspaceDirectory"].includes(String(candidate.source))) {
|
|
905
|
+
return {
|
|
906
|
+
status: "verified",
|
|
907
|
+
root: candidate.root,
|
|
908
|
+
source: candidate.source
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
if (candidate?.status === "hint" && ["claude.path-key", "cursor.path-key"].includes(String(candidate.source))) {
|
|
912
|
+
return {
|
|
913
|
+
status: "hint",
|
|
914
|
+
source: candidate.source
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return { status: "unknown", source: "none" };
|
|
918
|
+
}
|
|
919
|
+
function sanitizeDiscoveredSession(candidate, evidence) {
|
|
920
|
+
const metadata = redactJson(candidate.metadata ?? {});
|
|
921
|
+
const startedAt = candidate.startedAt === void 0 ? void 0 : normalizeSourceTimestamp(candidate.startedAt, "Native session start timestamp");
|
|
922
|
+
return {
|
|
923
|
+
reference: {
|
|
924
|
+
adapterId: candidate.reference.adapterId,
|
|
925
|
+
nativeSessionId: candidate.reference.nativeSessionId,
|
|
926
|
+
locator: candidate.reference.locator,
|
|
927
|
+
workspaceRoot: evidence.root
|
|
928
|
+
},
|
|
929
|
+
workspaceEvidence: evidence,
|
|
930
|
+
...normalizeTitle(candidate.title) ? { title: normalizeTitle(candidate.title) } : {},
|
|
931
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
932
|
+
updatedAt: normalizeSourceTimestamp(
|
|
933
|
+
candidate.updatedAt,
|
|
934
|
+
"Native session activity timestamp"
|
|
935
|
+
),
|
|
936
|
+
...portableScalar(candidate.clientVersion) ? { clientVersion: portableScalar(candidate.clientVersion) } : {},
|
|
937
|
+
...portableScalar(candidate.model) ? { model: portableScalar(candidate.model) } : {},
|
|
938
|
+
...portableScalar(candidate.gitBranch) ? { gitBranch: portableScalar(candidate.gitBranch) } : {},
|
|
939
|
+
...portableScalar(candidate.gitCommit) ? { gitCommit: portableScalar(candidate.gitCommit) } : {},
|
|
940
|
+
metadata: metadata.value
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function unassignedDiscoveryChoice(candidate, evidence) {
|
|
944
|
+
return {
|
|
945
|
+
reference: {
|
|
946
|
+
adapterId: candidate.reference.adapterId,
|
|
947
|
+
nativeSessionId: candidate.reference.nativeSessionId,
|
|
948
|
+
locator: candidate.reference.locator
|
|
949
|
+
},
|
|
950
|
+
workspaceEvidence: evidence,
|
|
951
|
+
updatedAt: normalizeSourceTimestamp(
|
|
952
|
+
candidate.updatedAt,
|
|
953
|
+
"Native session activity timestamp"
|
|
954
|
+
)
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function validWorkspaceRoot(value) {
|
|
958
|
+
return typeof value === "string" && value.length > 0 && Buffer.byteLength(value, "utf8") <= 4096 && !/[\0-\x1f\x7f]/u.test(value) && isAbsolute(value);
|
|
959
|
+
}
|
|
960
|
+
function normalizeEvent(event) {
|
|
961
|
+
assertEventAuthority(event);
|
|
962
|
+
const text = event.textContent === void 0 ? void 0 : redactPortableText(event.textContent);
|
|
963
|
+
const payload = redactJson(event.payload ?? {});
|
|
964
|
+
const textWasRedacted = text !== event.textContent;
|
|
965
|
+
return {
|
|
966
|
+
sourceEventId: event.sourceEventId,
|
|
967
|
+
...event.sourceSequence !== void 0 ? { sourceSequence: event.sourceSequence } : {},
|
|
968
|
+
kind: event.kind,
|
|
969
|
+
...event.role !== void 0 ? { role: event.role } : {},
|
|
970
|
+
occurredAt: normalizeSourceTimestamp(event.occurredAt, "Event timestamp"),
|
|
971
|
+
...text !== void 0 ? { textContent: text } : {},
|
|
972
|
+
payload: payload.value,
|
|
973
|
+
redactionState: textWasRedacted || payload.redacted ? "redacted" : "none"
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function assertEventAuthority(event) {
|
|
977
|
+
if (!nonEmpty(event.sourceEventId)) {
|
|
978
|
+
throw new SessionOperationError(
|
|
979
|
+
"malformed_source",
|
|
980
|
+
"Adapter emitted an event without a stable source ID",
|
|
981
|
+
["diagnose"]
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
if (event.kind === "message.user" && event.role !== "user") {
|
|
985
|
+
throw new SessionOperationError(
|
|
986
|
+
"malformed_source",
|
|
987
|
+
"Adapter emitted a user message without verified user provenance",
|
|
988
|
+
["diagnose"]
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (event.kind === "message.assistant" && event.role !== "assistant") {
|
|
992
|
+
throw new SessionOperationError(
|
|
993
|
+
"malformed_source",
|
|
994
|
+
"Adapter emitted an assistant message with inconsistent provenance",
|
|
995
|
+
["diagnose"]
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function redactJson(value, seen = /* @__PURE__ */ new Set(), depth = 0) {
|
|
1000
|
+
if (value === null || typeof value === "boolean") return { value, redacted: false };
|
|
1001
|
+
if (typeof value === "number") {
|
|
1002
|
+
return Number.isFinite(value) ? { value, redacted: false } : { value: String(value), redacted: true };
|
|
1003
|
+
}
|
|
1004
|
+
if (typeof value === "string") {
|
|
1005
|
+
const redacted = boundedPortableText(value, METADATA_STRING_MAX_CHARS);
|
|
1006
|
+
return { value: redacted, redacted: redacted !== value };
|
|
1007
|
+
}
|
|
1008
|
+
if (typeof value !== "object") {
|
|
1009
|
+
const stringified = boundedPortableText(String(value), METADATA_STRING_MAX_CHARS);
|
|
1010
|
+
return { value: stringified, redacted: true };
|
|
1011
|
+
}
|
|
1012
|
+
if (seen.has(value)) return { value: "[REDACTED CIRCULAR VALUE]", redacted: true };
|
|
1013
|
+
if (depth >= 12) return { value: "[TRUNCATED NESTED VALUE]", redacted: true };
|
|
1014
|
+
seen.add(value);
|
|
1015
|
+
try {
|
|
1016
|
+
if (Array.isArray(value)) {
|
|
1017
|
+
let changed2 = false;
|
|
1018
|
+
const result2 = value.slice(0, 1e3).map((item) => {
|
|
1019
|
+
const nested = redactJson(item, seen, depth + 1);
|
|
1020
|
+
changed2 ||= nested.redacted;
|
|
1021
|
+
return nested.value;
|
|
1022
|
+
});
|
|
1023
|
+
return { value: result2, redacted: changed2 || value.length > result2.length };
|
|
1024
|
+
}
|
|
1025
|
+
let changed = false;
|
|
1026
|
+
const result = {};
|
|
1027
|
+
for (const [key, item] of Object.entries(value).slice(0, 1e3)) {
|
|
1028
|
+
const portableKey = boundedPortableText(key, METADATA_KEY_MAX_CHARS) || "[REDACTED KEY]";
|
|
1029
|
+
if (Object.prototype.hasOwnProperty.call(result, portableKey)) {
|
|
1030
|
+
changed = true;
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (sensitiveKey(key)) {
|
|
1034
|
+
result[portableKey] = "[REDACTED]";
|
|
1035
|
+
changed = true;
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
const nested = redactJson(item, seen, depth + 1);
|
|
1039
|
+
result[portableKey] = nested.value;
|
|
1040
|
+
changed ||= portableKey !== key || nested.redacted;
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
value: result,
|
|
1044
|
+
redacted: changed || Object.keys(value).length > Object.keys(result).length
|
|
1045
|
+
};
|
|
1046
|
+
} finally {
|
|
1047
|
+
seen.delete(value);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function sensitiveKey(key) {
|
|
1051
|
+
return /(?:authorization|cookie|api[-_]?key|api[-_]?token|access[-_]?key|secret|token|password|passwd|private[-_]?key)/i.test(
|
|
1052
|
+
key
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
function sessionTitle(native, snapshot) {
|
|
1056
|
+
const nativeTitle = normalizeTitle(native.title);
|
|
1057
|
+
if (nativeTitle) return { title: nativeTitle, source: "native" };
|
|
1058
|
+
const firstUserMessage = snapshot.events.find(
|
|
1059
|
+
(event) => event.kind === "message.user" && event.role === "user" && nonEmpty(event.textContent)
|
|
1060
|
+
)?.textContent;
|
|
1061
|
+
const userTitle = normalizeTitle(firstUserMessage);
|
|
1062
|
+
return userTitle ? { title: userTitle, source: "user" } : { title: `Untitled ${native.reference.adapterId} session`, source: "fallback" };
|
|
1063
|
+
}
|
|
1064
|
+
function normalizeTitle(value) {
|
|
1065
|
+
if (!value) return null;
|
|
1066
|
+
const redacted = redactPortableText(value).replace(/\s+/gu, " ").trim();
|
|
1067
|
+
if (!redacted) return null;
|
|
1068
|
+
return [...redacted].slice(0, SESSION_TITLE_MAX_CHARS).join("");
|
|
1069
|
+
}
|
|
1070
|
+
function portableScalar(value) {
|
|
1071
|
+
if (!value) return null;
|
|
1072
|
+
const portable = redactPortableText(value).trim();
|
|
1073
|
+
return portable ? [...portable].slice(0, 512).join("") : null;
|
|
1074
|
+
}
|
|
1075
|
+
function boundedPortableText(value, maxChars) {
|
|
1076
|
+
return [...redactPortableText(value)].slice(0, maxChars).join("");
|
|
1077
|
+
}
|
|
1078
|
+
function sourceActivityAt(native, events, fallback) {
|
|
1079
|
+
const timestamps = [native.updatedAt, ...events.map((event) => event.occurredAt)].filter((value) => value !== void 0);
|
|
1080
|
+
return timestamps.length > 0 ? Math.max(...timestamps) : fallback;
|
|
1081
|
+
}
|
|
1082
|
+
function firstEventAt(events) {
|
|
1083
|
+
return events.map((event) => event.occurredAt).sort((left, right) => left - right)[0];
|
|
1084
|
+
}
|
|
1085
|
+
function normalizeSourceTimestamp(value, name) {
|
|
1086
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) {
|
|
1087
|
+
throw new SessionOperationError(
|
|
1088
|
+
"malformed_source",
|
|
1089
|
+
`${name} must be a finite non-negative safe millisecond value`,
|
|
1090
|
+
["diagnose", "cancel"]
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
return Math.floor(value);
|
|
1094
|
+
}
|
|
1095
|
+
function normalizeClockTimestamp(value) {
|
|
1096
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) {
|
|
1097
|
+
throw invalidInput("Ingestion clock must return a finite non-negative safe millisecond value");
|
|
1098
|
+
}
|
|
1099
|
+
return Math.floor(value);
|
|
1100
|
+
}
|
|
1101
|
+
function cursorStatusFor(snapshot) {
|
|
1102
|
+
if (snapshot.diagnostics.some((diagnostic) => /unsupported/i.test(diagnostic.code))) {
|
|
1103
|
+
return "unsupported";
|
|
1104
|
+
}
|
|
1105
|
+
if (snapshot.diagnostics.some(
|
|
1106
|
+
(diagnostic) => diagnostic.level === "error" || /^malformed(?:_|$)/i.test(diagnostic.code)
|
|
1107
|
+
)) {
|
|
1108
|
+
return "malformed";
|
|
1109
|
+
}
|
|
1110
|
+
return "active";
|
|
1111
|
+
}
|
|
1112
|
+
function parseStateFor(snapshot) {
|
|
1113
|
+
const status = cursorStatusFor(snapshot);
|
|
1114
|
+
if (status !== "active") return status;
|
|
1115
|
+
if (snapshot.truncated || snapshot.diagnostics.some((diagnostic) => diagnostic.code === "incomplete_record")) {
|
|
1116
|
+
return "partial";
|
|
1117
|
+
}
|
|
1118
|
+
return "complete";
|
|
1119
|
+
}
|
|
1120
|
+
function cursorError(snapshot) {
|
|
1121
|
+
const material = snapshot.diagnostics.filter(
|
|
1122
|
+
(diagnostic) => diagnostic.level === "error" || /^malformed(?:_|$)/i.test(diagnostic.code) || /unsupported/i.test(diagnostic.code)
|
|
1123
|
+
);
|
|
1124
|
+
if (material.length === 0) return null;
|
|
1125
|
+
return redactPortableText(
|
|
1126
|
+
material.map((diagnostic) => `[${diagnostic.code}] ${diagnostic.message}`).join("; ")
|
|
1127
|
+
).slice(0, 2e3);
|
|
1128
|
+
}
|
|
1129
|
+
function sanitizeDiagnostics(diagnostics) {
|
|
1130
|
+
return diagnostics.slice(0, 1e3).map((diagnostic) => {
|
|
1131
|
+
const candidate = diagnostic;
|
|
1132
|
+
const code = typeof candidate?.code === "string" ? boundedPortableText(candidate.code, 128) : "invalid_diagnostic";
|
|
1133
|
+
const message = typeof candidate?.message === "string" ? boundedPortableText(candidate.message, 2e3) : "Adapter returned an invalid diagnostic";
|
|
1134
|
+
const sanitized = {
|
|
1135
|
+
level: candidate?.level === "error" ? "error" : "warning",
|
|
1136
|
+
code: code || "invalid_diagnostic",
|
|
1137
|
+
message
|
|
1138
|
+
};
|
|
1139
|
+
if (typeof candidate?.sourceSequence === "number" && Number.isSafeInteger(candidate.sourceSequence) && candidate.sourceSequence >= 0) {
|
|
1140
|
+
sanitized.sourceSequence = candidate.sourceSequence;
|
|
1141
|
+
}
|
|
1142
|
+
return sanitized;
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
function initialSessionMetadata(native, activityAt, parseState, titleSource) {
|
|
1146
|
+
return {
|
|
1147
|
+
...mergeSessionMetadata(
|
|
1148
|
+
{},
|
|
1149
|
+
native,
|
|
1150
|
+
activityAt,
|
|
1151
|
+
parseState,
|
|
1152
|
+
0,
|
|
1153
|
+
null,
|
|
1154
|
+
true
|
|
1155
|
+
),
|
|
1156
|
+
titleSource
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function mergeSessionMetadata(existing, native, activityAt, parseState, eventCount, snapshot, preserveOrigin) {
|
|
1160
|
+
const metadata = {
|
|
1161
|
+
...existing,
|
|
1162
|
+
lastSourceAdapter: native.reference.adapterId,
|
|
1163
|
+
lastSourceNativeSessionId: native.reference.nativeSessionId,
|
|
1164
|
+
sourceActivityAt: Math.max(
|
|
1165
|
+
timestampMetadata(existing, "sourceActivityAt") ?? 0,
|
|
1166
|
+
activityAt
|
|
1167
|
+
),
|
|
1168
|
+
eventCount,
|
|
1169
|
+
parseState,
|
|
1170
|
+
provenance: "imported-history",
|
|
1171
|
+
...native.gitBranch ? { gitBranch: portableScalar(native.gitBranch) } : {},
|
|
1172
|
+
...native.gitCommit ? { gitCommit: portableScalar(native.gitCommit) } : {},
|
|
1173
|
+
...snapshot ? { sourceTruncated: snapshot.truncated } : {}
|
|
1174
|
+
};
|
|
1175
|
+
if (preserveOrigin || !("sourceAdapter" in existing)) {
|
|
1176
|
+
metadata.sourceAdapter = native.reference.adapterId;
|
|
1177
|
+
metadata.sourceNativeSessionId = native.reference.nativeSessionId;
|
|
1178
|
+
}
|
|
1179
|
+
return metadata;
|
|
1180
|
+
}
|
|
1181
|
+
function initialLegMetadata(native, activityAt, parseState) {
|
|
1182
|
+
const adapterMetadata = redactJson(native.metadata ?? {});
|
|
1183
|
+
return {
|
|
1184
|
+
sourceAdapter: native.reference.adapterId,
|
|
1185
|
+
sourceNativeSessionId: native.reference.nativeSessionId,
|
|
1186
|
+
sourceLocator: native.reference.locator,
|
|
1187
|
+
sourceTitle: normalizeTitle(native.title),
|
|
1188
|
+
sourceStartedAt: native.startedAt ?? null,
|
|
1189
|
+
sourceActivityAt: activityAt,
|
|
1190
|
+
gitBranch: portableScalar(native.gitBranch),
|
|
1191
|
+
gitCommit: portableScalar(native.gitCommit),
|
|
1192
|
+
eventCount: 0,
|
|
1193
|
+
parseState,
|
|
1194
|
+
provenance: "imported-history",
|
|
1195
|
+
adapterMetadata: adapterMetadata.value
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
function mergeLegMetadata(existing, native, activityAt, parseState, inserted, snapshot) {
|
|
1199
|
+
const previousCount = numberMetadata(existing, "eventCount") ?? 0;
|
|
1200
|
+
const diagnostics = redactJson(sanitizeDiagnostics(snapshot.diagnostics));
|
|
1201
|
+
return {
|
|
1202
|
+
...existing,
|
|
1203
|
+
sourceAdapter: native.reference.adapterId,
|
|
1204
|
+
sourceNativeSessionId: native.reference.nativeSessionId,
|
|
1205
|
+
sourceLocator: native.reference.locator,
|
|
1206
|
+
sourceTitle: normalizeTitle(native.title),
|
|
1207
|
+
sourceStartedAt: native.startedAt ?? timestampMetadata(existing, "sourceStartedAt") ?? null,
|
|
1208
|
+
sourceActivityAt: Math.max(
|
|
1209
|
+
timestampMetadata(existing, "sourceActivityAt") ?? 0,
|
|
1210
|
+
activityAt
|
|
1211
|
+
),
|
|
1212
|
+
gitBranch: native.gitBranch ? portableScalar(native.gitBranch) : existing.gitBranch ?? null,
|
|
1213
|
+
gitCommit: native.gitCommit ? portableScalar(native.gitCommit) : existing.gitCommit ?? null,
|
|
1214
|
+
eventCount: previousCount + inserted,
|
|
1215
|
+
parseState,
|
|
1216
|
+
sourceTruncated: snapshot.truncated,
|
|
1217
|
+
parseDiagnostics: diagnostics.value,
|
|
1218
|
+
provenance: "imported-history"
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function monotonicSeenAt(cursor, now) {
|
|
1222
|
+
if (!cursor) return now;
|
|
1223
|
+
const previous = normalizeStoredTimestamp(cursor.lastSeenAt, "Stored cursor timestamp");
|
|
1224
|
+
if (previous === Number.MAX_SAFE_INTEGER) {
|
|
1225
|
+
throw new SessionOperationError(
|
|
1226
|
+
"out_of_sync",
|
|
1227
|
+
"Stored cursor timestamp cannot advance safely",
|
|
1228
|
+
["diagnose", "cancel"]
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
return Math.max(now, previous + 1);
|
|
1232
|
+
}
|
|
1233
|
+
function stringMetadata(metadata, key) {
|
|
1234
|
+
const value = metadata[key];
|
|
1235
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1236
|
+
}
|
|
1237
|
+
function numberMetadata(metadata, key) {
|
|
1238
|
+
const value = metadata[key];
|
|
1239
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1240
|
+
}
|
|
1241
|
+
function timestampMetadata(metadata, key) {
|
|
1242
|
+
const value = metadata[key];
|
|
1243
|
+
return value === void 0 || value === null ? void 0 : normalizeStoredTimestamp(value, `Stored ${key} timestamp`);
|
|
1244
|
+
}
|
|
1245
|
+
function normalizeStoredTimestamp(value, name) {
|
|
1246
|
+
try {
|
|
1247
|
+
return normalizeSourceTimestamp(value, name);
|
|
1248
|
+
} catch {
|
|
1249
|
+
throw new SessionOperationError(
|
|
1250
|
+
"out_of_sync",
|
|
1251
|
+
`${name} is invalid`,
|
|
1252
|
+
["diagnose", "cancel"]
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
function errorMessage(error) {
|
|
1257
|
+
return error instanceof Error ? error.message : String(error);
|
|
1258
|
+
}
|
|
1259
|
+
function safeErrorMessage(error) {
|
|
1260
|
+
return boundedPortableText(errorMessage(error), 2e3);
|
|
1261
|
+
}
|
|
1262
|
+
function initialSourceFailure(operation, adapterId, error) {
|
|
1263
|
+
return new SessionOperationError(
|
|
1264
|
+
"malformed_source",
|
|
1265
|
+
`Adapter ${adapterId} could not ${operation} the selected native transcript: ${safeErrorMessage(error)}`,
|
|
1266
|
+
["retry", "diagnose", "cancel"],
|
|
1267
|
+
{ adapterId }
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
function sanitizedOperationError(error) {
|
|
1271
|
+
const code = typeof error.code === "string" && OPERATION_ERROR_CODES.has(error.code) ? error.code : "invalid_input";
|
|
1272
|
+
const alternatives = [];
|
|
1273
|
+
if (Array.isArray(error.alternatives)) {
|
|
1274
|
+
for (const alternative of error.alternatives) {
|
|
1275
|
+
if (typeof alternative === "string" && OPERATION_ALTERNATIVES.has(alternative) && !alternatives.includes(alternative)) {
|
|
1276
|
+
alternatives.push(alternative);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const redactedDetails = redactJson(error.details).value;
|
|
1281
|
+
const details = redactedDetails !== null && typeof redactedDetails === "object" && !Array.isArray(redactedDetails) ? redactedDetails : {};
|
|
1282
|
+
return new SessionOperationError(
|
|
1283
|
+
code,
|
|
1284
|
+
safeErrorMessage(error),
|
|
1285
|
+
alternatives,
|
|
1286
|
+
details
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
function ingestionOperationFailure(error) {
|
|
1290
|
+
if (error instanceof SessionOperationError) return sanitizedOperationError(error);
|
|
1291
|
+
return new SessionOperationError(
|
|
1292
|
+
"invalid_input",
|
|
1293
|
+
`Native session ingestion failed: ${safeErrorMessage(error)}`,
|
|
1294
|
+
["retry", "diagnose", "cancel"]
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
function isHeadChanged(error) {
|
|
1298
|
+
return error instanceof SessionOperationError && error.code === "head_changed";
|
|
1299
|
+
}
|
|
1300
|
+
function rootScopeId(binding, deviceId) {
|
|
1301
|
+
if (binding.status === "bound") return binding.workspaceId;
|
|
1302
|
+
return `urs_${createHash("sha256").update(deviceId).update("\0").update(binding.rootPath).digest("hex")}`;
|
|
1303
|
+
}
|
|
1304
|
+
function nonEmpty(value) {
|
|
1305
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1306
|
+
}
|
|
1307
|
+
function invalidInput(message) {
|
|
1308
|
+
return new SessionOperationError("invalid_input", message, ["cancel"]);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/commands/session.ts
|
|
1312
|
+
var MAX_TEXT_CHARS = 16e3;
|
|
1313
|
+
var MAX_FILE_CHARS = 64e3;
|
|
1314
|
+
var MAX_QUERY_CHARS = 256;
|
|
1315
|
+
var MAX_LIMIT = 100;
|
|
1316
|
+
var FATAL_UTF8_DECODER = new TextDecoder("utf-8", {
|
|
1317
|
+
fatal: true,
|
|
1318
|
+
ignoreBOM: false
|
|
14
1319
|
});
|
|
15
|
-
|
|
1320
|
+
var EVENT_KINDS = /* @__PURE__ */ new Set([
|
|
1321
|
+
"message.user",
|
|
1322
|
+
"message.assistant",
|
|
1323
|
+
"tool.call",
|
|
1324
|
+
"tool.result",
|
|
1325
|
+
"checkpoint",
|
|
1326
|
+
"correction",
|
|
1327
|
+
"tombstone"
|
|
1328
|
+
]);
|
|
1329
|
+
var SESSION_STATES = /* @__PURE__ */ new Set([
|
|
1330
|
+
"active",
|
|
1331
|
+
"checkpointed",
|
|
1332
|
+
"closed",
|
|
1333
|
+
"archived"
|
|
1334
|
+
]);
|
|
1335
|
+
var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1336
|
+
"not_found",
|
|
1337
|
+
"adapter_unavailable",
|
|
1338
|
+
"unsupported_version",
|
|
1339
|
+
"malformed_source",
|
|
1340
|
+
"workspace_mismatch",
|
|
1341
|
+
"head_changed",
|
|
1342
|
+
"handoff_expired",
|
|
1343
|
+
"handoff_consumed",
|
|
1344
|
+
"wrong_destination",
|
|
1345
|
+
"lease_conflict",
|
|
1346
|
+
"invalid_token",
|
|
1347
|
+
"out_of_sync",
|
|
1348
|
+
"invalid_input"
|
|
1349
|
+
]);
|
|
1350
|
+
function createSessionCommand(dependencies = {}) {
|
|
1351
|
+
const deps = resolveDependencies(dependencies);
|
|
1352
|
+
const command = new Command2("session").description("Manage portable cross-agent sessions");
|
|
1353
|
+
command.command("list").description("Discover and list portable sessions for the current workspace").option("--adapter <id>", "Filter by source adapter").option("--status <status>", "Filter by session state").option("--json", "Print machine-readable JSON").action(async (options) => {
|
|
1354
|
+
const adapterId = options.adapter === void 0 ? void 0 : requireAdapter2(deps, options.adapter, "adapter");
|
|
1355
|
+
const status = options.status === void 0 ? void 0 : sessionState(options.status);
|
|
1356
|
+
await withContext(deps, PORTABLE_CLI_ADAPTER_ID, async (context) => {
|
|
1357
|
+
const discovery = await context.discover();
|
|
1358
|
+
const sessions = context.service.listSessions({
|
|
1359
|
+
...adapterId === void 0 ? {} : { adapterId },
|
|
1360
|
+
...status === void 0 ? {} : { status }
|
|
1361
|
+
});
|
|
1362
|
+
const result = {
|
|
1363
|
+
diagnostics: publicDiagnostics(discovery.diagnostics),
|
|
1364
|
+
discovery: publicDiscovery(discovery),
|
|
1365
|
+
sessions: sessions.map((item) => publicSessionItem(item, context.project))
|
|
1366
|
+
};
|
|
1367
|
+
if (options.json) return writeJson2(deps.write, result);
|
|
1368
|
+
writeDiscoveryDiagnostics(deps.write, discovery);
|
|
1369
|
+
writeSessionList(deps.write, result.sessions, false, sessions);
|
|
1370
|
+
});
|
|
1371
|
+
});
|
|
1372
|
+
command.command("inspect <session-id>").description("Inspect a portable session").option("--json", "Print machine-readable JSON").action(async (sessionId, options) => {
|
|
1373
|
+
await withContext(deps, PORTABLE_CLI_ADAPTER_ID, async (context) => {
|
|
1374
|
+
const diagnostics = context.workspaceDiagnostics ?? [];
|
|
1375
|
+
if (!options.json) writeDiagnostics(deps.write, diagnostics);
|
|
1376
|
+
const inspection = context.service.inspectSession(identifier(sessionId, "Session ID"));
|
|
1377
|
+
const result = {
|
|
1378
|
+
...publicInspection(inspection, context.project),
|
|
1379
|
+
diagnostics: publicDiagnostics(diagnostics)
|
|
1380
|
+
};
|
|
1381
|
+
if (options.json) return writeJson2(deps.write, result);
|
|
1382
|
+
writeInspection(deps.write, result, inspection, context.workspacePath);
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
command.command("resume [session-id]").description("Prepare an explicit handoff to another adapter").requiredOption("--to <adapter>", "Destination adapter").option("--checkpoint <id>", "Resume from a specific checkpoint").option("--fork", "Fork instead of continuing the selected session").option("--confirm-active-source", "Confirm takeover from an active source").option("--exclude-event <event-id...>", "Exclude selected event IDs from this capsule").option("--json", "Print machine-readable JSON without prompting").action(async (sessionId, options) => {
|
|
1386
|
+
const destinationAdapterId = requireAdapter2(deps, options.to, "destination adapter");
|
|
1387
|
+
if (!sessionId && (options.json || !deps.isTTY)) {
|
|
1388
|
+
throw invalidInput2(
|
|
1389
|
+
"Pass a session ID or run this command in an interactive terminal"
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
if (!options.json && !deps.isTTY) {
|
|
1393
|
+
throw invalidInput2("Interactive confirmation requires a terminal; use --json for scripts");
|
|
1394
|
+
}
|
|
1395
|
+
await withContext(deps, PORTABLE_CLI_ADAPTER_ID, async (context) => {
|
|
1396
|
+
const discovery = await context.discover();
|
|
1397
|
+
if (!options.json) writeDiscoveryDiagnostics(deps.write, discovery);
|
|
1398
|
+
let selected;
|
|
1399
|
+
if (sessionId) {
|
|
1400
|
+
selected = {
|
|
1401
|
+
sessionId: identifier(sessionId, "Session ID"),
|
|
1402
|
+
mode: options.fork ? "fork" : "continue",
|
|
1403
|
+
confirmActiveSource: options.confirmActiveSource === true
|
|
1404
|
+
};
|
|
1405
|
+
} else {
|
|
1406
|
+
selected = await pickSession(context, deps, discovery);
|
|
1407
|
+
if (selected === null) return;
|
|
1408
|
+
if (!selected) {
|
|
1409
|
+
deps.write("Resume cancelled.\n");
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
const inspection = context.service.inspectSession(selected.sessionId);
|
|
1414
|
+
const prepareInput = {
|
|
1415
|
+
sessionId: inspection.session.sessionId,
|
|
1416
|
+
...options.checkpoint === void 0 ? {} : { checkpointId: identifier(options.checkpoint, "Checkpoint ID") },
|
|
1417
|
+
destinationAdapterId,
|
|
1418
|
+
mode: selected.mode,
|
|
1419
|
+
expectedHeadSequence: inspection.session.headSequence,
|
|
1420
|
+
...selected.confirmActiveSource ? { confirmActiveSource: true } : {},
|
|
1421
|
+
...options.excludeEvent === void 0 ? {} : { excludeEventIds: eventExclusionIds(options.excludeEvent) }
|
|
1422
|
+
};
|
|
1423
|
+
const prepared = await context.service.prepareResume(prepareInput);
|
|
1424
|
+
await finishPreparedResume(
|
|
1425
|
+
context,
|
|
1426
|
+
deps,
|
|
1427
|
+
prepareInput,
|
|
1428
|
+
prepared,
|
|
1429
|
+
options.json === true,
|
|
1430
|
+
discovery.diagnostics
|
|
1431
|
+
);
|
|
1432
|
+
});
|
|
1433
|
+
});
|
|
1434
|
+
command.command("append <session-id>").description("Append one human-authorized portable event").requiredOption("--kind <kind>", "Event kind").option("--text <text>", "Event text").option("--stdin", "Read event text from stdin").option("--json", "Print machine-readable JSON").action(async (sessionId, options) => {
|
|
1435
|
+
if (options.text !== void 0 === (options.stdin === true)) {
|
|
1436
|
+
throw invalidInput2("Choose exactly one of --text or --stdin");
|
|
1437
|
+
}
|
|
1438
|
+
const kind = eventKind(options.kind);
|
|
1439
|
+
const text = portableText(
|
|
1440
|
+
options.stdin ? await deps.readStdin() : options.text,
|
|
1441
|
+
"Event text"
|
|
1442
|
+
);
|
|
1443
|
+
await withContext(deps, PORTABLE_CLI_ADAPTER_ID, async (context) => {
|
|
1444
|
+
const diagnostics = context.workspaceDiagnostics ?? [];
|
|
1445
|
+
if (!options.json) writeDiagnostics(deps.write, diagnostics);
|
|
1446
|
+
const inspection = context.service.inspectSession(identifier(sessionId, "Session ID"));
|
|
1447
|
+
const appended = context.service.appendSessionEventOnce({
|
|
1448
|
+
sessionId: inspection.session.sessionId,
|
|
1449
|
+
idempotencyKey: idempotencyKey("append", deps.now()),
|
|
1450
|
+
expectedHeadSequence: inspection.session.headSequence,
|
|
1451
|
+
kind,
|
|
1452
|
+
text
|
|
1453
|
+
});
|
|
1454
|
+
const result = {
|
|
1455
|
+
inserted: appended.inserted,
|
|
1456
|
+
headEventId: appended.headEventId,
|
|
1457
|
+
headSequence: appended.headSequence,
|
|
1458
|
+
events: appended.events.map(publicEvent),
|
|
1459
|
+
diagnostics: publicDiagnostics(diagnostics)
|
|
1460
|
+
};
|
|
1461
|
+
if (options.json) return writeJson2(deps.write, result);
|
|
1462
|
+
deps.write(`Appended ${appended.inserted} event(s); head is ${appended.headSequence}.
|
|
1463
|
+
`);
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
command.command("checkpoint <session-id>").description("Create one human-authorized portable checkpoint").option("--summary <text>", "Checkpoint summary").option("--summary-file <path>", "Read summary text from a file").option("--state-file <path>", "Read checkpoint JSON from a file").option("--json", "Print machine-readable JSON").action(async (sessionId, options) => {
|
|
1467
|
+
const selectedInputs = [options.summary, options.summaryFile, options.stateFile].filter((value) => value !== void 0).length;
|
|
1468
|
+
if (selectedInputs > 1) {
|
|
1469
|
+
throw invalidInput2("Choose only one of --summary, --summary-file, or --state-file");
|
|
1470
|
+
}
|
|
1471
|
+
const checkpointInput = checkpointOptions(options, deps.readFile);
|
|
1472
|
+
await withContext(deps, PORTABLE_CLI_ADAPTER_ID, async (context) => {
|
|
1473
|
+
const diagnostics = context.workspaceDiagnostics ?? [];
|
|
1474
|
+
if (!options.json) writeDiagnostics(deps.write, diagnostics);
|
|
1475
|
+
const inspection = context.service.inspectSession(identifier(sessionId, "Session ID"));
|
|
1476
|
+
const created = context.service.checkpointSessionOnce({
|
|
1477
|
+
sessionId: inspection.session.sessionId,
|
|
1478
|
+
idempotencyKey: idempotencyKey("checkpoint", deps.now()),
|
|
1479
|
+
expectedHeadSequence: inspection.session.headSequence,
|
|
1480
|
+
...checkpointInput
|
|
1481
|
+
});
|
|
1482
|
+
const result = {
|
|
1483
|
+
...publicCheckpoint(created),
|
|
1484
|
+
diagnostics: publicDiagnostics(diagnostics)
|
|
1485
|
+
};
|
|
1486
|
+
if (options.json) return writeJson2(deps.write, result);
|
|
1487
|
+
deps.write(`Checkpoint ${result.checkpointId} created at sequence ${result.throughSequence}.
|
|
1488
|
+
`);
|
|
1489
|
+
});
|
|
1490
|
+
});
|
|
1491
|
+
command.command("fork <session-id>").description("Prepare a fork handoff from a checkpoint").requiredOption("--from <checkpoint-id|latest>", "Fork checkpoint").requiredOption("--to <adapter>", "Destination adapter").option("--title <title>", "Reserved title for the destination fork").option("--exclude-event <event-id...>", "Exclude selected event IDs from this capsule").option("--json", "Print machine-readable JSON").action(async (sessionId, options) => {
|
|
1492
|
+
const destinationAdapterId = requireAdapter2(deps, options.to, "destination adapter");
|
|
1493
|
+
if (!options.json && !deps.isTTY) {
|
|
1494
|
+
throw invalidInput2("Interactive confirmation requires a terminal; use --json for scripts");
|
|
1495
|
+
}
|
|
1496
|
+
const forkTitle = options.title === void 0 ? void 0 : portableText(options.title, "Fork title");
|
|
1497
|
+
await withContext(deps, PORTABLE_CLI_ADAPTER_ID, async (context) => {
|
|
1498
|
+
const diagnostics = context.workspaceDiagnostics ?? [];
|
|
1499
|
+
if (!options.json) writeDiagnostics(deps.write, diagnostics);
|
|
1500
|
+
const inspection = context.service.inspectSession(identifier(sessionId, "Session ID"));
|
|
1501
|
+
const checkpointId = options.from === "latest" ? inspection.session.headCheckpointId ?? void 0 : identifier(options.from, "Checkpoint ID");
|
|
1502
|
+
const prepareInput = {
|
|
1503
|
+
sessionId: inspection.session.sessionId,
|
|
1504
|
+
...checkpointId === void 0 ? {} : { checkpointId },
|
|
1505
|
+
destinationAdapterId,
|
|
1506
|
+
mode: "fork",
|
|
1507
|
+
expectedHeadSequence: inspection.session.headSequence,
|
|
1508
|
+
...forkTitle === void 0 ? {} : { forkTitle },
|
|
1509
|
+
...options.excludeEvent === void 0 ? {} : { excludeEventIds: eventExclusionIds(options.excludeEvent) }
|
|
1510
|
+
};
|
|
1511
|
+
const prepared = await context.service.prepareResume(prepareInput);
|
|
1512
|
+
await finishPreparedResume(
|
|
1513
|
+
context,
|
|
1514
|
+
deps,
|
|
1515
|
+
prepareInput,
|
|
1516
|
+
prepared,
|
|
1517
|
+
options.json === true,
|
|
1518
|
+
diagnostics
|
|
1519
|
+
);
|
|
1520
|
+
});
|
|
1521
|
+
});
|
|
1522
|
+
command.command("history <session-id>").description("Read a bounded page of portable session history").option("--before <cursor>", "Opaque history cursor").option("--limit <number>", "Events per page", "50").option("--json", "Print machine-readable JSON").action(async (sessionId, options) => {
|
|
1523
|
+
const limit = boundedInteger(options.limit, 1, MAX_LIMIT, "History limit");
|
|
1524
|
+
await withContext(deps, PORTABLE_CLI_ADAPTER_ID, async (context) => {
|
|
1525
|
+
const diagnostics = context.workspaceDiagnostics ?? [];
|
|
1526
|
+
if (!options.json) writeDiagnostics(deps.write, diagnostics);
|
|
1527
|
+
const page = context.service.getSessionHistory({
|
|
1528
|
+
sessionId: identifier(sessionId, "Session ID"),
|
|
1529
|
+
limit,
|
|
1530
|
+
...options.before === void 0 ? {} : { beforeCursor: identifier(options.before, "History cursor") }
|
|
1531
|
+
});
|
|
1532
|
+
const result = {
|
|
1533
|
+
events: page.events.map(publicEvent),
|
|
1534
|
+
beforeCursor: page.beforeCursor,
|
|
1535
|
+
diagnostics: publicDiagnostics(diagnostics)
|
|
1536
|
+
};
|
|
1537
|
+
if (options.json) return writeJson2(deps.write, result);
|
|
1538
|
+
for (const event of result.events) {
|
|
1539
|
+
deps.write(`${event.sequence} ${terminalText2(event.kind)} ${terminalText2(event.text ?? "")}
|
|
1540
|
+
`);
|
|
1541
|
+
}
|
|
1542
|
+
if (result.beforeCursor) deps.write(`Older events: ${terminalText2(result.beforeCursor)}
|
|
1543
|
+
`);
|
|
1544
|
+
});
|
|
1545
|
+
});
|
|
1546
|
+
return command;
|
|
1547
|
+
}
|
|
1548
|
+
function createHandoffCommand(dependencies = {}) {
|
|
1549
|
+
const deps = resolveDependencies(dependencies);
|
|
1550
|
+
const command = new Command2("handoff").description("Inspect and cancel prepared handoffs");
|
|
1551
|
+
command.command("list").description("List pending handoffs for the current workspace").option("--json", "Print machine-readable JSON").action(async (options) => {
|
|
1552
|
+
await withContext(deps, PORTABLE_CLI_ADAPTER_ID, async (context) => {
|
|
1553
|
+
const diagnostics = context.workspaceDiagnostics ?? [];
|
|
1554
|
+
if (!options.json) writeDiagnostics(deps.write, diagnostics);
|
|
1555
|
+
const result = {
|
|
1556
|
+
handoffs: context.service.listPendingHandoffs().map((handoff) => ({
|
|
1557
|
+
handoffId: handoff.handoffId,
|
|
1558
|
+
sessionId: handoff.sessionId,
|
|
1559
|
+
checkpointId: handoff.checkpointId,
|
|
1560
|
+
targetAdapterId: handoff.targetAdapterId,
|
|
1561
|
+
mode: handoff.mode,
|
|
1562
|
+
createdAt: handoff.createdAt,
|
|
1563
|
+
expiresAt: handoff.expiresAt
|
|
1564
|
+
})),
|
|
1565
|
+
diagnostics: publicDiagnostics(diagnostics)
|
|
1566
|
+
};
|
|
1567
|
+
if (options.json) return writeJson2(deps.write, result);
|
|
1568
|
+
for (const handoff of result.handoffs) {
|
|
1569
|
+
deps.write(
|
|
1570
|
+
`${terminalText2(handoff.handoffId)} ${terminalText2(handoff.mode)} ${terminalText2(handoff.targetAdapterId)} ${terminalText2(handoff.sessionId)}
|
|
1571
|
+
`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
command.command("cancel <handoff-id>").description("Cancel a pending handoff").option("--json", "Print machine-readable JSON").action(async (handoffId, options) => {
|
|
1577
|
+
await withContext(deps, PORTABLE_CLI_ADAPTER_ID, async (context) => {
|
|
1578
|
+
const diagnostics = context.workspaceDiagnostics ?? [];
|
|
1579
|
+
if (!options.json) writeDiagnostics(deps.write, diagnostics);
|
|
1580
|
+
const id = identifier(handoffId, "Handoff ID");
|
|
1581
|
+
const cancelled = context.service.cancelHandoff(id);
|
|
1582
|
+
const result = {
|
|
1583
|
+
handoffId: id,
|
|
1584
|
+
cancelled,
|
|
1585
|
+
diagnostics: publicDiagnostics(diagnostics)
|
|
1586
|
+
};
|
|
1587
|
+
if (options.json) return writeJson2(deps.write, result);
|
|
1588
|
+
deps.write(`Handoff ${terminalText2(id)} cancelled.
|
|
1589
|
+
`);
|
|
1590
|
+
});
|
|
1591
|
+
});
|
|
1592
|
+
return command;
|
|
1593
|
+
}
|
|
1594
|
+
function registerSessionCommands(program2, dependencies = {}) {
|
|
1595
|
+
program2.addCommand(createSessionCommand(dependencies));
|
|
1596
|
+
program2.addCommand(createHandoffCommand(dependencies));
|
|
1597
|
+
}
|
|
1598
|
+
async function createDefaultContext(clientAdapterId = PORTABLE_CLI_ADAPTER_ID) {
|
|
1599
|
+
const adapters = listAdapters();
|
|
1600
|
+
if (clientAdapterId !== PORTABLE_CLI_ADAPTER_ID && !adapters.some((adapter) => adapter.id === clientAdapterId)) {
|
|
1601
|
+
throw invalidInput2(`Unknown destination adapter: ${clientAdapterId}`);
|
|
1602
|
+
}
|
|
1603
|
+
const deviceId = loadOrCreateDeviceId();
|
|
1604
|
+
const repository = new SqliteSessionRepository(getSessionDbPath());
|
|
1605
|
+
try {
|
|
1606
|
+
const binding = resolveWorkspace(process.cwd(), deviceId, repository);
|
|
1607
|
+
const workspaceDiagnostics = binding.gitIdentityMismatch ? [{
|
|
1608
|
+
adapterId: clientAdapterId,
|
|
1609
|
+
code: "git_identity_mismatch",
|
|
1610
|
+
message: "Git identity changed at this workspace path; verify the open repository before resuming"
|
|
1611
|
+
}] : [];
|
|
1612
|
+
const ingestion = new SessionIngestionService({
|
|
1613
|
+
repository,
|
|
1614
|
+
adapters,
|
|
1615
|
+
binding,
|
|
1616
|
+
deviceId,
|
|
1617
|
+
budget: DEFAULT_READ_BUDGET
|
|
1618
|
+
});
|
|
1619
|
+
const service = new SessionService({
|
|
1620
|
+
repository,
|
|
1621
|
+
binding,
|
|
1622
|
+
deviceId,
|
|
1623
|
+
clientAdapterId,
|
|
1624
|
+
adapters,
|
|
1625
|
+
authority: "human",
|
|
1626
|
+
ingestion
|
|
1627
|
+
});
|
|
1628
|
+
return {
|
|
1629
|
+
service,
|
|
1630
|
+
project: basename(binding.rootPath),
|
|
1631
|
+
workspacePath: binding.rootPath,
|
|
1632
|
+
workspaceDiagnostics,
|
|
1633
|
+
async discover() {
|
|
1634
|
+
const discovered = await ingestion.discoverSessions();
|
|
1635
|
+
let imported = 0;
|
|
1636
|
+
const diagnostics = [
|
|
1637
|
+
...workspaceDiagnostics,
|
|
1638
|
+
...discovered.diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
1639
|
+
];
|
|
1640
|
+
for (const candidate of discovered.sessions) {
|
|
1641
|
+
if (candidate.workspaceEvidence.status !== "verified") continue;
|
|
1642
|
+
try {
|
|
1643
|
+
await ingestion.ingestNativeSession(candidate);
|
|
1644
|
+
imported += 1;
|
|
1645
|
+
} catch {
|
|
1646
|
+
diagnostics.push({
|
|
1647
|
+
adapterId: candidate.reference.adapterId,
|
|
1648
|
+
code: "import_failed",
|
|
1649
|
+
message: "A verified native session could not be imported"
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return {
|
|
1654
|
+
searchedAdapters: [...discovered.searchedAdapters],
|
|
1655
|
+
imported,
|
|
1656
|
+
diagnostics
|
|
1657
|
+
};
|
|
1658
|
+
},
|
|
1659
|
+
close: () => repository.close()
|
|
1660
|
+
};
|
|
1661
|
+
} catch (error) {
|
|
1662
|
+
repository.close();
|
|
1663
|
+
throw error;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function resolveDependencies(dependencies) {
|
|
1667
|
+
return {
|
|
1668
|
+
createContext: dependencies.createContext ?? createDefaultContext,
|
|
1669
|
+
adapterIds: dependencies.adapters ?? listAdapters().map((adapter) => adapter.id),
|
|
1670
|
+
write: dependencies.write ?? ((value) => process.stdout.write(value)),
|
|
1671
|
+
isTTY: dependencies.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
1672
|
+
...dependencies.prompt === void 0 ? {} : { prompt: dependencies.prompt },
|
|
1673
|
+
createPrompt: dependencies.createPrompt ?? readlinePrompt,
|
|
1674
|
+
readStdin: dependencies.readStdin ?? (() => readBoundedStdin(MAX_TEXT_CHARS)),
|
|
1675
|
+
readFile: dependencies.readFile ?? readBoundedInputFile,
|
|
1676
|
+
now: dependencies.now ?? Date.now
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
async function withContext(dependencies, clientAdapterId, operation) {
|
|
1680
|
+
const context = await dependencies.createContext(clientAdapterId);
|
|
1681
|
+
try {
|
|
1682
|
+
return await operation(context);
|
|
1683
|
+
} finally {
|
|
1684
|
+
context.close();
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
async function pickSession(context, dependencies, discovery) {
|
|
1688
|
+
const ownedPrompt = dependencies.prompt ? void 0 : readlinePrompt();
|
|
1689
|
+
const prompt = dependencies.prompt ?? ownedPrompt;
|
|
16
1690
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
1691
|
+
const sessions = context.service.listSessions();
|
|
1692
|
+
if (sessions.length === 0) {
|
|
1693
|
+
dependencies.write("No portable sessions found for this workspace.\n");
|
|
1694
|
+
dependencies.write(
|
|
1695
|
+
`Searched adapters: ${terminalText2(discovery.searchedAdapters.join(", "))}
|
|
1696
|
+
`
|
|
1697
|
+
);
|
|
1698
|
+
dependencies.write(
|
|
1699
|
+
"Diagnose an integration with: agentcache adapter doctor <adapter-id>\n"
|
|
1700
|
+
);
|
|
1701
|
+
return null;
|
|
1702
|
+
}
|
|
1703
|
+
while (true) {
|
|
1704
|
+
const query = bounded(
|
|
1705
|
+
(await prompt.ask("Search sessions (or type cancel): ")).trim(),
|
|
1706
|
+
MAX_QUERY_CHARS,
|
|
1707
|
+
"Search query"
|
|
1708
|
+
);
|
|
1709
|
+
if (query.toLowerCase() === "cancel") return void 0;
|
|
1710
|
+
const matches = sessions.filter((item) => matchesQuery(item, context.project, query));
|
|
1711
|
+
if (matches.length === 0) {
|
|
1712
|
+
dependencies.write("No matching sessions. Search again or type cancel.\n");
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
writeSessionList(
|
|
1716
|
+
dependencies.write,
|
|
1717
|
+
matches.map((item) => publicSessionItem(item, context.project)),
|
|
1718
|
+
true,
|
|
1719
|
+
matches
|
|
1720
|
+
);
|
|
1721
|
+
const selection = (await prompt.ask("Select a session number, search, or cancel: ")).trim();
|
|
1722
|
+
if (selection.toLowerCase() === "cancel") return void 0;
|
|
1723
|
+
if (selection.toLowerCase() === "search") continue;
|
|
1724
|
+
const index = Number(selection);
|
|
1725
|
+
if (!Number.isSafeInteger(index) || index < 1 || index > matches.length) {
|
|
1726
|
+
dependencies.write("Invalid selection; no session was selected.\n");
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
const inspection = context.service.inspectSession(matches[index - 1].session.sessionId);
|
|
1730
|
+
writeInspection(
|
|
1731
|
+
dependencies.write,
|
|
1732
|
+
publicInspection(inspection, context.project),
|
|
1733
|
+
inspection,
|
|
1734
|
+
context.workspacePath
|
|
1735
|
+
);
|
|
1736
|
+
while (true) {
|
|
1737
|
+
const action = (await prompt.ask("Choose continue, fork, back, or cancel: ")).trim().toLowerCase();
|
|
1738
|
+
if (action === "cancel") return void 0;
|
|
1739
|
+
if (action === "back") break;
|
|
1740
|
+
if (action === "fork") {
|
|
1741
|
+
return { sessionId: inspection.session.sessionId, mode: "fork", confirmActiveSource: false };
|
|
1742
|
+
}
|
|
1743
|
+
if (action === "continue") {
|
|
1744
|
+
if (inspection.activeWriter) {
|
|
1745
|
+
const confirmation = (await prompt.ask(
|
|
1746
|
+
"The source is active. Type continue to confirm takeover, or cancel: "
|
|
1747
|
+
)).trim().toLowerCase();
|
|
1748
|
+
if (confirmation !== "continue") return void 0;
|
|
1749
|
+
}
|
|
1750
|
+
return {
|
|
1751
|
+
sessionId: inspection.session.sessionId,
|
|
1752
|
+
mode: "continue",
|
|
1753
|
+
confirmActiveSource: inspection.activeWriter !== null
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
dependencies.write("Invalid action; nothing was selected.\n");
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
} finally {
|
|
1760
|
+
ownedPrompt?.close?.();
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
function readlinePrompt() {
|
|
1764
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
1765
|
+
return {
|
|
1766
|
+
ask: (message) => readline.question(message),
|
|
1767
|
+
close: () => readline.close()
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
function matchesQuery(item, project, query) {
|
|
1771
|
+
if (!query) return true;
|
|
1772
|
+
const needle = query.toLowerCase();
|
|
1773
|
+
return [
|
|
1774
|
+
item.session.sessionId,
|
|
1775
|
+
item.session.title,
|
|
1776
|
+
item.session.status,
|
|
1777
|
+
project,
|
|
1778
|
+
...item.legs.map((leg) => leg.adapterId)
|
|
1779
|
+
].some((value) => value.toLowerCase().includes(needle));
|
|
1780
|
+
}
|
|
1781
|
+
function publicDiscovery(discovery) {
|
|
1782
|
+
return {
|
|
1783
|
+
searchedAdapters: [...discovery.searchedAdapters],
|
|
1784
|
+
imported: discovery.imported,
|
|
1785
|
+
diagnostics: publicDiagnostics(discovery.diagnostics)
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function publicDiagnostics(diagnostics) {
|
|
1789
|
+
return diagnostics.map((diagnostic) => ({
|
|
1790
|
+
adapterId: diagnostic.adapterId,
|
|
1791
|
+
code: bounded(diagnostic.code, 128, "Diagnostic code"),
|
|
1792
|
+
message: bounded(diagnostic.message, 1024, "Diagnostic message")
|
|
1793
|
+
}));
|
|
1794
|
+
}
|
|
1795
|
+
function publicSessionItem(item, project) {
|
|
1796
|
+
return {
|
|
1797
|
+
sessionId: item.session.sessionId,
|
|
1798
|
+
sourceAdapters: [...new Set(item.legs.map((leg) => leg.adapterId))],
|
|
1799
|
+
project,
|
|
1800
|
+
title: item.session.title,
|
|
1801
|
+
lastActivity: item.session.updatedAt,
|
|
1802
|
+
state: item.session.status,
|
|
1803
|
+
headSequence: item.session.headSequence,
|
|
1804
|
+
headCheckpointId: item.session.headCheckpointId,
|
|
1805
|
+
fork: item.fork === null ? null : {
|
|
1806
|
+
parentSessionId: item.fork.parentSessionId,
|
|
1807
|
+
forkedFromCheckpointId: item.fork.forkedFromCheckpointId,
|
|
1808
|
+
forkedFromEventId: item.fork.forkedFromEventId,
|
|
1809
|
+
createdAt: item.fork.createdAt
|
|
1810
|
+
},
|
|
1811
|
+
activeWriter: item.activeWriter === null ? null : {
|
|
1812
|
+
legId: item.activeWriter.legId,
|
|
1813
|
+
generation: item.activeWriter.generation,
|
|
1814
|
+
expiresAt: item.activeWriter.expiresAt
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
function publicInspection(inspection, project) {
|
|
1819
|
+
return {
|
|
1820
|
+
...publicSessionItem(inspection, project),
|
|
1821
|
+
eventCount: inspection.eventCount,
|
|
1822
|
+
checkpoints: inspection.checkpoints.map(publicCheckpoint),
|
|
1823
|
+
forks: inspection.forks.map((fork) => ({
|
|
1824
|
+
forkSessionId: fork.forkSessionId,
|
|
1825
|
+
parentSessionId: fork.parentSessionId,
|
|
1826
|
+
forkedFromCheckpointId: fork.forkedFromCheckpointId,
|
|
1827
|
+
forkedFromEventId: fork.forkedFromEventId,
|
|
1828
|
+
reason: fork.reason,
|
|
1829
|
+
createdAt: fork.createdAt
|
|
1830
|
+
}))
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
function publicCheckpoint(checkpoint) {
|
|
1834
|
+
return {
|
|
1835
|
+
checkpointId: checkpoint.checkpointId,
|
|
1836
|
+
sessionId: checkpoint.sessionId,
|
|
1837
|
+
throughEventId: checkpoint.throughEventId,
|
|
1838
|
+
throughSequence: checkpoint.throughSequence,
|
|
1839
|
+
summary: checkpoint.summaryText,
|
|
1840
|
+
state: {
|
|
1841
|
+
objective: checkpoint.state.objective,
|
|
1842
|
+
completedWork: [...checkpoint.state.completedWork],
|
|
1843
|
+
openWork: [...checkpoint.state.openWork],
|
|
1844
|
+
blockers: [...checkpoint.state.blockers],
|
|
1845
|
+
explicitConstraints: [...checkpoint.state.explicitConstraints],
|
|
1846
|
+
decisions: [...checkpoint.state.decisions],
|
|
1847
|
+
rejectedApproaches: [...checkpoint.state.rejectedApproaches],
|
|
1848
|
+
nextStep: checkpoint.state.nextStep
|
|
1849
|
+
},
|
|
1850
|
+
workspaceState: {
|
|
1851
|
+
gitBranch: checkpoint.workspaceState.gitBranch,
|
|
1852
|
+
gitCommit: checkpoint.workspaceState.gitCommit,
|
|
1853
|
+
repositoryRelativePaths: [...checkpoint.workspaceState.repositoryRelativePaths]
|
|
1854
|
+
},
|
|
1855
|
+
createdAt: checkpoint.createdAt
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
function publicEvent(event) {
|
|
1859
|
+
return {
|
|
1860
|
+
eventId: event.eventId,
|
|
1861
|
+
sequence: event.timelineSequence,
|
|
1862
|
+
kind: event.kind,
|
|
1863
|
+
role: event.role,
|
|
1864
|
+
occurredAt: event.occurredAt,
|
|
1865
|
+
text: event.textContent,
|
|
1866
|
+
redactionState: event.redactionState
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
function publicPreparedResume(prepared) {
|
|
1870
|
+
return {
|
|
1871
|
+
handoffId: prepared.handoffId,
|
|
1872
|
+
sessionId: prepared.sessionId,
|
|
1873
|
+
checkpointId: prepared.checkpointId,
|
|
1874
|
+
targetAdapterId: prepared.targetAdapterId,
|
|
1875
|
+
mode: prepared.mode,
|
|
1876
|
+
createdAt: prepared.createdAt,
|
|
1877
|
+
expiresAt: prepared.expiresAt,
|
|
1878
|
+
token: prepared.token,
|
|
1879
|
+
...prepared.forkTitle === null ? {} : { forkTitle: prepared.forkTitle },
|
|
1880
|
+
warnings: [...prepared.warnings],
|
|
1881
|
+
capsule: serializePublicResumeCapsule(prepared.capsule)
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
async function finishPreparedResume(context, dependencies, prepareInput, initial, json, diagnostics) {
|
|
1885
|
+
if (json) {
|
|
19
1886
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1887
|
+
writeJson2(dependencies.write, {
|
|
1888
|
+
...publicPreparedResume(initial),
|
|
1889
|
+
diagnostics: publicDiagnostics(diagnostics)
|
|
1890
|
+
});
|
|
1891
|
+
return;
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
cancelPreparedAfterFailure(context, initial.handoffId);
|
|
1894
|
+
throw error;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
let pending = initial;
|
|
1898
|
+
let ownedPrompt;
|
|
1899
|
+
const exclusions = new Set(prepareInput.excludeEventIds ?? []);
|
|
1900
|
+
try {
|
|
1901
|
+
ownedPrompt = dependencies.prompt ? void 0 : dependencies.createPrompt();
|
|
1902
|
+
const prompt = dependencies.prompt ?? ownedPrompt;
|
|
1903
|
+
while (pending) {
|
|
1904
|
+
writeResumeCapsulePreview(dependencies.write, pending.capsule);
|
|
1905
|
+
const action = (await prompt.ask("Choose confirm, exclude, or cancel: ")).trim().toLowerCase();
|
|
1906
|
+
if (action === "confirm") {
|
|
1907
|
+
writePreparedResume(dependencies.write, publicPreparedResume(pending));
|
|
1908
|
+
pending = null;
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
if (action === "cancel") {
|
|
1912
|
+
context.service.cancelHandoff(pending.handoffId);
|
|
1913
|
+
pending = null;
|
|
1914
|
+
dependencies.write("Resume cancelled.\n");
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (action !== "exclude") {
|
|
1918
|
+
dependencies.write("Invalid action; choose confirm, exclude, or cancel.\n");
|
|
1919
|
+
continue;
|
|
23
1920
|
}
|
|
24
|
-
if (
|
|
25
|
-
|
|
1921
|
+
if (pending.capsule.recentEvents.length === 0) {
|
|
1922
|
+
dependencies.write("There are no capsule events available to exclude.\n");
|
|
1923
|
+
continue;
|
|
26
1924
|
}
|
|
1925
|
+
const selection = (await prompt.ask(
|
|
1926
|
+
"Exclude event numbers (comma-separated), or type cancel: "
|
|
1927
|
+
)).trim();
|
|
1928
|
+
if (selection.toLowerCase() === "cancel") {
|
|
1929
|
+
context.service.cancelHandoff(pending.handoffId);
|
|
1930
|
+
pending = null;
|
|
1931
|
+
dependencies.write("Resume cancelled.\n");
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
const selectedEventIds = selectedPreviewEventIds(selection, pending.capsule);
|
|
1935
|
+
for (const eventId of selectedEventIds) exclusions.add(eventId);
|
|
1936
|
+
const checkpointId = pending.checkpointId;
|
|
1937
|
+
context.service.cancelHandoff(pending.handoffId);
|
|
1938
|
+
pending = null;
|
|
1939
|
+
pending = await context.service.prepareResume({
|
|
1940
|
+
...prepareInput,
|
|
1941
|
+
checkpointId,
|
|
1942
|
+
excludeEventIds: [...exclusions]
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
if (pending) cancelPreparedAfterFailure(context, pending.handoffId);
|
|
1947
|
+
throw error;
|
|
1948
|
+
} finally {
|
|
1949
|
+
try {
|
|
1950
|
+
ownedPrompt?.close?.();
|
|
27
1951
|
} catch {
|
|
28
1952
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
function writeResumeCapsulePreview(write, capsule) {
|
|
1956
|
+
const publicCapsule = serializePublicResumeCapsule(capsule);
|
|
1957
|
+
write("Resume capsule preview (exact portable context sent to the destination):\n");
|
|
1958
|
+
write("--- BEGIN RESUME CAPSULE ---\n");
|
|
1959
|
+
write(`${terminalSafeJson(publicCapsule, 2)}
|
|
32
1960
|
`);
|
|
1961
|
+
write("--- END RESUME CAPSULE ---\n");
|
|
1962
|
+
if (publicCapsule.recentEvents.length > 0) {
|
|
1963
|
+
write("Event selections:\n");
|
|
1964
|
+
publicCapsule.recentEvents.forEach((event, index) => {
|
|
1965
|
+
write(
|
|
1966
|
+
` ${index + 1}. ${terminalText2(event.eventId)} (${terminalText2(event.kind)}, sequence ${event.sequence})
|
|
1967
|
+
`
|
|
1968
|
+
);
|
|
1969
|
+
});
|
|
33
1970
|
}
|
|
34
|
-
}
|
|
35
|
-
|
|
1971
|
+
}
|
|
1972
|
+
function terminalSafeJson(value, space) {
|
|
1973
|
+
return JSON.stringify(value, null, space).replace(
|
|
1974
|
+
/[\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g,
|
|
1975
|
+
(character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
function selectedPreviewEventIds(selection, capsule) {
|
|
1979
|
+
const events = serializePublicResumeCapsule(capsule).recentEvents;
|
|
1980
|
+
const tokens = selection.split(/[\s,]+/).filter(Boolean);
|
|
1981
|
+
if (tokens.length === 0 || tokens.length > 50) {
|
|
1982
|
+
throw invalidInput2("Select between 1 and 50 capsule event numbers");
|
|
1983
|
+
}
|
|
1984
|
+
const indexes = tokens.map((value) => Number(value));
|
|
1985
|
+
if (indexes.some(
|
|
1986
|
+
(index) => !Number.isSafeInteger(index) || index < 1 || index > events.length
|
|
1987
|
+
) || new Set(indexes).size !== indexes.length) {
|
|
1988
|
+
throw invalidInput2("Capsule event selections must be unique displayed numbers");
|
|
1989
|
+
}
|
|
1990
|
+
return indexes.map((index) => events[index - 1].eventId);
|
|
1991
|
+
}
|
|
1992
|
+
function eventExclusionIds(values) {
|
|
1993
|
+
if (values.length === 0 || values.length > 50) {
|
|
1994
|
+
throw invalidInput2("Choose between 1 and 50 --exclude-event values");
|
|
1995
|
+
}
|
|
1996
|
+
const ids = values.map((value) => identifier(value, "Excluded event ID"));
|
|
1997
|
+
if (new Set(ids).size !== ids.length) {
|
|
1998
|
+
throw invalidInput2("Excluded event IDs must be unique");
|
|
1999
|
+
}
|
|
2000
|
+
return ids;
|
|
2001
|
+
}
|
|
2002
|
+
function cancelPreparedAfterFailure(context, handoffId) {
|
|
36
2003
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
2004
|
+
context.service.cancelHandoff(handoffId);
|
|
2005
|
+
} catch {
|
|
2006
|
+
throw invalidInput2("Prepared handoff could not be cancelled safely");
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
function writeSessionList(write, sessions, numbered = false, sourceItems) {
|
|
2010
|
+
if (sessions.length === 0) {
|
|
2011
|
+
write("No portable sessions found for this workspace.\n");
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
sessions.forEach((session, index) => {
|
|
2015
|
+
const prefix = numbered ? `${index + 1}. ` : "";
|
|
2016
|
+
const fork = session.fork ? `fork of ${session.fork.parentSessionId}` : "not a fork";
|
|
2017
|
+
const branch = sourceItems?.[index] ? sessionMetadataText(sourceItems[index], "gitBranch") : null;
|
|
2018
|
+
write(`${prefix}${terminalText2(session.title)} [${terminalText2(session.sessionId)}]
|
|
2019
|
+
`);
|
|
2020
|
+
write(` Source: ${terminalText2(session.sourceAdapters.join(", ") || "portable")}
|
|
2021
|
+
`);
|
|
2022
|
+
write(` Project: ${terminalText2(session.project)}
|
|
41
2023
|
`);
|
|
2024
|
+
if (branch) write(` Branch: ${portableTerminalText(branch)}
|
|
2025
|
+
`);
|
|
2026
|
+
write(` Last activity: ${new Date(session.lastActivity).toISOString()}
|
|
2027
|
+
`);
|
|
2028
|
+
write(` State: ${terminalText2(session.state)}; ${terminalText2(fork)}
|
|
2029
|
+
`);
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
function writeInspection(write, inspection, source, workspacePath) {
|
|
2033
|
+
writeSessionList(write, [inspection]);
|
|
2034
|
+
const checkpoint = currentCheckpoint(source);
|
|
2035
|
+
const branch = checkpoint?.workspaceState.gitBranch ?? sessionMetadataText(source, "gitBranch");
|
|
2036
|
+
const commit = checkpoint?.workspaceState.gitCommit ?? sessionMetadataText(source, "gitCommit");
|
|
2037
|
+
write(` Workspace: ${workspacePath ? terminalText2(workspacePath) : "unavailable"}
|
|
2038
|
+
`);
|
|
2039
|
+
write(
|
|
2040
|
+
` Objective: ${checkpoint ? portableTerminalText(checkpoint.state.objective) : "unavailable (no checkpoint)"}
|
|
2041
|
+
`
|
|
2042
|
+
);
|
|
2043
|
+
write(` Git branch: ${branch ? portableTerminalText(branch) : "unknown"}
|
|
2044
|
+
`);
|
|
2045
|
+
write(` Git commit: ${commit ? portableTerminalText(commit) : "unknown"}
|
|
2046
|
+
`);
|
|
2047
|
+
write(` Events: ${inspection.eventCount}
|
|
2048
|
+
`);
|
|
2049
|
+
if (checkpoint) {
|
|
2050
|
+
write(` Checkpoint: ${portableTerminalText(checkpoint.summaryText)}
|
|
2051
|
+
`);
|
|
2052
|
+
write(
|
|
2053
|
+
` Approx. portable-context baseline: ~${approximateCheckpointContextChars(checkpoint)} characters of checkpoint-state JSON; the final resume capsule is larger
|
|
2054
|
+
`
|
|
2055
|
+
);
|
|
2056
|
+
} else {
|
|
2057
|
+
write(" Approx. portable context: unavailable (no checkpoint)\n");
|
|
42
2058
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
for await (const chunk of process.stdin) {
|
|
47
|
-
data += chunk;
|
|
2059
|
+
if (inspection.activeWriter) {
|
|
2060
|
+
write(` Active writer until: ${new Date(inspection.activeWriter.expiresAt).toISOString()}
|
|
2061
|
+
`);
|
|
48
2062
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
2063
|
+
}
|
|
2064
|
+
function currentCheckpoint(inspection) {
|
|
2065
|
+
const headCheckpointId = inspection.session.headCheckpointId;
|
|
2066
|
+
if (headCheckpointId) {
|
|
2067
|
+
const head = inspection.checkpoints.find(
|
|
2068
|
+
(checkpoint) => checkpoint.checkpointId === headCheckpointId
|
|
2069
|
+
);
|
|
2070
|
+
if (head) return head;
|
|
2071
|
+
}
|
|
2072
|
+
return inspection.checkpoints.reduce((latest, checkpoint) => {
|
|
2073
|
+
if (!latest || checkpoint.throughSequence > latest.throughSequence) return checkpoint;
|
|
2074
|
+
if (checkpoint.throughSequence === latest.throughSequence && checkpoint.createdAt > latest.createdAt) return checkpoint;
|
|
2075
|
+
return latest;
|
|
2076
|
+
}, null);
|
|
2077
|
+
}
|
|
2078
|
+
function sessionMetadataText(item, key) {
|
|
2079
|
+
const legs = [...item.legs].sort((left, right) => {
|
|
2080
|
+
const activityDifference = legActivityAt(right) - legActivityAt(left);
|
|
2081
|
+
return activityDifference === 0 ? right.startedAt - left.startedAt : activityDifference;
|
|
2082
|
+
});
|
|
2083
|
+
for (const leg of legs) {
|
|
2084
|
+
const value = storedMetadataText(leg.metadata[key]);
|
|
2085
|
+
if (value) return value;
|
|
2086
|
+
}
|
|
2087
|
+
return storedMetadataText(item.session.metadata[key]);
|
|
2088
|
+
}
|
|
2089
|
+
function legActivityAt(leg) {
|
|
2090
|
+
const sourceActivityAt2 = leg.metadata.sourceActivityAt;
|
|
2091
|
+
return typeof sourceActivityAt2 === "number" && Number.isSafeInteger(sourceActivityAt2) ? sourceActivityAt2 : leg.startedAt;
|
|
2092
|
+
}
|
|
2093
|
+
function storedMetadataText(value) {
|
|
2094
|
+
if (typeof value !== "string") return null;
|
|
2095
|
+
const trimmed = value.trim();
|
|
2096
|
+
return trimmed.length > 0 ? trimmed.slice(0, MAX_TEXT_CHARS) : null;
|
|
2097
|
+
}
|
|
2098
|
+
function approximateCheckpointContextChars(checkpoint) {
|
|
2099
|
+
const safe = (value) => redactPortableText(value);
|
|
2100
|
+
return JSON.stringify({
|
|
2101
|
+
goal: safe(checkpoint.state.objective),
|
|
2102
|
+
explicitConstraints: checkpoint.state.explicitConstraints.map(safe),
|
|
2103
|
+
completedWork: checkpoint.state.completedWork.map(safe),
|
|
2104
|
+
openWork: checkpoint.state.openWork.map(safe),
|
|
2105
|
+
blockers: checkpoint.state.blockers.map(safe),
|
|
2106
|
+
decisions: checkpoint.state.decisions.map(safe),
|
|
2107
|
+
rejectedApproaches: checkpoint.state.rejectedApproaches.map(safe),
|
|
2108
|
+
nextStep: checkpoint.state.nextStep === null ? null : safe(checkpoint.state.nextStep),
|
|
2109
|
+
workspaceState: {
|
|
2110
|
+
gitBranch: checkpoint.workspaceState.gitBranch === null ? null : safe(checkpoint.workspaceState.gitBranch),
|
|
2111
|
+
gitCommit: checkpoint.workspaceState.gitCommit === null ? null : safe(checkpoint.workspaceState.gitCommit),
|
|
2112
|
+
repositoryRelativePaths: checkpoint.workspaceState.repositoryRelativePaths.map(safe)
|
|
2113
|
+
}
|
|
2114
|
+
}).length;
|
|
2115
|
+
}
|
|
2116
|
+
function portableTerminalText(value) {
|
|
2117
|
+
return terminalText2(redactPortableText(value));
|
|
2118
|
+
}
|
|
2119
|
+
function writePreparedResume(write, prepared) {
|
|
2120
|
+
write(
|
|
2121
|
+
`Handoff ${terminalText2(prepared.handoffId)} prepared for ${terminalText2(prepared.targetAdapterId)}.
|
|
2122
|
+
`
|
|
2123
|
+
);
|
|
2124
|
+
write(
|
|
2125
|
+
`Use this one-time token in the destination session_resume tool: ${terminalText2(prepared.token)}
|
|
2126
|
+
`
|
|
2127
|
+
);
|
|
2128
|
+
if (prepared.forkTitle) {
|
|
2129
|
+
write(`Fork title: ${terminalText2(prepared.forkTitle)}
|
|
56
2130
|
`);
|
|
57
|
-
process.stdout.write("{}");
|
|
58
2131
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
2132
|
+
write(`Expires: ${new Date(prepared.expiresAt).toISOString()}
|
|
2133
|
+
`);
|
|
2134
|
+
}
|
|
2135
|
+
function writeDiscoveryDiagnostics(write, discovery) {
|
|
2136
|
+
writeDiagnostics(write, discovery.diagnostics);
|
|
2137
|
+
}
|
|
2138
|
+
function writeDiagnostics(write, diagnostics) {
|
|
2139
|
+
for (const diagnostic of diagnostics) {
|
|
2140
|
+
write(
|
|
2141
|
+
`Discovery ${terminalText2(diagnostic.adapterId)}/${terminalText2(diagnostic.code)}: ${terminalText2(diagnostic.message)}
|
|
2142
|
+
`
|
|
2143
|
+
);
|
|
65
2144
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
2145
|
+
}
|
|
2146
|
+
function checkpointOptions(options, readFile) {
|
|
2147
|
+
if (options.summary !== void 0) {
|
|
2148
|
+
return { summaryText: portableText(options.summary, "Checkpoint summary") };
|
|
2149
|
+
}
|
|
2150
|
+
if (options.summaryFile !== void 0) {
|
|
2151
|
+
return {
|
|
2152
|
+
summaryText: portableText(
|
|
2153
|
+
boundedFile(readFile(identifier(options.summaryFile, "Summary file"))),
|
|
2154
|
+
"Checkpoint summary"
|
|
2155
|
+
)
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
if (options.stateFile === void 0) return {};
|
|
2159
|
+
const parsed = parseCheckpointJson(
|
|
2160
|
+
boundedFile(readFile(identifier(options.stateFile, "State file")))
|
|
2161
|
+
);
|
|
2162
|
+
return parsed;
|
|
2163
|
+
}
|
|
2164
|
+
function parseCheckpointJson(value) {
|
|
2165
|
+
let parsed;
|
|
2166
|
+
try {
|
|
2167
|
+
parsed = JSON.parse(value);
|
|
2168
|
+
} catch {
|
|
2169
|
+
throw invalidInput2("Checkpoint JSON is malformed");
|
|
2170
|
+
}
|
|
2171
|
+
if (!isObject(parsed)) throw invalidInput2("Checkpoint JSON must be an object");
|
|
2172
|
+
assertOnlyKeys(parsed, ["throughEventId", "summaryText", "state", "workspaceState"]);
|
|
2173
|
+
const result = {};
|
|
2174
|
+
if (parsed.throughEventId !== void 0) {
|
|
2175
|
+
result.throughEventId = identifier(parsed.throughEventId, "Through event ID");
|
|
2176
|
+
}
|
|
2177
|
+
if (parsed.summaryText !== void 0) {
|
|
2178
|
+
result.summaryText = portableText(parsed.summaryText, "Checkpoint summary");
|
|
2179
|
+
}
|
|
2180
|
+
if (parsed.state !== void 0) result.state = checkpointState(parsed.state);
|
|
2181
|
+
if (parsed.workspaceState !== void 0) {
|
|
2182
|
+
result.workspaceState = checkpointWorkspaceState(parsed.workspaceState);
|
|
2183
|
+
}
|
|
2184
|
+
return result;
|
|
2185
|
+
}
|
|
2186
|
+
function checkpointState(value) {
|
|
2187
|
+
if (!isObject(value)) throw invalidInput2("Checkpoint state must be an object");
|
|
2188
|
+
const keys = [
|
|
2189
|
+
"objective",
|
|
2190
|
+
"completedWork",
|
|
2191
|
+
"openWork",
|
|
2192
|
+
"blockers",
|
|
2193
|
+
"explicitConstraints",
|
|
2194
|
+
"decisions",
|
|
2195
|
+
"rejectedApproaches",
|
|
2196
|
+
"nextStep"
|
|
2197
|
+
];
|
|
2198
|
+
assertOnlyKeys(value, keys);
|
|
2199
|
+
return {
|
|
2200
|
+
objective: portableText(value.objective, "Checkpoint objective"),
|
|
2201
|
+
completedWork: stringArray(value.completedWork, "Completed work"),
|
|
2202
|
+
openWork: stringArray(value.openWork, "Open work"),
|
|
2203
|
+
blockers: stringArray(value.blockers, "Blockers"),
|
|
2204
|
+
explicitConstraints: stringArray(value.explicitConstraints, "Explicit constraints"),
|
|
2205
|
+
decisions: stringArray(value.decisions, "Decisions"),
|
|
2206
|
+
rejectedApproaches: stringArray(value.rejectedApproaches, "Rejected approaches"),
|
|
2207
|
+
nextStep: value.nextStep === null ? null : portableText(value.nextStep, "Next step")
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
function checkpointWorkspaceState(value) {
|
|
2211
|
+
if (!isObject(value)) throw invalidInput2("Checkpoint workspace state must be an object");
|
|
2212
|
+
assertOnlyKeys(value, ["gitBranch", "gitCommit", "repositoryRelativePaths"]);
|
|
2213
|
+
return {
|
|
2214
|
+
...value.gitBranch === void 0 ? {} : { gitBranch: nullableText(value.gitBranch, "Git branch") },
|
|
2215
|
+
...value.gitCommit === void 0 ? {} : { gitCommit: nullableText(value.gitCommit, "Git commit") },
|
|
2216
|
+
...value.repositoryRelativePaths === void 0 ? {} : { repositoryRelativePaths: stringArray(value.repositoryRelativePaths, "Repository paths") }
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
function stringArray(value, name) {
|
|
2220
|
+
if (!Array.isArray(value) || value.length > 100) {
|
|
2221
|
+
throw invalidInput2(`${name} must be an array with at most 100 items`);
|
|
74
2222
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
2223
|
+
return value.map((item) => portableText(item, name));
|
|
2224
|
+
}
|
|
2225
|
+
function nullableText(value, name) {
|
|
2226
|
+
return value === null ? null : portableText(value, name);
|
|
2227
|
+
}
|
|
2228
|
+
function assertOnlyKeys(value, allowed) {
|
|
2229
|
+
const allowedKeys = new Set(allowed);
|
|
2230
|
+
const unexpected = Object.keys(value).find((key) => !allowedKeys.has(key));
|
|
2231
|
+
if (unexpected) throw invalidInput2(`Unexpected checkpoint JSON field: ${unexpected}`);
|
|
2232
|
+
}
|
|
2233
|
+
function isObject(value) {
|
|
2234
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2235
|
+
}
|
|
2236
|
+
function requireAdapter2(dependencies, value, label) {
|
|
2237
|
+
const adapterId = identifier(value, label);
|
|
2238
|
+
if (!dependencies.adapterIds.includes(adapterId)) {
|
|
2239
|
+
throw invalidInput2(`Unknown ${label}: ${adapterId}`);
|
|
2240
|
+
}
|
|
2241
|
+
return adapterId;
|
|
2242
|
+
}
|
|
2243
|
+
function eventKind(value) {
|
|
2244
|
+
if (!EVENT_KINDS.has(value)) throw invalidInput2(`Unsupported event kind: ${value}`);
|
|
2245
|
+
return value;
|
|
2246
|
+
}
|
|
2247
|
+
function sessionState(value) {
|
|
2248
|
+
if (!SESSION_STATES.has(value)) throw invalidInput2(`Unsupported session status: ${value}`);
|
|
2249
|
+
return value;
|
|
2250
|
+
}
|
|
2251
|
+
function identifier(value, name) {
|
|
2252
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.length > 1024) {
|
|
2253
|
+
throw invalidInput2(`${name} must be a non-empty bounded string`);
|
|
2254
|
+
}
|
|
2255
|
+
return value.trim();
|
|
2256
|
+
}
|
|
2257
|
+
function portableText(value, name) {
|
|
2258
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_TEXT_CHARS) {
|
|
2259
|
+
throw invalidInput2(`${name} must contain between 1 and ${MAX_TEXT_CHARS} characters`);
|
|
2260
|
+
}
|
|
2261
|
+
return value;
|
|
2262
|
+
}
|
|
2263
|
+
function bounded(value, max, name) {
|
|
2264
|
+
if (value.length > max) throw invalidInput2(`${name} exceeds ${max} characters`);
|
|
2265
|
+
return value;
|
|
2266
|
+
}
|
|
2267
|
+
function boundedFile(value) {
|
|
2268
|
+
if (value.length > MAX_FILE_CHARS) throw invalidInput2("Input file is too large");
|
|
2269
|
+
return value;
|
|
2270
|
+
}
|
|
2271
|
+
function boundedInteger(value, minimum, maximum, name) {
|
|
2272
|
+
if (!/^\d+$/.test(value)) throw invalidInput2(`${name} must be an integer`);
|
|
2273
|
+
const number = Number(value);
|
|
2274
|
+
if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
|
|
2275
|
+
throw invalidInput2(`${name} must be between ${minimum} and ${maximum}`);
|
|
2276
|
+
}
|
|
2277
|
+
return number;
|
|
2278
|
+
}
|
|
2279
|
+
function idempotencyKey(operation, now) {
|
|
2280
|
+
return `cli:${operation}:${now}:${randomUUID()}`;
|
|
2281
|
+
}
|
|
2282
|
+
async function readBoundedStdin(maxChars, input = process.stdin) {
|
|
2283
|
+
let value = "";
|
|
2284
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
|
|
2285
|
+
try {
|
|
2286
|
+
for await (const chunk of input) {
|
|
2287
|
+
value += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
2288
|
+
if (value.length > maxChars) {
|
|
2289
|
+
throw invalidInput2(`Standard input exceeds ${maxChars} characters`);
|
|
2290
|
+
}
|
|
78
2291
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
2292
|
+
value += decoder.decode();
|
|
2293
|
+
} catch (error) {
|
|
2294
|
+
if (error instanceof SessionOperationError) throw error;
|
|
2295
|
+
throw invalidInput2("Standard input contains malformed UTF-8");
|
|
2296
|
+
}
|
|
2297
|
+
if (value.length > maxChars) throw invalidInput2(`Standard input exceeds ${maxChars} characters`);
|
|
2298
|
+
return value;
|
|
2299
|
+
}
|
|
2300
|
+
function readBoundedInputFile(path, options = {}) {
|
|
2301
|
+
let initialStat;
|
|
2302
|
+
try {
|
|
2303
|
+
initialStat = lstatSync(path);
|
|
2304
|
+
} catch {
|
|
2305
|
+
throw invalidInput2("Input file could not be read");
|
|
82
2306
|
}
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
2307
|
+
if (initialStat.isSymbolicLink() || !initialStat.isFile()) {
|
|
2308
|
+
throw invalidInput2("Input path must be a regular non-symbolic file");
|
|
2309
|
+
}
|
|
2310
|
+
if (initialStat.size > MAX_FILE_CHARS) throw invalidInput2("Input file is too large");
|
|
2311
|
+
const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
|
|
2312
|
+
const nonBlocking = "O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0;
|
|
2313
|
+
const openFile = options.openFile ?? ((filePath, flags) => openSync(filePath, flags));
|
|
2314
|
+
let descriptor;
|
|
2315
|
+
try {
|
|
2316
|
+
descriptor = openFile(path, constants.O_RDONLY | noFollow | nonBlocking);
|
|
2317
|
+
} catch {
|
|
2318
|
+
throw invalidInput2("Input file could not be read");
|
|
2319
|
+
}
|
|
2320
|
+
try {
|
|
2321
|
+
const stat = fstatSync(descriptor);
|
|
2322
|
+
if (!stat.isFile() || stat.dev !== initialStat.dev || stat.ino !== initialStat.ino) {
|
|
2323
|
+
throw invalidInput2("Input file changed before it could be read safely");
|
|
86
2324
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
2325
|
+
if (stat.size > MAX_FILE_CHARS) throw invalidInput2("Input file is too large");
|
|
2326
|
+
const buffer = Buffer.alloc(MAX_FILE_CHARS + 1);
|
|
2327
|
+
let bytesRead = 0;
|
|
2328
|
+
while (bytesRead < buffer.length) {
|
|
2329
|
+
const count = readSync(
|
|
2330
|
+
descriptor,
|
|
2331
|
+
buffer,
|
|
2332
|
+
bytesRead,
|
|
2333
|
+
buffer.length - bytesRead,
|
|
2334
|
+
null
|
|
2335
|
+
);
|
|
2336
|
+
if (count === 0) break;
|
|
2337
|
+
bytesRead += count;
|
|
2338
|
+
}
|
|
2339
|
+
if (bytesRead > MAX_FILE_CHARS) throw invalidInput2("Input file is too large");
|
|
2340
|
+
const completedStat = fstatSync(descriptor);
|
|
2341
|
+
if (completedStat.dev !== stat.dev || completedStat.ino !== stat.ino || completedStat.size !== stat.size || completedStat.mtimeMs !== stat.mtimeMs || completedStat.ctimeMs !== stat.ctimeMs) {
|
|
2342
|
+
throw invalidInput2("Input file changed while it was being read");
|
|
2343
|
+
}
|
|
2344
|
+
try {
|
|
2345
|
+
return FATAL_UTF8_DECODER.decode(buffer.subarray(0, bytesRead));
|
|
2346
|
+
} catch {
|
|
2347
|
+
throw invalidInput2("Input file contains malformed UTF-8");
|
|
2348
|
+
}
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
if (error instanceof SessionOperationError) throw error;
|
|
2351
|
+
throw invalidInput2("Input file could not be read");
|
|
2352
|
+
} finally {
|
|
2353
|
+
try {
|
|
2354
|
+
closeSync(descriptor);
|
|
2355
|
+
} catch {
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
function invalidInput2(message) {
|
|
2360
|
+
return new SessionOperationError("invalid_input", message, ["cancel"]);
|
|
2361
|
+
}
|
|
2362
|
+
function formatCliError(error, json) {
|
|
2363
|
+
const operationError = error instanceof SessionOperationError ? error : void 0;
|
|
2364
|
+
const rawMessage = error instanceof Error ? error.message : "Command failed";
|
|
2365
|
+
const message = publicErrorText(rawMessage);
|
|
2366
|
+
const code = operationError && PUBLIC_ERROR_CODES.has(operationError.code) ? operationError.code : "command_failed";
|
|
2367
|
+
if (json) {
|
|
2368
|
+
return `${JSON.stringify({
|
|
2369
|
+
error: {
|
|
2370
|
+
code,
|
|
2371
|
+
message,
|
|
2372
|
+
alternatives: operationError ? operationError.alternatives.slice(0, 16).filter(isPublicAlternative) : []
|
|
2373
|
+
}
|
|
2374
|
+
})}
|
|
2375
|
+
`;
|
|
90
2376
|
}
|
|
91
|
-
|
|
2377
|
+
return `agentcache: ${message}
|
|
2378
|
+
`;
|
|
2379
|
+
}
|
|
2380
|
+
function publicErrorText(value) {
|
|
2381
|
+
const boundedSource = value.slice(0, 4096);
|
|
2382
|
+
return terminalText2(redactPortableText(terminalText2(boundedSource))).slice(0, 1024);
|
|
2383
|
+
}
|
|
2384
|
+
async function runCliProgram(program2, argv, output = {}) {
|
|
2385
|
+
const writeOut = output.writeOut ?? ((value) => process.stdout.write(value));
|
|
2386
|
+
const writeError = output.writeError ?? ((value) => process.stderr.write(value));
|
|
2387
|
+
configureCommander(program2, writeOut);
|
|
2388
|
+
try {
|
|
2389
|
+
await program2.parseAsync([...argv]);
|
|
2390
|
+
return 0;
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
if (error instanceof CommanderError && (error.code === "commander.helpDisplayed" || error.code === "commander.version")) {
|
|
2393
|
+
return 0;
|
|
2394
|
+
}
|
|
2395
|
+
const publicError = error instanceof CommanderError ? invalidInput2(error.message) : error;
|
|
2396
|
+
writeError(formatCliError(publicError, argv.includes("--json")));
|
|
2397
|
+
return error instanceof CommanderError && error.exitCode > 0 ? error.exitCode : 1;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
function configureCommander(program2, writeOut) {
|
|
2401
|
+
program2.configureOutput({ writeOut, writeErr: () => {
|
|
2402
|
+
} });
|
|
2403
|
+
program2.exitOverride();
|
|
2404
|
+
for (const child of program2.commands) configureCommander(child, writeOut);
|
|
2405
|
+
}
|
|
2406
|
+
function isPublicAlternative(value) {
|
|
2407
|
+
return ["refresh", "fork", "cancel", "wait", "retry", "setup", "diagnose"].includes(value);
|
|
2408
|
+
}
|
|
2409
|
+
function writeJson2(write, value) {
|
|
2410
|
+
write(`${terminalSafeJson(value)}
|
|
92
2411
|
`);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
2412
|
+
}
|
|
2413
|
+
function terminalText2(value) {
|
|
2414
|
+
return value.replace(
|
|
2415
|
+
/[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g,
|
|
2416
|
+
"\uFFFD"
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// src/cli.ts
|
|
2421
|
+
configureMcpRegistrationEntrypoint(fileURLToPath(import.meta.url));
|
|
2422
|
+
var PKG_VERSION = JSON.parse(
|
|
2423
|
+
readPackageFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
2424
|
+
).version;
|
|
2425
|
+
var program = new Command3();
|
|
2426
|
+
var MAX_DOCTOR_MESSAGE_CHARS = 512;
|
|
2427
|
+
var SQLITE_FILE_HEADER_BYTES = 100;
|
|
2428
|
+
var SQLITE_MAGIC = Buffer.from("SQLite format 3\0", "binary");
|
|
2429
|
+
function publicDoctorMessage(message) {
|
|
2430
|
+
const terminalSafe = message.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ");
|
|
2431
|
+
return [...redactPortableText(terminalSafe).replace(/\s+/gu, " ").trim()].slice(0, MAX_DOCTOR_MESSAGE_CHARS).join("");
|
|
2432
|
+
}
|
|
2433
|
+
function hasStructurallyValidSqliteHeader(header, fileSize) {
|
|
2434
|
+
if (header.length !== SQLITE_FILE_HEADER_BYTES) return false;
|
|
2435
|
+
if (!header.subarray(0, SQLITE_MAGIC.length).equals(SQLITE_MAGIC)) return false;
|
|
2436
|
+
const encodedPageSize = header.readUInt16BE(16);
|
|
2437
|
+
const pageSize = encodedPageSize === 1 ? 65536 : encodedPageSize;
|
|
2438
|
+
const validPageSize = pageSize >= 512 && pageSize <= 65536 && (pageSize & pageSize - 1) === 0;
|
|
2439
|
+
if (!validPageSize || fileSize < pageSize || fileSize % pageSize !== 0) return false;
|
|
2440
|
+
const writeVersion = header[18];
|
|
2441
|
+
const readVersion = header[19];
|
|
2442
|
+
const usablePageBytes = pageSize - header[20];
|
|
2443
|
+
if (writeVersion !== 1 && writeVersion !== 2 || readVersion !== 1 && readVersion !== 2 || usablePageBytes < 480 || header[21] !== 64 || header[22] !== 32 || header[23] !== 32) {
|
|
2444
|
+
return false;
|
|
110
2445
|
}
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return;
|
|
2446
|
+
const physicalPageCount = fileSize / pageSize;
|
|
2447
|
+
const declaredPageCount = header.readUInt32BE(28);
|
|
2448
|
+
const schemaFormat = header.readUInt32BE(44);
|
|
2449
|
+
const textEncoding = header.readUInt32BE(56);
|
|
2450
|
+
if (declaredPageCount === 0 || declaredPageCount > physicalPageCount || schemaFormat < 1 || schemaFormat > 4 || textEncoding < 1 || textEncoding > 3 || header.readUInt32BE(96) === 0) {
|
|
2451
|
+
return false;
|
|
118
2452
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
2453
|
+
return header.subarray(72, 92).every((byte) => byte === 0);
|
|
2454
|
+
}
|
|
2455
|
+
function probeSessionDatabase(path) {
|
|
2456
|
+
const openFlags = fsConstants.O_RDONLY | (typeof fsConstants.O_NONBLOCK === "number" ? fsConstants.O_NONBLOCK : 0) | (typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0);
|
|
2457
|
+
let descriptor;
|
|
2458
|
+
try {
|
|
2459
|
+
descriptor = openSync2(path, openFlags);
|
|
2460
|
+
const opened = fstatSync2(descriptor);
|
|
2461
|
+
if (!opened.isFile()) return "unsafe";
|
|
2462
|
+
const pathAtOpen = lstatFileSync(path);
|
|
2463
|
+
if (pathAtOpen.isSymbolicLink() || !pathAtOpen.isFile() || opened.dev !== pathAtOpen.dev || opened.ino !== pathAtOpen.ino) {
|
|
2464
|
+
return "unsafe";
|
|
2465
|
+
}
|
|
2466
|
+
const header = Buffer.alloc(SQLITE_FILE_HEADER_BYTES);
|
|
2467
|
+
let length = 0;
|
|
2468
|
+
while (length < header.length) {
|
|
2469
|
+
const read = readSync2(descriptor, header, length, header.length - length, length);
|
|
2470
|
+
if (read === 0) break;
|
|
2471
|
+
length += read;
|
|
2472
|
+
}
|
|
2473
|
+
const completed = fstatSync2(descriptor);
|
|
2474
|
+
const finalPath = lstatFileSync(path);
|
|
2475
|
+
if (!completed.isFile() || completed.dev !== opened.dev || completed.ino !== opened.ino || completed.size !== opened.size || completed.mtimeMs !== opened.mtimeMs || completed.ctimeMs !== opened.ctimeMs || finalPath.isSymbolicLink() || !finalPath.isFile() || finalPath.dev !== opened.dev || finalPath.ino !== opened.ino) {
|
|
2476
|
+
return "unsafe";
|
|
2477
|
+
}
|
|
2478
|
+
return length === SQLITE_FILE_HEADER_BYTES && hasStructurallyValidSqliteHeader(header, opened.size) ? "detected" : "unsafe";
|
|
2479
|
+
} catch (cause) {
|
|
2480
|
+
if (cause?.code !== "ENOENT") return "unsafe";
|
|
2481
|
+
try {
|
|
2482
|
+
lstatFileSync(path);
|
|
2483
|
+
return "unsafe";
|
|
2484
|
+
} catch (pathCause) {
|
|
2485
|
+
return pathCause?.code === "ENOENT" ? "missing" : "unsafe";
|
|
2486
|
+
}
|
|
2487
|
+
} finally {
|
|
2488
|
+
if (descriptor !== void 0) {
|
|
2489
|
+
try {
|
|
2490
|
+
closeSync2(descriptor);
|
|
2491
|
+
} catch {
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
129
2494
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
type: "rule",
|
|
139
|
-
title: content.slice(0, 80),
|
|
140
|
-
content,
|
|
141
|
-
confidence: "high",
|
|
142
|
-
observationCount: 1,
|
|
143
|
-
authority: "USER",
|
|
144
|
-
status: "active",
|
|
145
|
-
enforce: true,
|
|
146
|
-
project,
|
|
147
|
-
scope,
|
|
148
|
-
createdAt: Date.now(),
|
|
149
|
-
updatedAt: Date.now(),
|
|
150
|
-
lastSeenAt: Date.now(),
|
|
151
|
-
metadata: { source: "cli" }
|
|
152
|
-
});
|
|
153
|
-
console.log(`Enforced rule added (${scope}): ${content}`);
|
|
154
|
-
repo.close();
|
|
2495
|
+
}
|
|
2496
|
+
program.name("agentcache").description("Same-machine cross-agent session continuity with explicit user-selected handoffs").version(PKG_VERSION);
|
|
2497
|
+
program.command("setup").description("Detect IDEs and register AgentCache").action(async () => {
|
|
2498
|
+
const { runSetup } = await import("./setup-7JJPW3VG.js");
|
|
2499
|
+
await runSetup();
|
|
2500
|
+
});
|
|
2501
|
+
program.command("serve").description("Start AgentCache MCP server (spawned by IDEs automatically)").requiredOption("--adapter <id>", "Trusted adapter identity for this MCP process").action(async (options) => {
|
|
2502
|
+
await startMcpForAdapter(options.adapter);
|
|
155
2503
|
});
|
|
156
2504
|
program.command("doctor").description("Diagnose AgentCache installation and report problems").action(async () => {
|
|
157
|
-
const { existsSync,
|
|
2505
|
+
const { existsSync, lstatSync: lstatSync2 } = await import("fs");
|
|
158
2506
|
const { join } = await import("path");
|
|
159
2507
|
const { homedir } = await import("os");
|
|
160
|
-
const {
|
|
161
|
-
const { getDataDir, getDbPath, isInitialized } = await import("./paths-5LZRKNYY.js");
|
|
2508
|
+
const { getDataDir, getSessionDbPath: getSessionDbPath2 } = await import("./paths-NTZ2357O.js");
|
|
162
2509
|
let ok = 0;
|
|
163
2510
|
let warn = 0;
|
|
164
2511
|
let fail = 0;
|
|
165
2512
|
function pass(msg) {
|
|
166
|
-
console.log(` \u2713 ${msg}`);
|
|
2513
|
+
console.log(` \u2713 ${publicDoctorMessage(msg)}`);
|
|
167
2514
|
ok++;
|
|
168
2515
|
}
|
|
169
2516
|
function warning(msg) {
|
|
170
|
-
console.log(` \u26A0 ${msg}`);
|
|
2517
|
+
console.log(` \u26A0 ${publicDoctorMessage(msg)}`);
|
|
171
2518
|
warn++;
|
|
172
2519
|
}
|
|
173
2520
|
function error(msg) {
|
|
174
|
-
console.log(` \u2717 ${msg}`);
|
|
2521
|
+
console.log(` \u2717 ${publicDoctorMessage(msg)}`);
|
|
175
2522
|
fail++;
|
|
176
2523
|
}
|
|
2524
|
+
function info(msg) {
|
|
2525
|
+
console.log(` \xB7 ${publicDoctorMessage(msg)}`);
|
|
2526
|
+
}
|
|
177
2527
|
console.log("AgentCache Doctor\n");
|
|
178
2528
|
console.log("Storage:");
|
|
179
2529
|
const dataDir = getDataDir();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
2530
|
+
let dataDirectoryState = "unsafe";
|
|
2531
|
+
try {
|
|
2532
|
+
const metadata = lstatSync2(dataDir);
|
|
2533
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
2534
|
+
error("Session data directory is not a safe local directory");
|
|
2535
|
+
} else {
|
|
2536
|
+
dataDirectoryState = "available";
|
|
2537
|
+
pass("Session data directory available");
|
|
2538
|
+
}
|
|
2539
|
+
} catch (cause) {
|
|
2540
|
+
if (cause?.code === "ENOENT") {
|
|
2541
|
+
dataDirectoryState = "missing";
|
|
2542
|
+
info("Session data directory not initialized");
|
|
2543
|
+
} else {
|
|
2544
|
+
error("Session data directory could not be checked safely");
|
|
2545
|
+
}
|
|
184
2546
|
}
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
error(`Native module ABI mismatch \u2014 run: npm rebuild better-sqlite3 -g`);
|
|
195
|
-
} else {
|
|
196
|
-
error(`Database broken: ${err.message}`);
|
|
197
|
-
}
|
|
2547
|
+
const sessionDbPath = getSessionDbPath2();
|
|
2548
|
+
if (dataDirectoryState === "available") {
|
|
2549
|
+
const databaseProbe = probeSessionDatabase(sessionDbPath);
|
|
2550
|
+
if (databaseProbe === "detected") {
|
|
2551
|
+
info("Session database file detected; full validation occurs on session operations");
|
|
2552
|
+
} else if (databaseProbe === "unsafe") {
|
|
2553
|
+
error("Session database could not be opened safely");
|
|
2554
|
+
} else {
|
|
2555
|
+
info("Session database not initialized \u2014 will initialize on first session operation");
|
|
198
2556
|
}
|
|
199
|
-
} else if (
|
|
200
|
-
|
|
2557
|
+
} else if (dataDirectoryState === "missing") {
|
|
2558
|
+
info("Session database not initialized \u2014 will initialize on first session operation");
|
|
201
2559
|
} else {
|
|
202
|
-
|
|
2560
|
+
info("Session database not checked because the data directory is unsafe");
|
|
203
2561
|
}
|
|
204
2562
|
console.log("\nIDE registrations:");
|
|
205
|
-
const { detectInstalledIdes } = await import("./ide-detector-
|
|
2563
|
+
const { detectInstalledIdes } = await import("./ide-detector-ETGAVVXO.js");
|
|
206
2564
|
const ides = detectInstalledIdes();
|
|
207
2565
|
for (const ide of ides) {
|
|
208
2566
|
if (!ide.detected) continue;
|
|
209
|
-
if (ide.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
warning("Claude Code: detected but not registered");
|
|
224
|
-
}
|
|
225
|
-
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
226
|
-
if (existsSync(settingsPath)) {
|
|
227
|
-
try {
|
|
228
|
-
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
229
|
-
const perms = settings.permissions?.allow || [];
|
|
230
|
-
if (perms.some((p) => p.includes("agentcache"))) {
|
|
231
|
-
pass("Claude Code permissions: auto-approved");
|
|
232
|
-
} else {
|
|
233
|
-
warning("Claude Code permissions: not in allow list");
|
|
234
|
-
}
|
|
235
|
-
if (settings.hooks?.Stop?.some((h) => JSON.stringify(h).includes("agentcache"))) {
|
|
236
|
-
pass("Claude Code hooks: registered");
|
|
237
|
-
} else {
|
|
238
|
-
warning("Claude Code hooks: not registered");
|
|
239
|
-
}
|
|
240
|
-
} catch {
|
|
241
|
-
warning("Claude Code settings: unreadable");
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
} else if (ide.mcpConfigFormat === "codex-toml") {
|
|
245
|
-
if (existsSync(ide.mcpConfigPath)) {
|
|
246
|
-
try {
|
|
247
|
-
const content = readFileSync(ide.mcpConfigPath, "utf-8");
|
|
248
|
-
if (content.includes("[mcp_servers.agentcache]")) {
|
|
249
|
-
pass(`${ide.name}: registered`);
|
|
250
|
-
} else {
|
|
251
|
-
warning(`${ide.name}: detected but not registered`);
|
|
252
|
-
}
|
|
253
|
-
} catch {
|
|
254
|
-
warning(`${ide.name}: config unreadable`);
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
warning(`${ide.name}: detected but not registered`);
|
|
2567
|
+
if (isMcpServerRegistered(ide)) pass(`${ide.name}: registered`);
|
|
2568
|
+
else warning(`${ide.name}: detected but not registered`);
|
|
2569
|
+
}
|
|
2570
|
+
const claudeSettingsPath = join(homedir(), ".claude", "settings.json");
|
|
2571
|
+
if (existsSync(claudeSettingsPath)) {
|
|
2572
|
+
try {
|
|
2573
|
+
const settings = JSON.parse(readBoundedInputFile(claudeSettingsPath));
|
|
2574
|
+
const permissions = Array.isArray(settings.permissions?.allow) ? settings.permissions.allow : [];
|
|
2575
|
+
if (permissions.some(
|
|
2576
|
+
(permission) => typeof permission === "string" && permission.startsWith("mcp__agentcache__")
|
|
2577
|
+
)) {
|
|
2578
|
+
warning("Claude Code legacy blanket approvals detected \u2014 run `agentcache setup` to remove");
|
|
258
2579
|
}
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
pass(`${ide.name}: registered`);
|
|
265
|
-
} else {
|
|
266
|
-
warning(`${ide.name}: detected but not registered`);
|
|
267
|
-
}
|
|
268
|
-
} catch {
|
|
269
|
-
warning(`${ide.name}: config unreadable`);
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
warning(`${ide.name}: detected but not registered`);
|
|
2580
|
+
const legacyHookPresence = inspectLegacyClaudeHookPresence(settings);
|
|
2581
|
+
if (legacyHookPresence === "present") {
|
|
2582
|
+
warning("Claude Code legacy automatic hooks detected \u2014 run `agentcache setup` to remove");
|
|
2583
|
+
} else if (legacyHookPresence === "unknown") {
|
|
2584
|
+
warning("Claude Code hook configuration is ambiguous; no hooks were changed");
|
|
273
2585
|
}
|
|
2586
|
+
} catch {
|
|
2587
|
+
warning("Claude Code settings unreadable; legacy automation state could not be checked");
|
|
274
2588
|
}
|
|
275
2589
|
}
|
|
276
2590
|
const notDetected = ides.filter((i) => !i.detected).map((i) => i.name);
|
|
277
2591
|
if (notDetected.length > 0) {
|
|
278
2592
|
console.log(` \xB7 Not detected: ${notDetected.join(", ")}`);
|
|
279
2593
|
}
|
|
280
|
-
console.log("\nTranscript sources:");
|
|
281
|
-
const { findAllClaudeTranscripts, findAllCursorTranscripts, findAllContinueTranscripts, findAllCodexTranscripts, findAllRooCodeTranscripts } = await import("./transcript-JWSGSDSF.js");
|
|
282
|
-
const sources = [
|
|
283
|
-
{ name: "Claude Code", fn: findAllClaudeTranscripts },
|
|
284
|
-
{ name: "Cursor", fn: findAllCursorTranscripts },
|
|
285
|
-
{ name: "Continue", fn: findAllContinueTranscripts },
|
|
286
|
-
{ name: "Codex", fn: findAllCodexTranscripts },
|
|
287
|
-
{ name: "Roo Code", fn: findAllRooCodeTranscripts }
|
|
288
|
-
];
|
|
289
|
-
let totalTranscripts = 0;
|
|
290
|
-
for (const src of sources) {
|
|
291
|
-
const count = src.fn().length;
|
|
292
|
-
totalTranscripts += count;
|
|
293
|
-
if (count > 0) pass(`${src.name}: ${count} transcripts`);
|
|
294
|
-
}
|
|
295
|
-
if (totalTranscripts === 0) {
|
|
296
|
-
warning("No transcripts found from any IDE");
|
|
297
|
-
}
|
|
298
|
-
console.log("\nLLM backends (for compile-all):");
|
|
299
|
-
const backends = ["claude", "codex", "gemini", "copilot", "aider", "goose"];
|
|
300
|
-
const found = [];
|
|
301
|
-
for (const cmd of backends) {
|
|
302
|
-
try {
|
|
303
|
-
if (spawnSync("which", [cmd], { encoding: "utf-8", timeout: 3e3 }).status === 0) {
|
|
304
|
-
found.push(cmd);
|
|
305
|
-
}
|
|
306
|
-
} catch {
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (process.env.ANTHROPIC_API_KEY) found.push("Anthropic API (env)");
|
|
310
|
-
if (process.env.OPENAI_API_KEY) found.push("OpenAI API (env)");
|
|
311
|
-
if (found.length > 0) {
|
|
312
|
-
pass(`Available: ${found.join(", ")}`);
|
|
313
|
-
} else {
|
|
314
|
-
warning("No LLM backend found \u2014 compile-all won't work");
|
|
315
|
-
}
|
|
316
2594
|
console.log("\nRuntime:");
|
|
317
2595
|
const nodeVersion = process.version;
|
|
318
2596
|
const major = parseInt(nodeVersion.slice(1));
|
|
319
|
-
if (major >=
|
|
2597
|
+
if (major >= 22) {
|
|
320
2598
|
pass(`Node ${nodeVersion}`);
|
|
321
2599
|
} else {
|
|
322
|
-
error(`Node ${nodeVersion} \u2014 requires >=
|
|
2600
|
+
error(`Node ${nodeVersion} \u2014 requires >=22`);
|
|
323
2601
|
}
|
|
324
2602
|
console.log(`
|
|
325
2603
|
${ok} passed, ${warn} warnings, ${fail} errors`);
|
|
326
2604
|
if (fail > 0) process.exit(1);
|
|
327
2605
|
});
|
|
328
|
-
program
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
program.command("status").description("Show AgentCache knowledge stats").action(async () => {
|
|
333
|
-
const { getDbPath, isInitialized, findProjectRoot, getProjectId, getProjectDisplayName } = await import("./paths-5LZRKNYY.js");
|
|
334
|
-
if (!isInitialized()) {
|
|
335
|
-
console.log("AgentCache not initialized. Run: agentcache setup");
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
const { SqliteKnowledgeRepository } = await import("./sqlite-NM2BVHUY.js");
|
|
339
|
-
const repo = new SqliteKnowledgeRepository(getDbPath());
|
|
340
|
-
const projectRoot = findProjectRoot();
|
|
341
|
-
const project = getProjectId(projectRoot);
|
|
342
|
-
const displayName = getProjectDisplayName(projectRoot);
|
|
343
|
-
const items = repo.getKnowledgeForContext(project);
|
|
344
|
-
const rules = items.filter((i) => i.type === "rule");
|
|
345
|
-
const lessons = items.filter((i) => i.type === "lesson");
|
|
346
|
-
const decisions = items.filter((i) => i.type === "decision");
|
|
347
|
-
const context = items.filter((i) => i.type === "context");
|
|
348
|
-
const globalItems = items.filter((i) => i.scope === "global");
|
|
349
|
-
const projectItems = items.filter((i) => i.scope === "project");
|
|
350
|
-
const pending = repo.getPendingCount();
|
|
351
|
-
console.log(`AgentCache \u2014 ${displayName} (${project})`);
|
|
352
|
-
console.log(` ${items.length} items (${globalItems.length} global, ${projectItems.length} project)`);
|
|
353
|
-
console.log(` ${rules.length} rules | ${lessons.length} lessons | ${decisions.length} decisions | ${context.length} context`);
|
|
354
|
-
if (pending > 0) console.log(` ${pending} sessions pending compilation`);
|
|
355
|
-
const allProjects = repo.getProjectStats();
|
|
356
|
-
if (allProjects.length > 1) {
|
|
357
|
-
console.log("");
|
|
358
|
-
console.log("All projects:");
|
|
359
|
-
for (const p of allProjects) {
|
|
360
|
-
const marker = p.project === project ? " \u2190 current" : "";
|
|
361
|
-
console.log(` ${p.project}: ${p.count} items${marker}`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
repo.close();
|
|
365
|
-
});
|
|
366
|
-
program.parse();
|
|
2606
|
+
registerSessionCommands(program);
|
|
2607
|
+
registerAdapterCommands(program);
|
|
2608
|
+
var exitCode = await runCliProgram(program, process.argv);
|
|
2609
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|