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.
Files changed (38) hide show
  1. package/README.md +282 -151
  2. package/dist/{chunk-T4COG3XD.js → chunk-R5I6WWSD.js} +31 -14
  3. package/dist/chunk-RXGW4Q3G.js +109 -0
  4. package/dist/chunk-XRJ6QW6N.js +92 -0
  5. package/dist/chunk-YKG6CDGT.js +1818 -0
  6. package/dist/chunk-YY7QXBG5.js +6610 -0
  7. package/dist/cli.js +2535 -292
  8. package/dist/device-id-RV7RO5RB.js +7 -0
  9. package/dist/ide-detector-ETGAVVXO.js +8 -0
  10. package/dist/mcp.d.ts +734 -2
  11. package/dist/mcp.js +1126 -446
  12. package/dist/{paths-5LZRKNYY.js → paths-NTZ2357O.js} +3 -2
  13. package/dist/postinstall.js +1 -65
  14. package/dist/setup-7JJPW3VG.js +48 -0
  15. package/docs/compatibility.md +152 -0
  16. package/docs/demo-script.md +121 -0
  17. package/docs/launch-copy.md +125 -0
  18. package/docs/privacy.md +173 -0
  19. package/docs/troubleshooting.md +209 -0
  20. package/package.json +32 -14
  21. package/dist/3-canonicalizer-HIN2F7SZ.js +0 -11
  22. package/dist/chunk-5UO7NJPQ.js +0 -71
  23. package/dist/chunk-CUBZRYS5.js +0 -580
  24. package/dist/chunk-GGAATZKM.js +0 -120
  25. package/dist/chunk-JUDLOBOC.js +0 -77
  26. package/dist/chunk-KFQGP6VL.js +0 -33
  27. package/dist/chunk-PSASDZQE.js +0 -490
  28. package/dist/chunk-SLRKWMSE.js +0 -202
  29. package/dist/chunk-T7BJPANN.js +0 -45
  30. package/dist/chunk-WTXSZBQE.js +0 -388
  31. package/dist/compile-all-PTWTZVP5.js +0 -495
  32. package/dist/ide-detector-5TRCR4F5.js +0 -7
  33. package/dist/pre-tool-use-A4AJHZOJ.js +0 -30
  34. package/dist/session-start-DGMGEAJU.js +0 -78
  35. package/dist/setup-CVG35TUZ.js +0 -51
  36. package/dist/sqlite-NM2BVHUY.js +0 -7
  37. package/dist/stop-WGGRX6TQ.js +0 -38
  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
- var program = new Command();
6
- program.name("agentcache").description("Engineering Knowledge Compiler \u2014 universal, zero-config").version("0.3.1");
7
- program.command("setup").description("Detect IDEs and register AgentCache (runs automatically on install)").action(async () => {
8
- const { runSetup } = await import("./setup-CVG35TUZ.js");
9
- await runSetup();
10
- });
11
- program.command("serve").description("Start AgentCache MCP server (spawned by IDEs automatically)").action(async () => {
12
- const { startMcpServer } = await import("./mcp.js");
13
- await startMcpServer();
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
- program.command("compile-session").description("Stop hook: queue transcript for compilation").action(async () => {
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 { handleStop } = await import("./stop-WGGRX6TQ.js");
18
- let payload;
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
- let data = "";
21
- for await (const chunk of process.stdin) {
22
- data += chunk;
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 (data.trim()) {
25
- payload = JSON.parse(data);
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
- await handleStop(payload);
30
- } catch (err) {
31
- process.stderr.write(`agentcache compile-session: ${err.message}
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
- program.command("discover").description("SessionStart hook: discover uncompiled transcripts").action(async () => {
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
- const { handleSessionStart } = await import("./session-start-DGMGEAJU.js");
38
- await handleSessionStart();
39
- } catch (err) {
40
- process.stderr.write(`agentcache discover: ${err.message}
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
- program.command("enforce").description("PreToolUse hook: policy enforcement").action(async () => {
45
- let data = "";
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
- try {
50
- const { handlePreToolUse } = await import("./pre-tool-use-A4AJHZOJ.js");
51
- const input = JSON.parse(data);
52
- const result = handlePreToolUse(input);
53
- process.stdout.write(JSON.stringify(result));
54
- } catch (err) {
55
- process.stderr.write(`agentcache enforce: ${err.message}
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
- program.command("review").description("Review quarantined observations \u2014 approve or reject before they're injected").option("--approve-all", "Approve all pending items").option("--reject-all", "Reject (archive) all pending items").action(async (opts) => {
61
- const { getDbPath, isInitialized, findProjectRoot, getProjectId } = await import("./paths-5LZRKNYY.js");
62
- if (!isInitialized()) {
63
- console.log("AgentCache not initialized. Run: agentcache setup");
64
- return;
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
- const { SqliteKnowledgeRepository } = await import("./sqlite-NM2BVHUY.js");
67
- const repo = new SqliteKnowledgeRepository(getDbPath());
68
- const project = getProjectId(findProjectRoot());
69
- const items = repo.getQuarantinedItems(project);
70
- if (items.length === 0) {
71
- console.log("No quarantined items. All observations are either approved or auto-promoted.");
72
- repo.close();
73
- return;
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
- if (opts.approveAll) {
76
- for (const item of items) {
77
- repo.promoteItem(item.id);
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
- console.log(`Approved ${items.length} items. They will now be injected into future sessions.`);
80
- repo.close();
81
- return;
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 (opts.rejectAll) {
84
- for (const item of items) {
85
- repo.updateKnowledgeItem(item.id, { status: "archived", updatedAt: Date.now() });
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
- console.log(`Rejected ${items.length} items. They will not be injected.`);
88
- repo.close();
89
- return;
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
- console.log(`${items.length} quarantined observation(s):
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
- for (const item of items) {
94
- const age = Math.round((Date.now() - item.createdAt) / (1e3 * 60 * 60));
95
- console.log(` [${item.id}] (${item.type}/${item.scope}) ${age}h ago`);
96
- console.log(` ${item.content.slice(0, 120)}`);
97
- console.log("");
98
- }
99
- console.log("Actions:");
100
- console.log(" agentcache review --approve-all Approve all and inject into sessions");
101
- console.log(" agentcache review --reject-all Archive all (won't be injected)");
102
- console.log(" agentcache promote <id> Approve a specific item");
103
- repo.close();
104
- });
105
- program.command("promote <id>").description("Promote a specific quarantined item to approved (USER authority)").action(async (id) => {
106
- const { getDbPath, isInitialized } = await import("./paths-5LZRKNYY.js");
107
- if (!isInitialized()) {
108
- console.log("AgentCache not initialized. Run: agentcache setup");
109
- return;
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 { SqliteKnowledgeRepository } = await import("./sqlite-NM2BVHUY.js");
112
- const repo = new SqliteKnowledgeRepository(getDbPath());
113
- const item = repo.getKnowledgeItem(id);
114
- if (!item) {
115
- console.log(`Item not found: ${id}`);
116
- repo.close();
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
- repo.promoteItem(id);
120
- console.log(`Promoted: ${item.content.slice(0, 80)}`);
121
- repo.close();
122
- });
123
- program.command("add-rule <content>").description("Add an enforced policy rule (human-only, blocks tool calls that violate it)").option("--global", "Apply to all projects (default: current project only)").action(async (content, opts) => {
124
- const { getDbPath, isInitialized, findProjectRoot, getProjectId } = await import("./paths-5LZRKNYY.js");
125
- const { randomUUID } = await import("crypto");
126
- if (!isInitialized()) {
127
- console.log("AgentCache not initialized. Run: agentcache setup");
128
- return;
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
- const { SqliteKnowledgeRepository } = await import("./sqlite-NM2BVHUY.js");
131
- const { computeCanonicalHash } = await import("./3-canonicalizer-HIN2F7SZ.js");
132
- const repo = new SqliteKnowledgeRepository(getDbPath());
133
- const project = getProjectId(findProjectRoot());
134
- const scope = opts.global ? "global" : "project";
135
- repo.saveKnowledgeItem({
136
- id: `ki_${randomUUID().slice(0, 8)}`,
137
- canonicalHash: computeCanonicalHash(content),
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, readFileSync } = await import("fs");
2505
+ const { existsSync, lstatSync: lstatSync2 } = await import("fs");
158
2506
  const { join } = await import("path");
159
2507
  const { homedir } = await import("os");
160
- const { spawnSync } = await import("child_process");
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
- if (existsSync(dataDir)) {
181
- pass(`Data directory exists: ${dataDir}`);
182
- } else {
183
- error(`Data directory missing: ${dataDir}`);
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 dbPath = getDbPath();
186
- if (existsSync(dbPath)) {
187
- try {
188
- const { SqliteKnowledgeRepository } = await import("./sqlite-NM2BVHUY.js");
189
- const repo = new SqliteKnowledgeRepository(dbPath);
190
- repo.close();
191
- pass(`Database accessible: ${dbPath}`);
192
- } catch (err) {
193
- if (err.message?.includes("NODE_MODULE_VERSION") || err.message?.includes("was compiled against")) {
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 (isInitialized()) {
200
- warning("Database file missing but data directory exists");
2557
+ } else if (dataDirectoryState === "missing") {
2558
+ info("Session database not initialized \u2014 will initialize on first session operation");
201
2559
  } else {
202
- warning("Not initialized yet \u2014 run: agentcache setup");
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-5TRCR4F5.js");
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.mcpConfigFormat === "claude-settings") {
210
- const claudeJson = join(homedir(), ".claude.json");
211
- if (existsSync(claudeJson)) {
212
- try {
213
- const config = JSON.parse(readFileSync(claudeJson, "utf-8"));
214
- if (config.mcpServers?.agentcache) {
215
- pass("Claude Code: registered");
216
- } else {
217
- warning("Claude Code: detected but not registered");
218
- }
219
- } catch {
220
- warning("Claude Code: config unreadable");
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
- } else {
260
- if (existsSync(ide.mcpConfigPath)) {
261
- try {
262
- const config = JSON.parse(readFileSync(ide.mcpConfigPath, "utf-8"));
263
- if (config.mcpServers?.agentcache) {
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 >= 20) {
2597
+ if (major >= 22) {
320
2598
  pass(`Node ${nodeVersion}`);
321
2599
  } else {
322
- error(`Node ${nodeVersion} \u2014 requires >=20.12.0`);
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.command("compile-all").description("Batch-compile all unprocessed transcripts using an available LLM CLI").action(async () => {
329
- const { runCompileAll } = await import("./compile-all-PTWTZVP5.js");
330
- await runCompileAll();
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;