codemem 0.46.3 → 0.47.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { ContextOnlyReplayError, DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, DeviceIdentityError, EXTRACTION_BENCHMARK_QUALITY_WEIGHTS, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, ObserverOutputError, ObserverOutputTransportError, PROMPT_TRANSPORT_PROTOCOL_RANGE, REF_BACKFILL_JOB, RawEventIngestValidationError, RawEventSweeper, RefBackfillRunner, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, TRUSTED_HOOK_MAPPER_OPTIONS, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, applyDistillRule, arePromptTransportProtocolRangesCompatible, atomicReplaceConfigFile, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildTieredObserverSelection, buildViewerIdentityTarget, classifyPromptTransportFailure, clonePromptPackAttempt, collectOperationalStatus, compareMemoryRoleReports, connect, connectReadOnly, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, describePiObserverStatus, detectInstallKind, draftDistillRule, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, exportMemories, extractApplyPatchPaths, extractionReplayObserverIdentity, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getAttributionDiagnostics, getCodememConfigPath, getExtractionBenchmarkProfile, getExtractionModelPricing, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryArtifactReport, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getUpdateStatus, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, ingestRawEvents, initDatabase, isAutomaticRecallMeasurement, isEmbeddingDisabled, isExplicitUpdateInstallEligible, isReleaseVersionForChannel, judgeDistillReport, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadSqliteVec, mdnsEnabled, mutateCodememConfigFile, normalizePromptTransportProtocolRange, observerBaseUrlForProviderOverride, planReplicationOpsAgePrune, probeCodememViewerLiveness, probeCodememViewerLiveness as probeCodememViewerLiveness$1, probeRequiredNativeRuntime, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, recordAutomaticRecall, recordPromptPackArtifacts, recordPromptPackTerminal, recordRetrievalSurface, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveComparablePath, resolveDbPath, resolveHookProject, resolvePiAgentDir, resolvePiObserverConfig, resolveProject, resolveProjectRoot, resolveRetrievalSession, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, setPeerProjectFilter, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, vacuumDatabase, viewerUrl, writeCodememConfigFile } from "@codemem/core";
2
+ import { ACCOUNT_CONNECTION_MIGRATION_STATEMENTS, ACCOUNT_UNLINK_MIGRATION_STATEMENTS, AUTH_LINK_AUTHORITY_CUTOVER_SQL, ContextOnlyReplayError, CoordinatorAccountLinkError, DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, DeviceIdentityError, EXTRACTION_BENCHMARK_QUALITY_WEIGHTS, FILE_CONTEXT_TRANSPORT_MAX_BYTES, FILE_CONTEXT_TRANSPORT_PROFILE, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, ObserverOutputError, ObserverOutputTransportError, OwnerProfileEnrollmentError, PROMPT_TRANSPORT_PROTOCOL_RANGE, REF_BACKFILL_JOB, RawEventIngestValidationError, RawEventSweeper, RefBackfillRunner, RemoteCoordinatorRequestError, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, TRUSTED_HOOK_MAPPER_OPTIONS, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, applyDistillRule, arePromptTransportProtocolRangesCompatible, atomicReplaceConfigFile, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildTieredObserverSelection, buildViewerIdentityTarget, classifyPromptTransportFailure, clonePromptPackAttempt, collectOperationalStatus, compareMemoryRoleReports, connect, connectReadOnly, coordinatorAuthControllerReviewAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantIdentityGroupAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorQualifyOwnerAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewInviteAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeDeviceAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, createFileContextRunner, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, describePiObserverStatus, detectInstallKind, dispatchFileContextTransport, draftDistillRule, enrollOwnerProfile, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, expandUserPath, exportMemories, extractApplyPatchPaths, extractionReplayObserverIdentity, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getAttributionDiagnostics, getCodememConfigPath, getExtractionBenchmarkProfile, getExtractionModelPricing, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryArtifactReport, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getUpdateStatus, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, importPiSessions, ingestRawEvents, initDatabase, isAutomaticRecallMeasurement, isCoordinatorControllerAttestationReference, isEmbeddingDisabled, isExplicitUpdateInstallEligible, isReleaseVersionForChannel, judgeDistillReport, linkCoordinatorAccount, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadRuntimeSigningPublicKey, loadSqliteVec, mdnsEnabled, mutateCodememConfigFile, normalizeCoordinatorAccountLinkOrigin, normalizePromptTransportProtocolRange, observerBaseUrlForProviderOverride, parseFileContextTransportRequest, planReplicationOpsAgePrune, probeCodememViewerLiveness, probeCodememViewerLiveness as probeCodememViewerLiveness$1, probeRequiredNativeRuntime, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readAccountLinkDevice, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorAccountLinkStatus, readCoordinatorOwnerReviewLocalEvidence, readCoordinatorSyncConfig, readImportPayload, recordAutomaticRecall, recordPromptPackArtifacts, recordPromptPackTerminal, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveComparablePath, resolveDbPath, resolveHookProject, resolveKeyPaths, resolvePiAgentDir, resolvePiObserverConfig, resolveProject, resolveProjectRoot, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, searchPiSessions, setPeerProjectFilter, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, upgradeCoordinatorAccountSchemaFile, upgradeCoordinatorAuthSchemaFile, vacuumDatabase, viewerUrl, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
- import { randomInt, randomUUID } from "node:crypto";
6
5
  import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
6
  import { homedir, networkInterfaces } from "node:os";
8
7
  import { dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
9
- import { styleText } from "node:util";
8
+ import { isDeepStrictEqual, styleText } from "node:util";
9
+ import { randomInt, randomUUID } from "node:crypto";
10
10
  import * as p from "@clack/prompts";
11
11
  import { serve } from "@hono/node-server";
12
12
  import { execFileSync, spawn, spawnSync } from "node:child_process";
@@ -79,6 +79,54 @@ function emitJsonError(errorCode, message, exitCode = 1) {
79
79
  process.exitCode = exitCode;
80
80
  }
81
81
  //#endregion
82
+ //#region src/commands/claude-hook-file-context-transport.ts
83
+ /** Internal packaged-hook transport. Normal CLI invocation keeps its original guards/output. */
84
+ function runClaudeFileContextTransport(payload, opts) {
85
+ const parsed = parseFileContextTransportRequest(payload);
86
+ if (!parsed.success) throw new Error("invalid file-context transport request");
87
+ let request = parsed.data;
88
+ if ([
89
+ "1",
90
+ "true",
91
+ "yes",
92
+ "on"
93
+ ].includes(String(process.env.CODEMEM_PLUGIN_IGNORE ?? "").trim().toLowerCase())) return {
94
+ ok: true,
95
+ attempt_id: request.attempt_id,
96
+ request_id: request.request_id,
97
+ result: { continue: true },
98
+ delivery_pending: false
99
+ };
100
+ const disabled = (value) => [
101
+ "0",
102
+ "false",
103
+ "off"
104
+ ].includes(String(value ?? "").trim().toLowerCase());
105
+ if (disabled(process.env.CODEMEM_RETRIEVAL_LEDGER)) request = {
106
+ ...request,
107
+ ledger_enabled: false
108
+ };
109
+ if (request.action === "lookup" && disabled(process.env.CODEMEM_FILE_CONTEXT)) {
110
+ const { relative_path: _path, project: _project, file_mtime_ms: _mtime, ...base } = request;
111
+ request = {
112
+ ...base,
113
+ action: "skip",
114
+ reason: "file_context_disabled"
115
+ };
116
+ }
117
+ const dbPath = resolveDbPath(resolveDbOpt(opts));
118
+ if (resolve(dbPath) !== resolve(request.db_path) || !isDeepStrictEqual(buildViewerIdentityTarget(), request.identity_target)) throw new Error("file-context target mismatch");
119
+ const store = new MemoryStore(dbPath);
120
+ try {
121
+ if (!store.hasCurrentIdentity()) throw new Error("file-context identity changed");
122
+ return dispatchFileContextTransport(store, request);
123
+ } finally {
124
+ try {
125
+ store.close();
126
+ } catch {}
127
+ }
128
+ }
129
+ //#endregion
82
130
  //#region src/commands/claude-hook-plugin-log.ts
83
131
  /**
84
132
  * Append-only plugin event log used by `claude-hook-inject` and
@@ -130,24 +178,12 @@ function logHookEvent(message) {
130
178
  //#endregion
131
179
  //#region src/commands/claude-hook-file-context.ts
132
180
  var FILE_GATE_MIN_BYTES = 1500;
133
- var FETCH_LIMIT = 40;
134
- var DISPLAY_LIMIT = 15;
135
- var MTIME_FRESH_TOLERANCE_MS = 3e5;
136
181
  var SMALL_FILE_BYPASS_PATTERNS = [
137
182
  /\.(json|jsonc|toml|ya?ml)$/i,
138
183
  /\.env(\.|$)/i,
139
184
  /(^|\/)dockerfile(\.|$)/i,
140
185
  /\.config\.(js|ts|mjs|cjs|json)$/i
141
186
  ];
142
- var KIND_ICONS = {
143
- decision: "⚖️",
144
- bugfix: "🔴",
145
- feature: "🟢",
146
- refactor: "🔄",
147
- discovery: "🔵",
148
- change: "✅",
149
- exploration: "🔬"
150
- };
151
187
  function emitJson$2(value) {
152
188
  console.log(JSON.stringify(value));
153
189
  }
@@ -187,105 +223,6 @@ function statFile(absPath) {
187
223
  return null;
188
224
  }
189
225
  }
190
- function parseJsonArray(value) {
191
- if (!value) return [];
192
- try {
193
- const parsed = JSON.parse(value);
194
- if (!Array.isArray(parsed)) return [];
195
- return parsed.filter((item) => typeof item === "string");
196
- } catch {
197
- return [];
198
- }
199
- }
200
- function normalizePathForCompare(path) {
201
- return path.replace(/\\/g, "/");
202
- }
203
- function scoreRow(row, normalizedTarget, idx) {
204
- const filesModified = parseJsonArray(row.files_modified);
205
- const inModified = filesModified.some((f) => normalizePathForCompare(f) === normalizedTarget);
206
- let score = 0;
207
- if (inModified) score += 2;
208
- if (filesModified.length <= 1) score += 2;
209
- else if (filesModified.length <= 3) score += 1;
210
- return {
211
- row,
212
- score,
213
- idx
214
- };
215
- }
216
- function scoreAndDedupe(rows, targetPath, limit) {
217
- const normalizedTarget = normalizePathForCompare(targetPath);
218
- const scored = rows.map((row, idx) => scoreRow(row, normalizedTarget, idx));
219
- const bestPerSession = /* @__PURE__ */ new Map();
220
- for (const item of scored) {
221
- const existing = bestPerSession.get(item.row.session_id);
222
- if (!existing || item.score > existing.score || item.score === existing.score && item.idx < existing.idx) bestPerSession.set(item.row.session_id, item);
223
- }
224
- const deduped = Array.from(bestPerSession.values());
225
- deduped.sort((a, b) => b.score - a.score || a.idx - b.idx);
226
- return deduped.slice(0, limit).map((s) => s.row);
227
- }
228
- function compactTime(timeStr) {
229
- return timeStr.toLowerCase().replace(" am", "a").replace(" pm", "p");
230
- }
231
- function formatTime(epochMs) {
232
- return new Date(epochMs).toLocaleString("en-US", {
233
- hour: "numeric",
234
- minute: "2-digit",
235
- hour12: true
236
- });
237
- }
238
- function formatDate(epochMs) {
239
- return new Date(epochMs).toLocaleString("en-US", {
240
- month: "short",
241
- day: "numeric",
242
- year: "numeric"
243
- });
244
- }
245
- function formatTimeline(rows, filePath, staleness) {
246
- const safePath = filePath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
247
- const enriched = rows.map((row) => ({
248
- row,
249
- epochMs: Date.parse(row.created_at)
250
- })).filter((item) => Number.isFinite(item.epochMs) && item.epochMs > 0);
251
- const byDay = /* @__PURE__ */ new Map();
252
- for (const item of enriched) {
253
- const day = formatDate(item.epochMs);
254
- const bucket = byDay.get(day);
255
- if (bucket) bucket.push(item);
256
- else byDay.set(day, [item]);
257
- }
258
- const sortedDays = Array.from(byDay.entries()).sort((a, b) => {
259
- return Math.min(...a[1].map((i) => i.epochMs)) - Math.min(...b[1].map((i) => i.epochMs));
260
- });
261
- const ids = rows.map((r) => r.id);
262
- const lines = [`This file (${safePath}) has prior codemem observations. The Read result below is unchanged.`, `- Fetch full bodies on demand: memory.get_observations([${ids.join(", ")}]).`];
263
- if (staleness) {
264
- const driftMinutes = Math.max(1, Math.round((staleness.fileMtimeMs - staleness.newestObservationMs) / 6e4));
265
- lines.unshift(`Heads up: this file was modified ~${driftMinutes} min after the most recent observation below. Past entries may be partially stale — verify against the Read result before relying on them.`);
266
- }
267
- for (const [day, dayItems] of sortedDays) {
268
- const chronological = [...dayItems].sort((a, b) => a.epochMs - b.epochMs);
269
- lines.push(`### ${day}`);
270
- for (const { row, epochMs } of chronological) {
271
- const title = (row.title || "Untitled").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
272
- const icon = KIND_ICONS[row.kind] ?? "❔";
273
- const time = compactTime(formatTime(epochMs));
274
- lines.push(`${row.id} ${time} ${icon} (${row.kind}) ${title}`);
275
- }
276
- }
277
- return lines.join("\n");
278
- }
279
- function queryByFile(dbPath, relativePath, project, limit) {
280
- const store = new MemoryStore(dbPath);
281
- try {
282
- const opts = { limit };
283
- if (project) opts.project = project;
284
- return store.findByFile(relativePath, opts);
285
- } finally {
286
- store.close();
287
- }
288
- }
289
226
  function resolveProject$1(payload) {
290
227
  const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
291
228
  return resolveHookProject(cwd, payload.project);
@@ -294,73 +231,45 @@ function sourceSessionId(payload) {
294
231
  const value = payload.session_id;
295
232
  return typeof value === "string" && value.trim() ? value.trim() : null;
296
233
  }
297
- async function buildClaudeFileContext(payload, opts, deps = {}) {
298
- if (envTruthy$4(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$2();
299
- const filePath = extractFilePath(payload);
300
- if (!filePath) return continueResult$2();
301
- const startedAt = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
302
- const attemptId = (deps.createAttemptId ?? randomUUID)();
303
- const resolveDb = deps.resolveDb ?? resolveDbPath;
234
+ function fileContextStore(opts, deps) {
304
235
  let resolvedDbPath = null;
305
236
  let activeStore = null;
306
- const getStore = () => {
307
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
308
- activeStore ??= (deps.createStore ?? ((dbPath) => new MemoryStore(dbPath)))(resolvedDbPath);
309
- return activeStore;
237
+ const dbPath = () => {
238
+ resolvedDbPath ??= (deps.resolveDb ?? resolveDbPath)(resolveDbOpt(opts));
239
+ return resolvedDbPath;
310
240
  };
311
- const finish = (result) => {
312
- try {
313
- activeStore?.close();
314
- } catch {}
315
- activeStore = null;
316
- return result;
317
- };
318
- const record = (attemptInput) => {
319
- if (!envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1")) return;
320
- try {
321
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
322
- const completedAt = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
323
- const ledgerInput = {
324
- ...attemptInput,
325
- attemptId,
326
- surface: "file_context",
327
- trigger: "automatic",
328
- startedAt: startedAt.toISOString(),
329
- completedAt: completedAt.toISOString(),
330
- latencyMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
331
- recorderVersion: "claude-file-context-v1",
332
- source: "claude",
333
- streamId: sourceSessionId(payload),
334
- sourceSessionId: sourceSessionId(payload),
335
- mode: "claude_pre_tool_use_read"
336
- };
337
- if (deps.recordAttempt) deps.recordAttempt(resolvedDbPath, ledgerInput);
338
- else {
339
- const store = getStore();
340
- recordRetrievalSurface(store.db, {
341
- ...ledgerInput,
342
- sessionId: resolveRetrievalSession(store.db, "claude", ledgerInput.sourceSessionId)
343
- });
344
- }
345
- } catch {}
346
- };
347
- const updateDelivery = (status) => {
348
- if (!envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1")) return;
349
- try {
350
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
351
- if (deps.updateDelivery) deps.updateDelivery(resolvedDbPath, attemptId, status);
352
- else tryUpdateRetrievalDelivery(getStore().db, attemptId, status);
353
- } catch {}
354
- };
355
- if (!envNotDisabled$3(process.env.CODEMEM_FILE_CONTEXT || "1")) {
356
- record({
357
- retrievalStatus: "skipped",
358
- deliveryStatus: "not_attempted",
359
- failureCode: "file_context_disabled",
360
- failureStage: "configuration"
361
- });
362
- return finish(continueResult$2());
363
- }
241
+ return {
242
+ dbPath,
243
+ getStore: () => {
244
+ activeStore ??= (deps.createStore ?? ((path) => new MemoryStore(path)))(dbPath());
245
+ return activeStore;
246
+ },
247
+ finish: (result) => {
248
+ try {
249
+ activeStore?.close();
250
+ } catch {}
251
+ activeStore = null;
252
+ return result;
253
+ }
254
+ };
255
+ }
256
+ function fileContextRunner(payload, store, deps) {
257
+ const findByFile = deps.queryByFile;
258
+ const recordAttempt = deps.recordAttempt;
259
+ const updateDelivery = deps.updateDelivery;
260
+ return createFileContextRunner({
261
+ getStore: store.getStore,
262
+ sourceSessionId: sourceSessionId(payload),
263
+ ledgerEnabled: () => envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1"),
264
+ now: deps.now,
265
+ createAttemptId: deps.createAttemptId,
266
+ log: logHookEvent,
267
+ ...findByFile ? { findByFile: (path, options) => findByFile(store.dbPath(), path, options.project ?? null, options.limit) } : {},
268
+ ...recordAttempt ? { recordAttempt: (input) => recordAttempt(store.dbPath(), input) } : {},
269
+ ...updateDelivery ? { updateDelivery: (attemptId, status) => updateDelivery(store.dbPath(), attemptId, status) } : {}
270
+ });
271
+ }
272
+ function fileContextPath(filePath, payload) {
364
273
  const cwd = typeof payload.cwd === "string" && payload.cwd.trim() ? payload.cwd : process.cwd();
365
274
  const expandedPath = expandHome$2(filePath);
366
275
  const absolutePath = isAbsolute(expandedPath) ? expandedPath : resolve(cwd, expandedPath);
@@ -368,133 +277,78 @@ async function buildClaudeFileContext(payload, opts, deps = {}) {
368
277
  const escapesCwd = relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath);
369
278
  if (!relativePath || escapesCwd) {
370
279
  logHookEvent(`file_context.skip reason=outside_cwd path=${JSON.stringify(filePath)} cwd=${JSON.stringify(cwd)}`);
371
- record({
372
- retrievalStatus: "skipped",
373
- deliveryStatus: "not_attempted",
374
- failureCode: "outside_cwd",
375
- failureStage: "path_validation"
376
- });
377
- return finish(continueResult$2());
280
+ return null;
378
281
  }
282
+ return {
283
+ absolutePath,
284
+ relativePath
285
+ };
286
+ }
287
+ async function buildClaudeFileContext(payload, opts, deps = {}) {
288
+ if (envTruthy$4(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$2();
289
+ const filePath = extractFilePath(payload);
290
+ if (!filePath) return continueResult$2();
291
+ const store = fileContextStore(opts, deps);
292
+ const runner = fileContextRunner(payload, store, deps);
293
+ if (!envNotDisabled$3(process.env.CODEMEM_FILE_CONTEXT || "1")) return store.finish(runner.skip({ reason: "file_context_disabled" }));
294
+ const path = fileContextPath(filePath, payload);
295
+ if (!path) return store.finish(runner.skip({ reason: "outside_cwd" }));
296
+ const { absolutePath, relativePath } = path;
379
297
  const minBytes = Number.parseInt(process.env.CODEMEM_FILE_CONTEXT_MIN_BYTES ?? `${FILE_GATE_MIN_BYTES}`, 10);
380
298
  const minBytesEffective = Number.isFinite(minBytes) && minBytes >= 0 ? minBytes : FILE_GATE_MIN_BYTES;
381
299
  const stat = (deps.statFile ?? statFile)(absolutePath);
382
300
  if (!stat) {
383
301
  logHookEvent(`file_context.skip reason=stat_failed path=${JSON.stringify(relativePath)}`);
384
- record({
385
- retrievalStatus: "skipped",
386
- deliveryStatus: "not_attempted",
387
- failureCode: "stat_failed",
388
- failureStage: "file_access",
302
+ return store.finish(runner.skip({
303
+ reason: "stat_failed",
389
304
  repositoryPaths: [relativePath]
390
- });
391
- return finish(continueResult$2());
305
+ }));
392
306
  }
393
307
  const bypassSizeGate = SMALL_FILE_BYPASS_PATTERNS.some((p) => p.test(relativePath));
394
308
  if (stat.sizeBytes < minBytesEffective && !bypassSizeGate) {
395
309
  logHookEvent(`file_context.skip reason=below_size_gate path=${JSON.stringify(relativePath)} size=${stat.sizeBytes} gate=${minBytesEffective}`);
396
- record({
397
- retrievalStatus: "skipped",
398
- deliveryStatus: "not_attempted",
399
- failureCode: "below_size_gate",
400
- failureStage: "size_gate",
401
- repositoryPaths: [relativePath]
402
- });
403
- return finish(continueResult$2());
404
- }
405
- const project = resolveProject$1(payload);
406
- const queryFn = deps.queryByFile ?? queryByFile;
407
- let rows = [];
408
- try {
409
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
410
- rows = deps.queryByFile ? queryFn(resolvedDbPath, relativePath, project, FETCH_LIMIT) : getStore().findByFile(relativePath, {
411
- limit: FETCH_LIMIT,
412
- ...project ? { project } : {}
413
- });
414
- } catch (err) {
415
- logHookEvent(`codemem claude-hook-file-context query failed: ${err instanceof Error ? err.message : String(err)}`);
416
- record({
417
- retrievalStatus: "failed",
418
- deliveryStatus: "not_attempted",
419
- failureCode: "query_failed",
420
- failureStage: "retrieval",
421
- project,
422
- filters: project ? { project } : void 0,
310
+ return store.finish(runner.skip({
311
+ reason: "below_size_gate",
423
312
  repositoryPaths: [relativePath]
424
- });
425
- return finish(continueResult$2());
426
- }
427
- if (rows.length === 0) {
428
- logHookEvent(`file_context.skip reason=no_observations path=${JSON.stringify(relativePath)} project=${JSON.stringify(project ?? "")}`);
429
- record({
430
- retrievalStatus: "no_results",
431
- deliveryStatus: "not_attempted",
432
- project,
433
- filters: project ? { project } : void 0,
434
- repositoryPaths: [relativePath]
435
- });
436
- return finish(continueResult$2());
437
- }
438
- const top = scoreAndDedupe(rows, relativePath, DISPLAY_LIMIT);
439
- if (top.length === 0) {
440
- logHookEvent(`file_context.skip reason=no_top_after_dedupe path=${JSON.stringify(relativePath)} candidates=${rows.length}`);
441
- record({
442
- retrievalStatus: "succeeded",
443
- deliveryStatus: "not_attempted",
444
- candidateIds: rows.map((row) => row.id),
445
- candidateCount: rows.length,
446
- selectedIds: [],
447
- failureCode: "no_top_after_dedupe",
448
- failureStage: "selection",
449
- project,
450
- filters: project ? { project } : void 0,
451
- repositoryPaths: [relativePath]
452
- });
453
- return finish(continueResult$2());
454
- }
455
- let staleness = null;
456
- if (stat.mtimeMs > 0) {
457
- const newestObservationMs = top.reduce((max, row) => {
458
- const epoch = Date.parse(row.created_at);
459
- return Number.isFinite(epoch) && epoch > max ? epoch : max;
460
- }, 0);
461
- if (newestObservationMs > 0 && stat.mtimeMs > newestObservationMs + MTIME_FRESH_TOLERANCE_MS) staleness = {
462
- fileMtimeMs: stat.mtimeMs,
463
- newestObservationMs
464
- };
465
- }
466
- record({
467
- retrievalStatus: "succeeded",
468
- deliveryStatus: "not_attempted",
469
- candidateIds: rows.map((row) => row.id),
470
- candidateCount: rows.length,
471
- selectedIds: top.map((row) => row.id),
472
- project,
473
- filters: project ? { project } : void 0,
474
- repositoryPaths: [relativePath]
475
- });
476
- let timeline;
477
- try {
478
- timeline = formatTimeline(top, relativePath, staleness);
479
- } catch {
480
- updateDelivery("failed");
481
- return finish(continueResult$2());
313
+ }));
482
314
  }
483
- logHookEvent(`file_context.ok path=${JSON.stringify(relativePath)} candidates=${rows.length} surfaced=${top.length} project=${JSON.stringify(project ?? "")} stale=${staleness ? "true" : "false"}`);
484
- updateDelivery("handed_off");
485
- return finish({ hookSpecificOutput: {
486
- hookEventName: "PreToolUse",
487
- permissionDecision: "allow",
488
- additionalContext: timeline
489
- } });
315
+ return store.finish(runner.run({
316
+ relativePath,
317
+ project: resolveProject$1(payload),
318
+ fileMtimeMs: stat.mtimeMs
319
+ }));
490
320
  }
491
321
  var claudeHookFileContextCmd = new Command("claude-hook-file-context").configureHelp(helpStyle).description("Return Claude PreToolUse:Read additionalContext from per-file observation timeline");
492
322
  addDbOption(claudeHookFileContextCmd);
323
+ claudeHookFileContextCmd.addOption(new Option("--transport-profile").hideHelp());
324
+ claudeHookFileContextCmd.addOption(new Option("--transport").hideHelp());
493
325
  var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts) => {
326
+ if (opts.transportProfile) {
327
+ process.stdout.write(`${JSON.stringify(FILE_CONTEXT_TRANSPORT_PROFILE)}\n`);
328
+ return;
329
+ }
494
330
  let raw = "";
495
- for await (const chunk of process.stdin) raw += String(chunk);
331
+ for await (const chunk of process.stdin) {
332
+ raw += String(chunk);
333
+ if (opts.transport && Buffer.byteLength(raw) > FILE_CONTEXT_TRANSPORT_MAX_BYTES) {
334
+ emitError$2({
335
+ error: "parse_error",
336
+ message: "transport payload too large"
337
+ });
338
+ process.exitCode = 1;
339
+ return;
340
+ }
341
+ }
496
342
  const trimmed = raw.trim();
497
343
  if (!trimmed) {
344
+ if (opts.transport) {
345
+ process.stdout.write(`${JSON.stringify({
346
+ error: "parse_error",
347
+ message: "transport request required"
348
+ })}\n`);
349
+ process.exitCode = 1;
350
+ return;
351
+ }
498
352
  emitJson$2(continueResult$2());
499
353
  return;
500
354
  }
@@ -518,8 +372,23 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
518
372
  process.exitCode = 1;
519
373
  return;
520
374
  }
521
- emitJson$2(await buildClaudeFileContext(payload, opts));
375
+ await emitFileContext(payload, opts);
522
376
  });
377
+ async function emitFileContext(payload, opts) {
378
+ try {
379
+ if (opts.transport) {
380
+ process.stdout.write(`${JSON.stringify(runClaudeFileContextTransport(payload, opts))}\n`);
381
+ return;
382
+ }
383
+ emitJson$2(await buildClaudeFileContext(payload, opts));
384
+ } catch {
385
+ emitError$2({
386
+ error: "file_context_failed",
387
+ message: "file context unavailable"
388
+ });
389
+ process.exitCode = 1;
390
+ }
391
+ }
523
392
  //#endregion
524
393
  //#region src/commands/hook-ingest-live-spool.ts
525
394
  function spoolLiveHookPayload(dir, payload, logPrefix) {
@@ -2525,63 +2394,994 @@ workspaceCmd.action((workspaceIdArg, opts) => {
2525
2394
  process.exitCode = 2;
2526
2395
  return;
2527
2396
  }
2528
- const result = runWorkspaceConfigCommand({
2529
- ...opts,
2530
- workspaceId
2531
- });
2532
- if (opts.json) {
2533
- console.log(JSON.stringify(result, null, 2));
2397
+ const result = runWorkspaceConfigCommand({
2398
+ ...opts,
2399
+ workspaceId
2400
+ });
2401
+ if (opts.json) {
2402
+ console.log(JSON.stringify(result, null, 2));
2403
+ return;
2404
+ }
2405
+ console.log(`Updated workspace config: ${result.config_path}`);
2406
+ console.log(`Updated keys: ${result.updated_keys.join(", ")}`);
2407
+ } catch (err) {
2408
+ if (opts.json) console.log(JSON.stringify({
2409
+ error: "config_error",
2410
+ message: err instanceof Error ? err.message : String(err)
2411
+ }));
2412
+ else console.error(err instanceof Error ? err.message : String(err));
2413
+ process.exitCode = 1;
2414
+ }
2415
+ });
2416
+ configCommand.addCommand(workspaceCmd);
2417
+ function formatWhereHuman(result) {
2418
+ const lines = [];
2419
+ const allEntries = [result.resolved, ...result.fallbackChain];
2420
+ const sourceOrder = {
2421
+ "cli-flag": 0,
2422
+ "env-codemem-config": 1,
2423
+ "env-runtime-root": 2,
2424
+ "env-workspace-id": 3,
2425
+ "legacy-global": 4
2426
+ };
2427
+ allEntries.sort((a, b) => (sourceOrder[a.source] ?? 99) - (sourceOrder[b.source] ?? 99));
2428
+ for (const entry of allEntries) {
2429
+ const marker = entry === result.resolved ? ">>>" : " ";
2430
+ const existsLabel = entry.exists ? "exists" : "missing";
2431
+ lines.push(`${marker} [${entry.source}] ${entry.path}`);
2432
+ lines.push(` ${entry.reason} (${existsLabel})`);
2433
+ }
2434
+ return lines.join("\n");
2435
+ }
2436
+ var whereCmd = new Command("where").configureHelp(helpStyle).description("show resolved config file path");
2437
+ addConfigOption(whereCmd);
2438
+ addJsonOption(whereCmd);
2439
+ whereCmd.action((opts) => {
2440
+ try {
2441
+ const result = resolveCodememConfigPath(opts.config, "read");
2442
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
2443
+ else console.log(formatWhereHuman(result));
2444
+ } catch (err) {
2445
+ if (opts.json) console.log(JSON.stringify({
2446
+ error: "config_error",
2447
+ message: err instanceof Error ? err.message : String(err)
2448
+ }));
2449
+ else console.error(err instanceof Error ? err.message : String(err));
2450
+ process.exitCode = 1;
2451
+ }
2452
+ });
2453
+ configCommand.addCommand(whereCmd);
2454
+ //#endregion
2455
+ //#region src/commands/coordinator-auth-schema.ts
2456
+ function failure$4(options, message, exitCode, errorCode = "auth_schema_upgrade_failed") {
2457
+ if (options.json) {
2458
+ emitJsonError(exitCode === 2 ? "usage_error" : errorCode, message, exitCode);
2459
+ return;
2460
+ }
2461
+ p.log.error(`Coordinator auth upgrade stopped: ${message}`);
2462
+ process.exitCode = exitCode;
2463
+ }
2464
+ function upgrade(options) {
2465
+ const path = resolveDbOpt(options);
2466
+ if (options.exportSql && path) {
2467
+ failure$4(options, "Use --export-sql without --db-path; export opens no database.", 2);
2468
+ return;
2469
+ }
2470
+ if (!options.exportSql && !path) {
2471
+ failure$4(options, "Select an existing coordinator database with --db-path. Pause auth writers, drain attempts and back up the database first.", 2);
2472
+ return;
2473
+ }
2474
+ try {
2475
+ if (options.exportSql) {
2476
+ const sql = `${AUTH_LINK_AUTHORITY_CUTOVER_SQL}\n`;
2477
+ process.stdout.write(options.json ? `${JSON.stringify({
2478
+ sql,
2479
+ applied: false
2480
+ })}\n` : sql);
2481
+ return;
2482
+ }
2483
+ if (!path) return;
2484
+ upgradeCoordinatorAuthSchemaFile(path);
2485
+ process.stdout.write(options.json ? `${JSON.stringify({ upgraded: true })}\n` : "Coordinator authentication schema upgraded.\n");
2486
+ } catch (error) {
2487
+ failure$4(options, `Upgrade aborted without committing changes. Check that attempts are drained and the legacy schema and provenance match the reviewed source. ${error instanceof Error ? error.message : "Coordinator auth upgrade failed."}`, 1);
2488
+ }
2489
+ }
2490
+ function buildCoordinatorAuthSchemaCommand() {
2491
+ const command = new Command("upgrade-auth-schema").configureHelp(helpStyle).description("Upgrade local coordinator auth or export reviewed D1 SQL").option("-e, --export-sql", "print D1 cutover SQL without opening a database");
2492
+ addDbOption(command);
2493
+ addJsonOption(command);
2494
+ command.action(upgrade);
2495
+ return command;
2496
+ }
2497
+ var accountPrerequisites = "Pause auth writers, drain pending attempts and back up the selected coordinator database first. This command does not pause writers or create a backup.";
2498
+ function hasDatabaseSelection(options) {
2499
+ return options.dbPath !== void 0 || options.db !== void 0;
2500
+ }
2501
+ function exportAccountSchema(options) {
2502
+ const statements = options.deviceUnlink ? ACCOUNT_UNLINK_MIGRATION_STATEMENTS : ACCOUNT_CONNECTION_MIGRATION_STATEMENTS;
2503
+ const text = `-- ${accountPrerequisites}\n-- Execute in ONE atomic D1 batch; do not split into autocommitted requests. Nothing has been applied.\n${statements.map((s) => `${s.trim().replace(/;$/u, "")};`).join("\n")}\n`;
2504
+ process.stdout.write(options.json ? `${JSON.stringify({
2505
+ statements,
2506
+ applied: false
2507
+ })}\n` : text);
2508
+ }
2509
+ async function upgradeAccount(options) {
2510
+ if (options.deviceUnlink && !options.exportSql) {
2511
+ failure$4(options, "Use --device-unlink only with --export-sql; local upgrades detect the installed schema.", 2, "account_schema_upgrade_failed");
2512
+ return;
2513
+ }
2514
+ const path = resolveDbOpt(options);
2515
+ const selected = hasDatabaseSelection(options);
2516
+ if (options.exportSql && selected || !options.exportSql && !path) {
2517
+ failure$4(options, options.exportSql ? "Use --export-sql without --db-path; export opens no database." : `Select an existing coordinator database with --db-path. ${accountPrerequisites}`, 2, "account_schema_upgrade_failed");
2518
+ return;
2519
+ }
2520
+ try {
2521
+ if (options.exportSql) {
2522
+ exportAccountSchema(options);
2523
+ return;
2524
+ }
2525
+ if (!path) return;
2526
+ if (!options.json) process.stderr.write(`${accountPrerequisites}\n`);
2527
+ await upgradeCoordinatorAccountSchemaFile(path);
2528
+ process.stdout.write(options.json ? `${JSON.stringify({ upgraded: true })}\n` : "Coordinator account schema upgraded.\n");
2529
+ } catch (error) {
2530
+ failure$4(options, `Account schema upgrade stopped. Inspect the selected database before retrying. ${error instanceof Error ? error.message : "Coordinator account schema upgrade failed."}`, 1, "account_schema_upgrade_failed");
2531
+ }
2532
+ }
2533
+ function buildCoordinatorAccountSchemaCommand() {
2534
+ const command = new Command("upgrade-account-schema").configureHelp(helpStyle).description("Explicitly install optional coordinator account management on an existing V2 database").option("--device-unlink", "export only the delta for the prior account-management schema (requires --export-sql)").option("-e, --export-sql", "export statement boundaries for one atomic D1 batch without opening a database").addHelpText("after", `\n${accountPrerequisites}\nD1 export does not apply SQL or verify remote execution limits.\n`);
2535
+ addDbOption(command);
2536
+ addJsonOption(command);
2537
+ command.action(upgradeAccount);
2538
+ return command;
2539
+ }
2540
+ //#endregion
2541
+ //#region src/commands/coordinator-enroll-owner.ts
2542
+ var CAPTURE_WARNING = "Stop OpenCode capture, the Viewer and the sync daemon before preparing or resuming. Keep them stopped until Verified coordinator Identity is shown. Any capture/history change during the browser ceremony can stop local adoption after the server commits, leaving DB and config separate and requiring operator review.";
2543
+ function showPrivateBrowserLink$1(privateUrl) {
2544
+ p.log.warn("Keep this browser link private. Open it to review the Google account and device, then return here to finish. No Team or Project access is added.", { output: process.stderr });
2545
+ p.log.message(privateUrl, { output: process.stderr });
2546
+ }
2547
+ function showAccountAdmissionLink(url, request) {
2548
+ if (!request) throw new OwnerProfileEnrollmentError("review_required");
2549
+ p.log.warn("Open this account admission page while signed in to the intended account. Upload the private capability-request.json file below unchanged, compare the local fingerprint, and review the device. Keep this command running and capture stopped; browser confirmation alone does not finish enrollment. No Team or Project access is added.", { output: process.stderr });
2550
+ p.log.message(`${url}\nPrivate request file: ${request.requestFile}\nDevice: ${request.deviceId}\nLocal fingerprint: ${request.fingerprint}`, { output: process.stderr });
2551
+ }
2552
+ function admissionOptions(options) {
2553
+ if (options.accountAdmission) return {
2554
+ admissionMode: "account",
2555
+ onBrowserStart: showAccountAdmissionLink
2556
+ };
2557
+ return {
2558
+ admissionMode: "operator",
2559
+ onBrowserStart: showPrivateBrowserLink$1
2560
+ };
2561
+ }
2562
+ function reportUsageError(options, message) {
2563
+ if (options.json) emitJsonError("usage_error", message, 2);
2564
+ else {
2565
+ p.log.error(message, { output: process.stderr });
2566
+ process.exitCode = 2;
2567
+ }
2568
+ return false;
2569
+ }
2570
+ function usage(options) {
2571
+ if (options.prepare === options.resume || options.prepare && (options.capability || options.reviewDigest || options.accountAdmission) || options.accountAdmission && options.capability !== void 0 || options.resume && !options.reviewDigest || options.resume && (options.json || !process.stderr.isTTY)) return reportUsageError(options, "Use --prepare first, then review its device, current Identity, capture counts and digest. Resume interactively with --resume --review-digest; use either --capability for operator admission or --account-admission for signed-in account admission, never both.");
2572
+ if (options.capability !== void 0 && !/^[A-Za-z0-9._-]{1,512}$/.test(options.capability)) return reportUsageError(options, "Supply --capability exactly as issued, without whitespace or unsupported characters, and at most 512 characters. The prepared attempt has not been consumed.");
2573
+ if (options.reviewDigest !== void 0 && !/^[0-9a-f]{64}$/.test(options.reviewDigest)) return reportUsageError(options, "Supply --review-digest as the exact 64-character lowercase hexadecimal digest from --prepare. Enrollment has not started.");
2574
+ return environmentUsage(options);
2575
+ }
2576
+ function environmentUsage(options) {
2577
+ const overrides = [
2578
+ "CODEMEM_SYNC_COORDINATOR_URL",
2579
+ "CODEMEM_SYNC_COORDINATOR_GROUP",
2580
+ "CODEMEM_SYNC_COORDINATOR_GROUPS",
2581
+ "CODEMEM_WORKSPACE_ID",
2582
+ "CODEMEM_ACTOR_ID",
2583
+ "CODEMEM_DEVICE_ID",
2584
+ "CODEMEM_ACTOR_DISPLAY_NAME"
2585
+ ].filter((name) => process.env[name] !== void 0);
2586
+ if (overrides.length) return reportUsageError(options, `Unset ${overrides.join(" and ")} before owner enrollment. Use --coordinator and the group argument, or the selected config; environment overrides cannot be adopted safely.`);
2587
+ if (process.env.CODEMEM_SYNC_KEY_STORE?.trim().toLowerCase() === "keychain") return reportUsageError(options, "CODEMEM_SYNC_KEY_STORE=keychain is not supported for owner enrollment. Keep signing keys unchanged; select a supported existing file-key profile.");
2588
+ return true;
2589
+ }
2590
+ function coordinatorOriginUsage(options, value) {
2591
+ if (typeof value !== "string") return reportUsageError(options, "Set sync_coordinator_url to a valid HTTPS origin string before owner enrollment.");
2592
+ try {
2593
+ const url = new URL(value);
2594
+ if (value.trim() === value && url.protocol === "https:" && !url.username && !url.password && (value === url.origin || value === `${url.origin}/`)) return true;
2595
+ } catch {}
2596
+ return reportUsageError(options, "Set --coordinator or sync_coordinator_url to an HTTPS origin without credentials, paths, query strings, fragments, or surrounding whitespace.");
2597
+ }
2598
+ function groupUsage(options, groupId) {
2599
+ if (groupId.length > 0 && groupId.length <= 256 && groupId.trim() === groupId && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(groupId)) return true;
2600
+ return reportUsageError(options, "Use a nonempty group ID of at most 256 characters, without surrounding whitespace or control characters.");
2601
+ }
2602
+ function pathEnvironmentUsage(options, selected) {
2603
+ for (const [name, path, flag] of [
2604
+ [
2605
+ "CODEMEM_CONFIG",
2606
+ selected.config,
2607
+ "--config"
2608
+ ],
2609
+ [
2610
+ "CODEMEM_DB",
2611
+ selected.db,
2612
+ "--db-path"
2613
+ ],
2614
+ [
2615
+ "CODEMEM_KEYS_DIR",
2616
+ selected.keys,
2617
+ "--keys-dir"
2618
+ ]
2619
+ ]) {
2620
+ const selectedPath = normalizeEnrollmentPath(name, path);
2621
+ const override = normalizeEnrollmentPath(name, name === "CODEMEM_KEYS_DIR" ? process.env[name]?.trim() || void 0 : process.env[name]);
2622
+ if (selectedPath !== void 0 && override !== void 0 && resolve(selectedPath) !== resolve(override)) return reportUsageError(options, `Unset ${name} or make it match ${flag} before owner enrollment. Conflicting profile paths cannot be adopted safely.`);
2623
+ }
2624
+ return true;
2625
+ }
2626
+ function normalizeEnrollmentPath(name, path) {
2627
+ if (path === void 0) return void 0;
2628
+ if (name === "CODEMEM_KEYS_DIR") return path;
2629
+ const value = name === "CODEMEM_CONFIG" ? path.trim() : path;
2630
+ return value ? expandUserPath(value) : void 0;
2631
+ }
2632
+ function resolveEnrollmentProfile(options) {
2633
+ const configPath = resolveCodememConfigPath(options.config, "write").resolved.path;
2634
+ const config = readCodememConfigFileAtPath(configPath);
2635
+ if (config.sync_coordinator_url !== void 0 && !coordinatorOriginUsage(options, config.sync_coordinator_url)) return null;
2636
+ const coordinatorUrl = options.coordinator ?? readCoordinatorSyncConfig(config).syncCoordinatorUrl;
2637
+ if (!coordinatorUrl) {
2638
+ reportUsageError(options, "Set --coordinator to your coordinator HTTPS URL, or set sync_coordinator_url in the selected config.");
2639
+ return null;
2640
+ }
2641
+ if (!coordinatorOriginUsage(options, coordinatorUrl)) return null;
2642
+ const dbPath = resolveDbPath(resolveDbOpt(options));
2643
+ const keysDir = options.keysDir ?? (process.env.CODEMEM_KEYS_DIR?.trim() || void 0) ?? dirname(resolveKeyPaths()[0]);
2644
+ if (!pathEnvironmentUsage(options, {
2645
+ config: configPath,
2646
+ db: dbPath,
2647
+ keys: keysDir
2648
+ })) return null;
2649
+ return {
2650
+ configPath,
2651
+ coordinatorUrl,
2652
+ dbPath,
2653
+ keysDir
2654
+ };
2655
+ }
2656
+ async function enroll(groupId, options) {
2657
+ if (!usage(options)) return;
2658
+ if (!groupUsage(options, groupId)) return;
2659
+ if (!pathEnvironmentUsage(options, {
2660
+ config: options.config,
2661
+ db: resolveDbOpt(options),
2662
+ keys: options.keysDir
2663
+ })) return;
2664
+ const controller = new AbortController();
2665
+ const stop = () => controller.abort();
2666
+ process.once("SIGINT", stop);
2667
+ process.once("SIGTERM", stop);
2668
+ try {
2669
+ if (!options.json) p.log.warn(CAPTURE_WARNING, { output: process.stderr });
2670
+ const profile = resolveEnrollmentProfile(options);
2671
+ if (!profile) return;
2672
+ showResult(await enrollOwnerProfile({
2673
+ ...profile,
2674
+ groupId,
2675
+ stateDir: options.stateDir ?? join(dirname(profile.dbPath), "owner-enrollment"),
2676
+ mode: options.prepare ? "prepare" : "resume",
2677
+ ...admissionOptions(options),
2678
+ capability: options.capability,
2679
+ reviewedDigest: options.reviewDigest,
2680
+ signal: controller.signal
2681
+ }), options);
2682
+ } catch (error) {
2683
+ const safe = error instanceof OwnerProfileEnrollmentError ? new OwnerProfileEnrollmentError(error.code) : new OwnerProfileEnrollmentError("review_required");
2684
+ const message = safe.message;
2685
+ if (options.json) emitJsonError(safe.code, message);
2686
+ else {
2687
+ p.log.error(message, { output: process.stderr });
2688
+ process.exitCode = 1;
2689
+ }
2690
+ } finally {
2691
+ process.removeListener("SIGINT", stop);
2692
+ process.removeListener("SIGTERM", stop);
2693
+ }
2694
+ }
2695
+ function showResult(result, options) {
2696
+ if (options.json) {
2697
+ process.stdout.write(`${JSON.stringify({
2698
+ ...result,
2699
+ warning: CAPTURE_WARNING
2700
+ })}\n`);
2701
+ return;
2702
+ }
2703
+ if (result.kind === "prepared") {
2704
+ p.log.message(`Current Identity: ${result.review.actorId}\nDevice: ${result.review.deviceId}\nPublic key: ${result.review.publicKey}\nCapture: ${result.review.captureId ?? "none"} (${result.review.captureAssociation})\nOwn local memories: ${result.review.localMemoryCount}\nForeign rows retained without new access: ${result.review.retainedForeignMemoryCount}\nReview digest: ${result.reviewDigest}`, { output: process.stderr });
2705
+ p.log.message(`Private request file for operator admission or upload during --resume --account-admission: ${result.requestFile}`, { output: process.stderr });
2706
+ p.log.warn("Only the current Identity/device assignment will change. Signing keys, memory authorship and history stay unchanged; no membership is inferred.", { output: process.stderr });
2707
+ return;
2708
+ }
2709
+ p.log.success(`Verified coordinator Identity: ${result.identityId}\nOwner binding: ${result.bindingId}`, { output: process.stderr });
2710
+ }
2711
+ function buildCoordinatorEnrollOwnerCommand() {
2712
+ const command = new Command("enroll-owner").configureHelp(helpStyle).description("Review and enroll this existing profile's device owner").argument("<group>", "group id").option("-u, --coordinator <url>", "coordinator HTTPS origin (defaults to config)").option("--keys-dir <path>", "existing signing-key directory").option("-s, --state-dir <path>", "private retained-request directory").option("--prepare", "save a reviewed request for operator admission").option("--resume", "resume the saved request or config-only adoption").option("--capability <token>", "admin-issued capability for this exact start").option("--account-admission", "use signed-in account admission with --resume only").option("-v, --review-digest <digest>", "explicitly approve the prepared profile review");
2713
+ addDbOption(command);
2714
+ addConfigOption(command);
2715
+ addJsonOption(command);
2716
+ command.action(enroll);
2717
+ return command;
2718
+ }
2719
+ //#endregion
2720
+ //#region src/commands/coordinator-grant-identity-group.ts
2721
+ function failure$3(options, message, exitCode = 1) {
2722
+ if (options.json) {
2723
+ emitJsonError("identity_group_grant_unavailable", message, exitCode);
2724
+ return;
2725
+ }
2726
+ p.log.error(message, { output: process.stderr });
2727
+ process.exitCode = exitCode;
2728
+ }
2729
+ function grantErrorMessage(error) {
2730
+ if (error instanceof RemoteCoordinatorRequestError) {
2731
+ if (error.status === 401 || error.status === 403) return "Coordinator refused the configured admin credential. Ask the operator to check it; do not pass credentials on the command line.";
2732
+ if (error.code === "invalid_grant_input") return "Coordinator rejected the reference. Use the stored controller attestation reference returned by device-owner review.";
2733
+ if (error.code === "grant_authority_unavailable") return "The stored review cannot authorize this grant. Ask the operator to check current enrollment, review revocation, and group status; do not edit the database.";
2734
+ if (error.status === 429 || error.code === "rate_limited") return "Coordinator rate limit reached. Wait before previewing and confirming again.";
2735
+ if (error.status === 404) return "This coordinator does not support the grant endpoint. Ask the operator to check its deployment.";
2736
+ }
2737
+ return "Transport grant could not be confirmed. Its outcome may be unknown; check coordinator setup and the stored review before retrying. No local profile was changed.";
2738
+ }
2739
+ function configuredTarget(options) {
2740
+ const config = options.config ? readCodememConfigFileAtPath(options.config) : readCodememConfigFile();
2741
+ const sync = readCoordinatorSyncConfig(config);
2742
+ return {
2743
+ remoteUrl: normalizeCoordinatorAccountLinkOrigin(sync.syncCoordinatorUrl),
2744
+ adminSecret: sync.syncCoordinatorAdminSecret,
2745
+ timeoutS: Math.max(1, sync.syncCoordinatorTimeoutS)
2746
+ };
2747
+ }
2748
+ async function confirmGrant(request, options) {
2749
+ const approved = await p.confirm({
2750
+ message: "Issue the reviewed Identity's transport enrollment grant?",
2751
+ initialValue: false,
2752
+ output: process.stderr
2753
+ });
2754
+ if (p.isCancel(approved) || approved !== true) {
2755
+ p.log.info("Nothing changed.", { output: process.stderr });
2756
+ return;
2757
+ }
2758
+ const current = configuredTarget(options);
2759
+ if (current.remoteUrl !== request.remoteUrl || current.adminSecret !== request.adminSecret) return failure$3(options, "Coordinator configuration changed. Nothing changed; preview again.");
2760
+ const result = await coordinatorGrantIdentityGroupAction(request);
2761
+ p.log.success(`Transport enrollment grant ${result.kind}: coordinator ${result.grant.coordinator_id}; Identity ${result.grant.identity_id}; group ${result.grant.group_id}. No Team/Project access or local profile changes.`, { output: process.stderr });
2762
+ }
2763
+ async function grantIdentityGroup(attestationId, options) {
2764
+ try {
2765
+ if (!isCoordinatorControllerAttestationReference(attestationId)) return failure$3(options, "Use a stored controller-v1: reference followed by exactly 64 lowercase hexadecimal characters.", 2);
2766
+ const target = configuredTarget(options);
2767
+ if (!target.remoteUrl) return failure$3(options, "Configure a coordinator HTTPS origin without credentials, path, query, or fragment (HTTP only on literal loopback). Nothing changed.", 2);
2768
+ if (!target.adminSecret) return failure$3(options, "Ask the coordinator operator to configure the existing admin credential. Nothing changed.");
2769
+ const preview = {
2770
+ state: "preview",
2771
+ coordinator_url: target.remoteUrl,
2772
+ attestation_id: attestationId,
2773
+ unchanged: true,
2774
+ eligibility_checked: false,
2775
+ message: "Explicit transport enrollment grant only, not Team/Project access. The server derives Identity and group from the stored review. No Google flow or local profile changes. Run in a terminal without --json to confirm."
2776
+ };
2777
+ if (options.json) {
2778
+ process.stdout.write(`${JSON.stringify(preview)}\n`);
2779
+ return;
2780
+ }
2781
+ p.log.info(`Coordinator: ${target.remoteUrl}\nController attestation: ${attestationId}\n${preview.message}\nThis is a local request preview, not a check of server eligibility.`, { output: process.stderr });
2782
+ if (!process.stdin.isTTY || !process.stderr.isTTY) return;
2783
+ await confirmGrant({
2784
+ ...target,
2785
+ remoteUrl: target.remoteUrl,
2786
+ attestationId
2787
+ }, options);
2788
+ } catch (error) {
2789
+ failure$3(options, grantErrorMessage(error));
2790
+ }
2791
+ }
2792
+ function buildCoordinatorGrantIdentityGroupCommand() {
2793
+ const command = new Command("grant-identity-group").configureHelp(helpStyle).description("Preview and confirm an explicit transport enrollment grant").argument("<attestation-id>", "stored controller attestation reference");
2794
+ addConfigOption(command);
2795
+ addDbOption(command);
2796
+ addJsonOption(command);
2797
+ command.action(grantIdentityGroup);
2798
+ return command;
2799
+ }
2800
+ //#endregion
2801
+ //#region src/commands/coordinator-link-account.ts
2802
+ function reportLinkFailure(error, coordinatorOrigin) {
2803
+ const message = error instanceof CoordinatorAccountLinkError ? error.message : "Account linking could not finish. Check your existing device setup and coordinator configuration, then try again.";
2804
+ p.log.error(message, { output: process.stderr });
2805
+ if (!(error instanceof CoordinatorAccountLinkError)) return;
2806
+ if (error.code !== "link_conflict") return;
2807
+ let guidance = "Replacing or removing coordinator account links is not supported. ";
2808
+ if (coordinatorOrigin) {
2809
+ const signInUrl = new URL("/auth/sign-in", coordinatorOrigin).href;
2810
+ guidance += `If this Google account already belongs to the intended coordinator Identity, sign in at ${signInUrl}. Otherwise, ask your coordinator operator.`;
2811
+ } else guidance += "Ask your coordinator operator.";
2812
+ p.log.message(guidance, { output: process.stderr });
2813
+ }
2814
+ function showPrivateBrowserLink(privateUrl) {
2815
+ p.log.warn("Keep this browser link private. Open it to review account linking, then return to the coordinator tab to finish.", { output: process.stderr });
2816
+ p.log.message(privateUrl, { output: process.stderr });
2817
+ }
2818
+ function requireInteractive(options) {
2819
+ if (options.json) {
2820
+ emitJsonError("interactive_flow_only", "Account linking is interactive. Run without --json and keep the browser link private.", 2);
2821
+ return false;
2822
+ }
2823
+ if (!process.stderr.isTTY) {
2824
+ p.log.error("Account linking requires a terminal. Run this command interactively and keep the browser link private.", { output: process.stderr });
2825
+ process.exitCode = 2;
2826
+ return false;
2827
+ }
2828
+ return true;
2829
+ }
2830
+ function readLinkConfiguration(options) {
2831
+ const config = options.config ? readCodememConfigFileAtPath(options.config) : readCodememConfigFile();
2832
+ const coordinatorUrl = normalizeCoordinatorAccountLinkOrigin(options.coordinator ?? readCoordinatorSyncConfig(config).syncCoordinatorUrl);
2833
+ if (!coordinatorUrl || options.loopbackHost !== "127.0.0.1" && options.loopbackHost !== "::1") return null;
2834
+ return {
2835
+ config,
2836
+ coordinatorUrl,
2837
+ loopbackHost: options.loopbackHost
2838
+ };
2839
+ }
2840
+ function readLinkProfile(options, selected) {
2841
+ try {
2842
+ const dbPath = resolveDbPath(resolveDbOpt(options));
2843
+ const before = statSync(dbPath, { bigint: true });
2844
+ const device = readAccountLinkDevice(dbPath);
2845
+ const after = statSync(dbPath, { bigint: true });
2846
+ const override = process.env.CODEMEM_DEVICE_ID?.trim() || void 0;
2847
+ if (!before.isFile() || before.dev !== after.dev || before.ino !== after.ino || override !== void 0 && override !== device.deviceId) throw new CoordinatorAccountLinkError("device_unavailable");
2848
+ let actor = selected.config.actor_id;
2849
+ if (Object.hasOwn(process.env, "CODEMEM_ACTOR_ID")) actor = process.env.CODEMEM_ACTOR_ID;
2850
+ const identityId = (typeof actor === "string" ? actor.trim() : "") || `local:${device.deviceId}`;
2851
+ return {
2852
+ dbPath,
2853
+ keysDir: process.env.CODEMEM_KEYS_DIR?.trim() || void 0,
2854
+ coordinatorUrl: selected.coordinatorUrl,
2855
+ expectedIdentityId: identityId,
2856
+ deviceId: device.deviceId,
2857
+ fingerprint: device.fingerprint,
2858
+ fileIdentity: {
2859
+ dev: after.dev.toString(),
2860
+ ino: after.ino.toString()
2861
+ }
2862
+ };
2863
+ } catch {
2864
+ throw new CoordinatorAccountLinkError("device_unavailable");
2865
+ }
2866
+ }
2867
+ function profileMatches(options, snapshot) {
2868
+ const selected = readLinkConfiguration(options);
2869
+ return selected !== null && JSON.stringify(readLinkProfile(options, selected)) === snapshot;
2870
+ }
2871
+ function eligibleStatus(remote, identityId) {
2872
+ if (remote.kind !== "status") {
2873
+ if (remote.kind === "unavailable" && remote.code === "coordinator_auth_schema_upgrade_required") throw new CoordinatorAccountLinkError("schema_upgrade_required");
2874
+ throw new CoordinatorAccountLinkError("invalid_response");
2875
+ }
2876
+ const status = remote.status;
2877
+ if (status.state === "conflict" || status.state === "revoked") throw new CoordinatorAccountLinkError("link_conflict");
2878
+ if (status.state === "review_required") throw new CoordinatorAccountLinkError("review_required");
2879
+ if (status.identityId !== identityId) throw new CoordinatorAccountLinkError("identity_mismatch");
2880
+ return status;
2881
+ }
2882
+ async function linkAccount(group, options) {
2883
+ if (!requireInteractive(options)) return;
2884
+ const controller = new AbortController();
2885
+ const stop = () => controller.abort();
2886
+ process.once("SIGINT", stop);
2887
+ process.once("SIGTERM", stop);
2888
+ let coordinatorOrigin = null;
2889
+ try {
2890
+ const selected = readLinkConfiguration(options);
2891
+ if (!selected) {
2892
+ p.log.error("Set --coordinator to your coordinator HTTPS origin and use --loopback-host 127.0.0.1 or ::1.", { output: process.stderr });
2893
+ process.exitCode = 2;
2894
+ return;
2895
+ }
2896
+ const { coordinatorUrl } = selected;
2897
+ coordinatorOrigin = coordinatorUrl;
2898
+ const profile = readLinkProfile(options, selected);
2899
+ const status = eligibleStatus(await readCoordinatorAccountLinkStatus({
2900
+ ...profile,
2901
+ coordinatorUrl,
2902
+ identityId: profile.expectedIdentityId,
2903
+ groupId: group,
2904
+ signal: controller.signal
2905
+ }), profile.expectedIdentityId);
2906
+ if (status.state === "linked") {
2907
+ p.log.success("This Identity is already linked. No account-link request was started.", { output: process.stderr });
2908
+ return;
2909
+ }
2910
+ const snapshot = JSON.stringify(profile);
2911
+ const result = await linkCoordinatorAccount({
2912
+ ...profile,
2913
+ groupId: group,
2914
+ coordinatorUrl,
2915
+ expectedAuthority: status.authority,
2916
+ expectedCoordinatorId: status.coordinatorId,
2917
+ assertCurrentContext: () => profileMatches(options, snapshot),
2918
+ loopbackHost: selected.loopbackHost,
2919
+ signal: controller.signal,
2920
+ onBrowserStart: showPrivateBrowserLink
2921
+ });
2922
+ p.log.success(`Coordinator account linked to coordinator Identity ${result.identityId}. Your local Identity has not changed.`, { output: process.stderr });
2923
+ } catch (error) {
2924
+ reportLinkFailure(error, coordinatorOrigin);
2925
+ process.exitCode = 1;
2926
+ } finally {
2927
+ process.removeListener("SIGINT", stop);
2928
+ process.removeListener("SIGTERM", stop);
2929
+ }
2930
+ }
2931
+ function buildCoordinatorLinkAccountCommand() {
2932
+ const command = new Command("link-account").configureHelp(helpStyle).description("Link a coordinator account to your existing Identity").argument("<group>", "group id").option("-u, --coordinator <url>", "coordinator HTTPS origin (defaults to saved config)").option("-l, --loopback-host <host>", "literal loopback host", "127.0.0.1");
2933
+ addDbOption(command);
2934
+ addConfigOption(command);
2935
+ addJsonOption(command);
2936
+ command.action(linkAccount);
2937
+ return command;
2938
+ }
2939
+ //#endregion
2940
+ //#region src/commands/coordinator-qualify-owner.ts
2941
+ var isAttestation = (value) => typeof value === "string" && /^controller-v1:[a-f0-9]{64}$/u.test(value);
2942
+ var isHash$1 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
2943
+ function isRef(value) {
2944
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
2945
+ }
2946
+ function setup$1(options) {
2947
+ const config = options.config ? readCodememConfigFileAtPath(options.config) : readCodememConfigFile();
2948
+ const sync = readCoordinatorSyncConfig(config);
2949
+ const remoteUrl = normalizeCoordinatorAccountLinkOrigin(sync.syncCoordinatorUrl);
2950
+ if (!remoteUrl || !sync.syncCoordinatorAdminSecret) throw new Error("configuration_unavailable");
2951
+ return {
2952
+ remoteUrl,
2953
+ adminSecret: sync.syncCoordinatorAdminSecret,
2954
+ timeoutS: Math.max(1, sync.syncCoordinatorTimeoutS)
2955
+ };
2956
+ }
2957
+ function parsePreview$1(reply, attestationId) {
2958
+ if (reply.state !== "ready" || reply.attestation_id !== attestationId || !isHash$1(reply.evidence_digest) || !isHash$1(reply.fingerprint) || ![
2959
+ "coordinator_id",
2960
+ "review_receipt_id",
2961
+ "link_id",
2962
+ "claim_id",
2963
+ "identity_id",
2964
+ "group_id",
2965
+ "device_id"
2966
+ ].every((key) => isRef(reply[key]))) throw new Error("invalid_preview");
2967
+ return {
2968
+ state: "ready",
2969
+ attestation_id: attestationId,
2970
+ evidence_digest: reply.evidence_digest,
2971
+ coordinator_id: reply.coordinator_id,
2972
+ review_receipt_id: reply.review_receipt_id,
2973
+ link_id: reply.link_id,
2974
+ claim_id: reply.claim_id,
2975
+ identity_id: reply.identity_id,
2976
+ group_id: reply.group_id,
2977
+ device_id: reply.device_id,
2978
+ fingerprint: reply.fingerprint
2979
+ };
2980
+ }
2981
+ var QUALIFICATION_DENIAL_CODES = [
2982
+ "invalid_input",
2983
+ "preview_changed",
2984
+ "qualification_conflict",
2985
+ "unavailable"
2986
+ ];
2987
+ function failure$2(options, error, message, exitCode = 1) {
2988
+ if (options.json) return emitJsonError(error, message, exitCode);
2989
+ p.log.error(message, { output: process.stderr });
2990
+ process.exitCode = exitCode;
2991
+ }
2992
+ function sameConfiguration$1(options, request) {
2993
+ try {
2994
+ const current = setup$1(options);
2995
+ return current.remoteUrl === request.remoteUrl && current.adminSecret === request.adminSecret;
2996
+ } catch {
2997
+ return false;
2998
+ }
2999
+ }
3000
+ async function confirmQualification(options, request, preview, stage) {
3001
+ const approved = await p.confirm({
3002
+ message: "Allow this linked account to enroll new devices under this Identity?",
3003
+ initialValue: false,
3004
+ output: process.stderr
3005
+ });
3006
+ if (p.isCancel(approved) || !approved) {
3007
+ p.log.info("Nothing changed.", { output: process.stderr });
3008
+ return;
3009
+ }
3010
+ if (!sameConfiguration$1(options, request)) return failure$2(options, "qualification_configuration_changed", "Operator configuration changed. Preview again before confirming.");
3011
+ stage.onDispatch();
3012
+ const result = await coordinatorQualifyOwnerAction({
3013
+ ...request,
3014
+ confirmEvidenceDigest: preview.evidence_digest
3015
+ });
3016
+ if (result.state !== "created" && result.state !== "existing" || !isRef(result.qualification_id) || result.attestation_id !== request.attestationId || result.coordinator_id !== preview.coordinator_id || result.evidence_digest !== preview.evidence_digest) throw new Error("invalid_commit_response");
3017
+ p.log.success("Linked account qualified for device enrollment. Team and Project access are unchanged.", { output: process.stderr });
3018
+ }
3019
+ async function qualifyOwner(attestationId, options) {
3020
+ let writeAttempted = false;
3021
+ try {
3022
+ if (!isAttestation(attestationId)) return failure$2(options, "qualification_invalid_input", "Use the exact controller-v1 reference from a real device-owner review.", 2);
3023
+ const request = {
3024
+ ...setup$1(options),
3025
+ attestationId
3026
+ };
3027
+ const preview = parsePreview$1(await coordinatorQualifyOwnerAction(request), attestationId);
3028
+ if (options.json) {
3029
+ process.stdout.write(`${JSON.stringify({
3030
+ preview,
3031
+ network_request: true,
3032
+ unchanged: true
3033
+ })}\n`);
3034
+ return;
3035
+ }
3036
+ p.log.info(`Network preview from ${preview.coordinator_id}\nIdentity: ${preview.identity_id}\nLinked account reference: ${preview.link_id}\nDevice: ${preview.device_id}; fingerprint: ${preview.fingerprint}\nReview: ${preview.attestation_id}\nReceipt: ${preview.review_receipt_id}\nClaim: ${preview.claim_id}; group: ${preview.group_id}`, { output: process.stderr });
3037
+ p.log.warn("Independently verify the real operator review. Qualification permits new devices under this Identity; an independently trusted transport-group grant is still separate. This does not create a Team, issue a grant, or rewrite a device claim.", { output: process.stderr });
3038
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
3039
+ p.log.info("Preview only; no qualification issued. Use a terminal without --json to confirm.", { output: process.stderr });
3040
+ return;
3041
+ }
3042
+ await confirmQualification(options, request, preview, { onDispatch: () => {
3043
+ writeAttempted = true;
3044
+ } });
3045
+ } catch (error) {
3046
+ if (error instanceof RemoteCoordinatorRequestError && error.status === 409 && QUALIFICATION_DENIAL_CODES.includes(error.code)) return failure$2(options, "qualification_rejected", "Qualification denied: evidence is invalid, changed, was revoked, conflicts, or is unavailable. Preview again; no automatic retry.");
3047
+ let message = "Qualification server unavailable or preview invalid. Ask the coordinator operator to check the configured Worker's browser authentication and owner-enrollment settings, required migrations 0035 and 0036, and the saved trusted HTTPS coordinator origin and admin credential.";
3048
+ if (writeAttempted) message = "Qualification outcome is unknown. It may have committed; do not assume rollback or retry automatically. Ask the coordinator operator to inspect the outcome.";
3049
+ failure$2(options, writeAttempted ? "qualification_outcome_unknown" : "qualification_unavailable", message);
3050
+ }
3051
+ }
3052
+ function buildCoordinatorQualifyOwnerCommand() {
3053
+ const command = new Command("qualify-owner").configureHelp(helpStyle).description("Preview and confirm linked-account device enrollment permission").argument("<attestation-id>", "controller-v1 reference from a real operator review");
3054
+ addConfigOption(command);
3055
+ addJsonOption(command);
3056
+ command.action(qualifyOwner);
3057
+ return command;
3058
+ }
3059
+ //#endregion
3060
+ //#region src/commands/coordinator-review-device-owner.ts
3061
+ var REVIEW_REASONS = [
3062
+ "group_unavailable",
3063
+ "enrollment_unavailable",
3064
+ "key_mismatch",
3065
+ "enrollment_identity_mismatch",
3066
+ "invite_identity_mismatch",
3067
+ "invite_evidence_invalid"
3068
+ ];
3069
+ function validId$1(value) {
3070
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
3071
+ }
3072
+ function isHash(value) {
3073
+ return typeof value === "string" && value.length === 64 && /^[a-f0-9]{64}$/.test(value);
3074
+ }
3075
+ function reviewErrorMessage(error) {
3076
+ if (error instanceof RemoteCoordinatorRequestError) {
3077
+ if (error.code === "review_stale") return "Evidence changed since the preview. Nothing changed; run the review again.";
3078
+ if (error.code === "already_reviewed_or_needs_review") return "This device already has a reviewed owner, or needs a separate review. Nothing changed.";
3079
+ if (error.code === "needs_review") return "Coordinator ownership evidence needs review. Nothing changed; ask the coordinator operator.";
3080
+ }
3081
+ return "Device owner review could not finish. Check the existing device, admin credential, and coordinator setup; preview again before retrying.";
3082
+ }
3083
+ function failure$1(options, message, exitCode = 1) {
3084
+ if (options.json) {
3085
+ emitJsonError("owner_review_unavailable", message, exitCode);
3086
+ return;
3087
+ }
3088
+ p.log.error(message, { output: process.stderr });
3089
+ process.exitCode = exitCode;
3090
+ }
3091
+ function localOptions(options) {
3092
+ const config = options.config ? readCodememConfigFileAtPath(options.config) : readCodememConfigFile();
3093
+ const sync = readCoordinatorSyncConfig(config);
3094
+ let actorId;
3095
+ let identitySource = "config";
3096
+ if (Object.hasOwn(process.env, "CODEMEM_ACTOR_ID")) {
3097
+ actorId = process.env.CODEMEM_ACTOR_ID?.trim();
3098
+ identitySource = "env";
3099
+ } else if (typeof config.actor_id === "string") actorId = config.actor_id.trim();
3100
+ return {
3101
+ local: {
3102
+ dbPath: resolveDbPath(resolveDbOpt(options)),
3103
+ actorId,
3104
+ identitySource,
3105
+ deviceIdOverride: process.env.CODEMEM_DEVICE_ID?.trim()
3106
+ },
3107
+ remoteUrl: normalizeCoordinatorAccountLinkOrigin(options.coordinator ?? sync.syncCoordinatorUrl),
3108
+ adminSecret: sync.syncCoordinatorAdminSecret,
3109
+ timeoutS: Math.max(1, sync.syncCoordinatorTimeoutS)
3110
+ };
3111
+ }
3112
+ function displayLocal(evidence) {
3113
+ const device = evidence.device;
3114
+ return {
3115
+ ...evidence,
3116
+ device: device ? {
3117
+ deviceId: device.deviceId,
3118
+ fingerprint: device.fingerprint,
3119
+ label: device.label
3120
+ } : null
3121
+ };
3122
+ }
3123
+ function showLocal(evidence) {
3124
+ const count = (value) => value === null ? "unknown" : String(value);
3125
+ const device = evidence.device;
3126
+ const identity = evidence.identity;
3127
+ p.log.message([
3128
+ `Device: ${device?.label ?? device?.deviceId ?? "unavailable"}`,
3129
+ `Device ID: ${device?.deviceId ?? "unknown"}; fingerprint: ${device?.fingerprint ?? "unknown"}`,
3130
+ `Identity: ${identity?.label ?? identity?.identityId ?? "unavailable"} (${identity?.source ?? "unknown"})`,
3131
+ `Identity ID: ${identity?.identityId ?? "unknown"}`,
3132
+ `Memory authors: current ${count(evidence.memoryCounts.current)}, other ${count(evidence.memoryCounts.others)}, unknown ${count(evidence.memoryCounts.unknown)} — unchanged`,
3133
+ `Active Teams: ${count(evidence.teamCount)}; direct Projects: ${count(evidence.projectCount)} — unchanged`,
3134
+ "This review does not move memories or change access. Google account linking is a separate step."
3135
+ ].join("\n"), { output: process.stderr });
3136
+ }
3137
+ function validInviteCount(reply, invites) {
3138
+ return invites.length <= 10 && Number.isSafeInteger(reply.reviewed_invite_count) && reply.reviewed_invite_count >= invites.length;
3139
+ }
3140
+ function validInviteRef(invite) {
3141
+ return !!invite && validId$1(invite.invite_id) && (invite.kind === "team_member" || invite.kind === "add_device");
3142
+ }
3143
+ function parsePreview(payload, request) {
3144
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid_preview");
3145
+ const reply = payload;
3146
+ const enrollment = reply.enrollment;
3147
+ const invites = reply.reviewed_invites;
3148
+ if (reply.state !== "ready" && reply.state !== "needs_review" || !validId$1(reply.coordinator_id) || !Array.isArray(reply.reasons) || !reply.reasons.every((reason) => typeof reason === "string" && REVIEW_REASONS.includes(reason)) || !Array.isArray(invites) || !validInviteCount(reply, invites) || !invites.every(validInviteRef)) throw new Error("invalid_preview");
3149
+ if (enrollment !== null && (!enrollment || enrollment.device_id !== request.deviceId || !isHash(enrollment.fingerprint) || !["none", "matches"].includes(enrollment.identity_label))) throw new Error("invalid_preview");
3150
+ if (reply.state === "ready" && (!enrollment || enrollment.device_id !== request.deviceId || enrollment.fingerprint !== request.fingerprint || !["none", "matches"].includes(enrollment.identity_label) || !isHash(reply.evidence_digest) || reply.reasons.length)) throw new Error("invalid_preview");
3151
+ return {
3152
+ state: reply.state,
3153
+ coordinator_id: reply.coordinator_id,
3154
+ reasons: reply.reasons,
3155
+ evidence_digest: reply.state === "ready" ? reply.evidence_digest : void 0,
3156
+ enrollment: enrollment ? {
3157
+ device_id: enrollment.device_id,
3158
+ fingerprint: enrollment.fingerprint,
3159
+ display_name: null,
3160
+ identity_label: enrollment.identity_label
3161
+ } : null,
3162
+ reviewed_invites: invites.map((invite) => ({
3163
+ invite_id: invite.invite_id,
3164
+ kind: invite.kind
3165
+ })),
3166
+ reviewed_invite_count: reply.reviewed_invite_count
3167
+ };
3168
+ }
3169
+ function sameLocalOwner(before, after) {
3170
+ return after.state === "ready" && before.ownershipRecords.actorPresent === after.ownershipRecords.actorPresent && before.ownershipRecords.deviceAssignmentPresent === after.ownershipRecords.deviceAssignmentPresent && before.device?.deviceId === after.device?.deviceId && before.device?.publicKey === after.device?.publicKey && before.device?.fingerprint === after.device?.fingerprint && before.identity?.identityId === after.identity?.identityId && before.identity?.source === after.identity?.source;
3171
+ }
3172
+ async function confirmReview(group, options, evidence, request, preview) {
3173
+ if (!process.stdin.isTTY || !process.stderr.isTTY) return failure$1(options, "Preview only. Run in a terminal without --json to confirm this review.", 2);
3174
+ const approved = await p.confirm({
3175
+ message: "Review this device owner?",
3176
+ initialValue: false,
3177
+ output: process.stderr
3178
+ });
3179
+ if (p.isCancel(approved) || !approved) {
3180
+ p.log.info("Nothing changed.", { output: process.stderr });
3181
+ return;
3182
+ }
3183
+ const current = localOptions(options);
3184
+ if (current.remoteUrl !== request.remoteUrl || !sameLocalOwner(evidence, readCoordinatorOwnerReviewLocalEvidence(current.local))) return failure$1(options, "Your device or Identity changed. Nothing changed on the coordinator; preview again.");
3185
+ const result = await coordinatorAuthControllerReviewAction({
3186
+ ...request,
3187
+ confirmEvidenceDigest: preview.evidence_digest
3188
+ });
3189
+ if (result.state !== "created" && result.state !== "existing" || typeof result.attestation_id !== "string" || result.attestation_id.length !== 78 || !/^controller-v1:[a-f0-9]{64}$/.test(result.attestation_id) || result.coordinator_id !== preview.coordinator_id || result.group_id !== group || result.device_id !== request.deviceId || result.identity_id !== request.identityId || result.fingerprint !== request.fingerprint || typeof result.created_at !== "string") throw new Error("invalid_commit_response");
3190
+ p.log.success("Device owner reviewed. Memories and access are unchanged.", { output: process.stderr });
3191
+ p.log.info(`Controller attestation reference: ${result.attestation_id}\nKeep this noncredential reference for a separate, explicit operator-issued transport group grant. This review does not issue that grant or grant Team/Project access.`, { output: process.stderr });
3192
+ if (showTargetedLinkFollowUp(options)) return;
3193
+ showLinkFollowUp(group);
3194
+ }
3195
+ function showLinkFollowUp(group) {
3196
+ if (group.includes("\\")) {
3197
+ p.log.info(`Next, run codemem coordinator link-account with group ID ${JSON.stringify(group)}. Quote the ID for your shell.`, { output: process.stderr });
3198
+ return;
3199
+ }
3200
+ const quotedGroup = `'${group.replaceAll("'", "'\\''")}'`;
3201
+ p.log.info(`Next, link your Google account separately: codemem coordinator link-account ${quotedGroup}`, { output: process.stderr });
3202
+ }
3203
+ function showTargetedLinkFollowUp(options) {
3204
+ if (!options.coordinator && !options.config && !resolveDbOpt(options)) return false;
3205
+ p.log.info("Next, link your Google account separately with codemem coordinator link-account for this group. Reuse the same --coordinator, --config, and --db-path options from this review.", { output: process.stderr });
3206
+ return true;
3207
+ }
3208
+ function showLocalStop(evidence, options) {
3209
+ if (options.json) {
3210
+ process.stdout.write(`${JSON.stringify({
3211
+ ...displayLocal(evidence),
3212
+ unchanged: true,
3213
+ error: "needs_review",
3214
+ message: "Local device ownership needs review. Nothing changed."
3215
+ })}\n`);
3216
+ return;
3217
+ }
3218
+ p.log.warn(`Review stopped: ${evidence.reasons.join(", ")}. Nothing changed.`, { output: process.stderr });
3219
+ }
3220
+ function emitReviewPreview(evidence, preview) {
3221
+ let stopped = {};
3222
+ if (preview.state !== "ready") {
3223
+ stopped = {
3224
+ error: "needs_review",
3225
+ message: "Coordinator device ownership needs review. Nothing changed."
3226
+ };
3227
+ process.exitCode = 1;
3228
+ }
3229
+ process.stdout.write(`${JSON.stringify({
3230
+ local: displayLocal(evidence),
3231
+ review: preview,
3232
+ unchanged: true,
3233
+ ...stopped
3234
+ })}\n`);
3235
+ }
3236
+ async function reviewDeviceOwner(group, options) {
3237
+ try {
3238
+ if (!validId$1(group)) return failure$1(options, "Use a valid coordinator group ID.", 2);
3239
+ const setup = localOptions(options);
3240
+ const evidence = readCoordinatorOwnerReviewLocalEvidence(setup.local);
3241
+ if (!options.json) showLocal(evidence);
3242
+ if (evidence.state !== "ready" || !evidence.device || !evidence.identity) {
3243
+ showLocalStop(evidence, options);
3244
+ process.exitCode = 1;
3245
+ return;
3246
+ }
3247
+ if (!setup.adminSecret) return failure$1(options, "Ask the coordinator operator to configure the existing admin credential. Nothing changed.");
3248
+ if (!setup.remoteUrl) return failure$1(options, "Set --coordinator to a coordinator HTTPS origin (HTTP is allowed only on literal loopback).", 2);
3249
+ const request = {
3250
+ remoteUrl: setup.remoteUrl,
3251
+ adminSecret: setup.adminSecret,
3252
+ timeoutS: setup.timeoutS,
3253
+ groupId: group,
3254
+ deviceId: evidence.device.deviceId,
3255
+ identityId: evidence.identity.identityId,
3256
+ fingerprint: evidence.device.fingerprint
3257
+ };
3258
+ const preview = parsePreview(await coordinatorAuthControllerReviewAction(request), request);
3259
+ if (options.json) {
3260
+ emitReviewPreview(evidence, preview);
3261
+ return;
3262
+ }
3263
+ if (preview.state !== "ready") return failure$1(options, `Coordinator review stopped: ${preview.reasons.join(", ")}. Nothing changed.`);
3264
+ p.log.info(`Coordinator: ${preview.coordinator_id}; reviewed invitations: ${preview.reviewed_invite_count}`, { output: process.stderr });
3265
+ await confirmReview(group, options, evidence, request, preview);
3266
+ } catch (error) {
3267
+ failure$1(options, reviewErrorMessage(error));
3268
+ }
3269
+ }
3270
+ function buildCoordinatorReviewDeviceOwnerCommand() {
3271
+ const command = new Command("review-device-owner").configureHelp(helpStyle).description("Preview and confirm the owner of your existing device").argument("<group>", "group id").option("-u, --coordinator <url>", "coordinator HTTPS origin (defaults to saved config)");
3272
+ addDbOption(command);
3273
+ addConfigOption(command);
3274
+ addJsonOption(command);
3275
+ command.action(reviewDeviceOwner);
3276
+ return command;
3277
+ }
3278
+ //#endregion
3279
+ //#region src/commands/coordinator-revoke-device.ts
3280
+ function validId(value) {
3281
+ return value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
3282
+ }
3283
+ function setup(options) {
3284
+ const config = options.config ? readCodememConfigFileAtPath(options.config) : readCodememConfigFile();
3285
+ const remoteUrl = normalizeCoordinatorAccountLinkOrigin(config.sync_coordinator_url);
3286
+ const adminSecret = config.sync_coordinator_admin_secret;
3287
+ const timeoutS = config.sync_coordinator_timeout_s ?? 3;
3288
+ if (!remoteUrl || typeof adminSecret !== "string" || !adminSecret.trim() || /[\r\n\0]/u.test(adminSecret) || typeof timeoutS !== "number" || !Number.isFinite(timeoutS) || timeoutS < 1 || timeoutS > 3600) throw new Error("revocation_configuration_unavailable");
3289
+ return {
3290
+ remoteUrl,
3291
+ adminSecret,
3292
+ timeoutS
3293
+ };
3294
+ }
3295
+ function sameConfiguration(options, request) {
3296
+ try {
3297
+ const current = setup(options);
3298
+ return current.remoteUrl === request.remoteUrl && current.adminSecret === request.adminSecret && current.timeoutS === request.timeoutS;
3299
+ } catch {
3300
+ return false;
3301
+ }
3302
+ }
3303
+ function failure(options, code, message, exitCode = 1) {
3304
+ if (options.json) {
3305
+ emitJsonError(code, message, exitCode);
3306
+ return;
3307
+ }
3308
+ p.log.error(message, { output: process.stderr });
3309
+ process.exitCode = exitCode;
3310
+ }
3311
+ var IMPACT = "Across the whole coordinator database: revoke this device ID with ANY key and this canonical key under ANY device ID, in ALL groups. The selected group supplies evidence only. Another device continues only when BOTH its device ID and key differ. No immediate offline enforcement, manual/direct-peer trust revocation, or erasure of remote copies. Account links, sessions, grants, memberships and stored memories are not rewritten.";
3312
+ function receiptLabel(value) {
3313
+ return JSON.stringify(value).replace(/[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/gu, (character) => `\\u{${(character.codePointAt(0) ?? 0).toString(16)}}`);
3314
+ }
3315
+ function receiptJson(result) {
3316
+ return JSON.stringify(result).replace(/[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/gu, (character) => character.split("").map((unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, "0")}`).join(""));
3317
+ }
3318
+ async function confirmRevocation(options, request, preview, onDispatch) {
3319
+ const approved = await p.confirm({
3320
+ message: "Revoke BOTH subjects across the whole coordinator database?",
3321
+ initialValue: false,
3322
+ output: process.stderr
3323
+ });
3324
+ if (p.isCancel(approved) || approved !== true) {
3325
+ p.log.info("No revocation requested.", { output: process.stderr });
3326
+ return;
3327
+ }
3328
+ if (!sameConfiguration(options, request)) return failure(options, "revocation_configuration_changed", "Operator configuration changed or is unavailable. No revocation requested; preview again.");
3329
+ const result = await coordinatorRevokeDeviceAction({
3330
+ ...request,
3331
+ expectedDigest: preview.expected_digest,
3332
+ expectedContext: {
3333
+ coordinatorId: preview.coordinator_id,
3334
+ keyId: preview.key_id
3335
+ },
3336
+ onDispatch
3337
+ });
3338
+ if (result.state !== "revoked") throw new Error("revocation_response_invalid");
3339
+ process.stdout.write(`${receiptJson(result)}\n`);
3340
+ p.log.success(`Coordinator ${result.coordinator_id}: both whole-database subjects revoked.`, { output: process.stderr });
3341
+ for (const row of result.revocations) p.log.info(`${row.subject_kind}: ${row.subject_value}; retained reference: ${receiptLabel(row.revocation_id)}; created: ${receiptLabel(row.created_at)}`, { output: process.stderr });
3342
+ }
3343
+ async function revokeDevice(groupId, deviceId, options) {
3344
+ let writeDispatched = false;
3345
+ try {
3346
+ if (!validId(groupId) || !validId(deviceId)) return failure(options, "revocation_invalid_input", "Use exact group and device IDs without whitespace or control characters (maximum 256 characters).", 2);
3347
+ const request = {
3348
+ ...setup(options),
3349
+ groupId,
3350
+ deviceId
3351
+ };
3352
+ const preview = await coordinatorRevokeDeviceAction(request);
3353
+ if (preview.state !== "ready") throw new Error("revocation_response_invalid");
3354
+ if (options.json) {
3355
+ process.stdout.write(`${JSON.stringify({
3356
+ preview,
3357
+ network_request: true,
3358
+ unchanged: true,
3359
+ message: IMPACT
3360
+ })}\n`);
3361
+ return;
3362
+ }
3363
+ p.log.info(`Coordinator: ${preview.coordinator_id}\nEvidence group: ${preview.group_id}\nGlobal device ID: ${preview.device_id}\nCanonical key ID: ${preview.key_id}\nScope: ${preview.scope}`, { output: process.stderr });
3364
+ p.log.warn(IMPACT, { output: process.stderr });
3365
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
3366
+ p.log.info("Network preview only; no revocation requested. Use a terminal without --json to confirm.", { output: process.stderr });
2534
3367
  return;
2535
3368
  }
2536
- console.log(`Updated workspace config: ${result.config_path}`);
2537
- console.log(`Updated keys: ${result.updated_keys.join(", ")}`);
2538
- } catch (err) {
2539
- if (opts.json) console.log(JSON.stringify({
2540
- error: "config_error",
2541
- message: err instanceof Error ? err.message : String(err)
2542
- }));
2543
- else console.error(err instanceof Error ? err.message : String(err));
2544
- process.exitCode = 1;
2545
- }
2546
- });
2547
- configCommand.addCommand(workspaceCmd);
2548
- function formatWhereHuman(result) {
2549
- const lines = [];
2550
- const allEntries = [result.resolved, ...result.fallbackChain];
2551
- const sourceOrder = {
2552
- "cli-flag": 0,
2553
- "env-codemem-config": 1,
2554
- "env-runtime-root": 2,
2555
- "env-workspace-id": 3,
2556
- "legacy-global": 4
2557
- };
2558
- allEntries.sort((a, b) => (sourceOrder[a.source] ?? 99) - (sourceOrder[b.source] ?? 99));
2559
- for (const entry of allEntries) {
2560
- const marker = entry === result.resolved ? ">>>" : " ";
2561
- const existsLabel = entry.exists ? "exists" : "missing";
2562
- lines.push(`${marker} [${entry.source}] ${entry.path}`);
2563
- lines.push(` ${entry.reason} (${existsLabel})`);
3369
+ await confirmRevocation(options, request, preview, () => {
3370
+ writeDispatched = true;
3371
+ });
3372
+ } catch (error) {
3373
+ if (error instanceof RemoteCoordinatorRequestError && error.status === 409 && (error.code === "revocation_preview_stale" || error.code === "revocation_target_unavailable")) return failure(options, error.code, "Revocation rejected: the target is unavailable or preview evidence changed. Preview again; do not retry automatically.");
3374
+ if (writeDispatched) return failure(options, "revocation_outcome_unknown", "Revocation outcome is unknown. It may have committed; do not assume rollback or retry automatically. Ask the coordinator operator to inspect both retained subjects.");
3375
+ failure(options, "revocation_unavailable", "Revocation preview unavailable or invalid. Check the saved HTTPS origin (HTTP only on literal loopback), operator credential and operator-enabled server context. The CLI does not activate the server. No revocation requested.");
2564
3376
  }
2565
- return lines.join("\n");
2566
3377
  }
2567
- var whereCmd = new Command("where").configureHelp(helpStyle).description("show resolved config file path");
2568
- addConfigOption(whereCmd);
2569
- addJsonOption(whereCmd);
2570
- whereCmd.action((opts) => {
2571
- try {
2572
- const result = resolveCodememConfigPath(opts.config, "read");
2573
- if (opts.json) console.log(JSON.stringify(result, null, 2));
2574
- else console.log(formatWhereHuman(result));
2575
- } catch (err) {
2576
- if (opts.json) console.log(JSON.stringify({
2577
- error: "config_error",
2578
- message: err instanceof Error ? err.message : String(err)
2579
- }));
2580
- else console.error(err instanceof Error ? err.message : String(err));
2581
- process.exitCode = 1;
2582
- }
2583
- });
2584
- configCommand.addCommand(whereCmd);
3378
+ function buildCoordinatorRevokeDeviceCommand() {
3379
+ const command = new Command("revoke-device").configureHelp(helpStyle).description("Preview and confirm whole-database device ID and key revocation").argument("<group>", "group supplying current enrollment evidence only").argument("<device>", "device ID to revoke across all groups");
3380
+ addConfigOption(command);
3381
+ addJsonOption(command);
3382
+ command.action(revokeDevice);
3383
+ return command;
3384
+ }
2585
3385
  //#endregion
2586
3386
  //#region src/commands/coordinator.ts
2587
3387
  /**
@@ -2616,7 +3416,7 @@ function parseOptionalInteger(value, name) {
2616
3416
  }
2617
3417
  function formatImportInviteError(error) {
2618
3418
  const message = error instanceof Error ? error.message : String(error);
2619
- const recipientInviteViewerGuidance = "Accept Team/add-device recipient invitations through the Viewer: run `codemem serve`, review the access details, and confirm the invitation there.";
3419
+ const recipientInviteReviewGuidance = "Review this invitation with `codemem coordinator import-invite <invite> --review`, then accept with --reviewed-onboarding-digest <digest> using the same profile and device name. You can also review and accept through the Viewer with `codemem serve`.";
2620
3420
  switch (message) {
2621
3421
  case "invite_already_bound": return "This invite was already used. Ask the sender for a new one.";
2622
3422
  case "invite_expired": return "This invite has expired. Ask the sender for a new one.";
@@ -2631,16 +3431,66 @@ function formatImportInviteError(error) {
2631
3431
  case "invite_identity_conflict":
2632
3432
  case "recipient_invite_intent_mismatch":
2633
3433
  case "recipient_invite_review_unavailable":
2634
- case "reviewed_onboarding_digest_required": return recipientInviteViewerGuidance;
3434
+ case "reviewed_onboarding_digest_required":
3435
+ case "reviewed_onboarding_stale": return recipientInviteReviewGuidance;
3436
+ case "invite_review_profile_required": return "Invite review needs an initialized device and matching keys. Run `codemem sync pair --payload-only` with the intended --db-path, --config, and CODEMEM_KEYS_DIR first, then review that profile with --keys-dir.";
3437
+ case "recipient_invite_review_required": return "Only Team/add-device recipient invitations need --review. Import this legacy invitation without --review.";
3438
+ case "invite_inspection_transport_failed": return "Could not inspect the invitation at its coordinator. Check network access and use the original coordinator URL; redirects are not accepted.";
3439
+ case "invite_coordinator_conflict": return "This profile already uses another coordinator. Use the intended device profile and review again.";
2635
3440
  default: return message;
2636
3441
  }
2637
3442
  }
3443
+ function inviteImportProfile(invite, opts) {
3444
+ return {
3445
+ inviteValue: invite,
3446
+ dbPath: resolveDbOpt(opts) ?? null,
3447
+ keysDir: opts.keysDir ?? null,
3448
+ configPath: opts.config ?? null,
3449
+ recipientDisplayName: opts.recipientName?.trim() || null,
3450
+ deviceDisplayName: opts.deviceName?.trim() || null
3451
+ };
3452
+ }
3453
+ async function acceptInviteWithProfileCheck(profile, reviewedOnboardingDigest) {
3454
+ const digest = reviewedOnboardingDigest?.trim() || null;
3455
+ if (digest) {
3456
+ if ((await coordinatorReviewInviteAction(profile)).reviewed_onboarding_digest !== digest) throw new Error("reviewed_onboarding_stale");
3457
+ }
3458
+ return coordinatorImportInviteAction({
3459
+ ...profile,
3460
+ reviewedOnboardingDigest: digest
3461
+ });
3462
+ }
3463
+ function rejectConflictingInviteReviewOptions(opts) {
3464
+ if (!opts.review || opts.reviewedOnboardingDigest === void 0) return false;
3465
+ const message = "Use --review or --reviewed-onboarding-digest, not both.";
3466
+ if (opts.json) emitJsonError("usage_error", message);
3467
+ else p.log.error(message);
3468
+ process.exitCode = 2;
3469
+ return true;
3470
+ }
3471
+ function printInviteReview(review, opts) {
3472
+ console.log(JSON.stringify(review, null, 2));
3473
+ if (opts.json) return;
3474
+ p.outro("Review only: no invitation accepted or local profile changed. To accept, rerun with --reviewed-onboarding-digest <digest> and the same profile and device name. A Team recipient display name is cosmetic and may be changed before acceptance.");
3475
+ }
2638
3476
  /**
2639
3477
  * Build a fresh coordinator command tree. Each call returns independent
2640
3478
  * Commander instances so the tree can be mounted under multiple parents.
2641
3479
  */
2642
- function buildCoordinatorCommand() {
3480
+ function createCoordinatorCommandRoot() {
2643
3481
  const cmd = new Command("coordinator").configureHelp(helpStyle).description("Manage coordinator invites, join requests, and relay server");
3482
+ cmd.addCommand(buildCoordinatorLinkAccountCommand());
3483
+ cmd.addCommand(buildCoordinatorEnrollOwnerCommand());
3484
+ cmd.addCommand(buildCoordinatorReviewDeviceOwnerCommand());
3485
+ cmd.addCommand(buildCoordinatorQualifyOwnerCommand());
3486
+ cmd.addCommand(buildCoordinatorGrantIdentityGroupCommand());
3487
+ cmd.addCommand(buildCoordinatorRevokeDeviceCommand());
3488
+ cmd.addCommand(buildCoordinatorAuthSchemaCommand());
3489
+ cmd.addCommand(buildCoordinatorAccountSchemaCommand());
3490
+ return cmd;
3491
+ }
3492
+ function buildCoordinatorCommand() {
3493
+ const cmd = createCoordinatorCommandRoot();
2644
3494
  const groupCreateCmd = new Command("group-create").configureHelp(helpStyle).description("Create a coordinator group in the local store").argument("<group>", "group id").option("--name <name>", "display name override");
2645
3495
  addDbOption(groupCreateCmd);
2646
3496
  addJsonOption(groupCreateCmd);
@@ -3265,20 +4115,19 @@ function buildCoordinatorCommand() {
3265
4115
  }
3266
4116
  });
3267
4117
  cmd.addCommand(createInviteCmd);
3268
- const importInviteCmd = new Command("import-invite").configureHelp(helpStyle).description("Import a coordinator invite").argument("<invite>", "invite value or link").option("-R, --recipient-name <name>", "recipient Identity display name").option("-N, --device-name <name>", "device display name").option("--keys-dir <path>", "keys directory");
4118
+ const importInviteCmd = new Command("import-invite").configureHelp(helpStyle).description("Import a coordinator invite").argument("<invite>", "invite value or link").option("-R, --recipient-name <name>", "recipient Identity display name").option("-N, --device-name <name>", "device display name").option("--review", "Review recipient access without accepting").option("--reviewed-onboarding-digest <digest>", "Accept the reviewed recipient access").option("--keys-dir <path>", "keys directory");
3269
4119
  addDbOption(importInviteCmd);
3270
4120
  addConfigOption(importInviteCmd);
3271
4121
  addJsonOption(importInviteCmd);
3272
4122
  importInviteCmd.action(async (invite, opts) => {
3273
4123
  try {
3274
- const result = await coordinatorImportInviteAction({
3275
- inviteValue: invite,
3276
- dbPath: resolveDbOpt(opts) ?? null,
3277
- keysDir: opts.keysDir ?? null,
3278
- configPath: opts.config ?? null,
3279
- recipientDisplayName: opts.recipientName?.trim() || null,
3280
- deviceDisplayName: opts.deviceName?.trim() || null
3281
- });
4124
+ if (rejectConflictingInviteReviewOptions(opts)) return;
4125
+ const profile = inviteImportProfile(invite, opts);
4126
+ if (opts.review) {
4127
+ printInviteReview(await coordinatorReviewInviteAction(profile), opts);
4128
+ return;
4129
+ }
4130
+ const result = await acceptInviteWithProfileCheck(profile, opts.reviewedOnboardingDigest);
3282
4131
  if (opts.json) {
3283
4132
  console.log(JSON.stringify(result, null, 2));
3284
4133
  return;
@@ -4250,11 +5099,11 @@ async function distillAction(opts) {
4250
5099
  store?.close();
4251
5100
  }
4252
5101
  }
4253
- var cmd$3 = new Command("distill").configureHelp(helpStyle).description("Mine recurring memories into reviewable context candidates").option("-p, --project <project>", "project identifier (defaults to git repo root)").option("-A, --all-projects", "mine memories across all projects").option("-k, --kind <kind>", "memory kind to mine (repeat or comma-separate)", collectKind, []).option("-m, --min-recurrence <n>", "minimum member count per candidate", "2").option("-l, --limit <n>", "max candidates", "10").option("-e, --explain", "include evidence snippets in human output").option("--include-documented", "include candidates already represented in context files").option("--no-judge", "skip the observer-model worthiness judgment that drops routine-activity clusters").option("-D, --draft", "draft an AGENTS.md rule for the top candidate and show the diff").option("--apply", "write the drafted rule to the target file (implies --draft; prompts to confirm; with --json applies immediately without prompting)");
4254
- addDbOption(cmd$3);
4255
- addJsonOption(cmd$3);
4256
- cmd$3.action(distillAction);
4257
- var distillCommand = cmd$3;
5102
+ var cmd$5 = new Command("distill").configureHelp(helpStyle).description("Mine recurring memories into reviewable context candidates").option("-p, --project <project>", "project identifier (defaults to git repo root)").option("-A, --all-projects", "mine memories across all projects").option("-k, --kind <kind>", "memory kind to mine (repeat or comma-separate)", collectKind, []).option("-m, --min-recurrence <n>", "minimum member count per candidate", "2").option("-l, --limit <n>", "max candidates", "10").option("-e, --explain", "include evidence snippets in human output").option("--include-documented", "include candidates already represented in context files").option("--no-judge", "skip the observer-model worthiness judgment that drops routine-activity clusters").option("-D, --draft", "draft an AGENTS.md rule for the top candidate and show the diff").option("--apply", "write the drafted rule to the target file (implies --draft; prompts to confirm; with --json applies immediately without prompting)");
5103
+ addDbOption(cmd$5);
5104
+ addJsonOption(cmd$5);
5105
+ cmd$5.action(distillAction);
5106
+ var distillCommand = cmd$5;
4258
5107
  //#endregion
4259
5108
  //#region src/commands/embed.ts
4260
5109
  function parseOptionalPositiveInt(value) {
@@ -4353,12 +5202,12 @@ function invokedAsTopLevelAlias(name, argv = process.argv.slice(2)) {
4353
5202
  }
4354
5203
  //#endregion
4355
5204
  //#region src/commands/export-memories.ts
4356
- function expandUserPath(value) {
5205
+ function expandUserPath$1(value) {
4357
5206
  return value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
4358
5207
  }
4359
- var cmd$2 = new Command("export-memories").configureHelp(helpStyle).description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--project <project>", "filter by project (defaults to git repo root)").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp");
4360
- addDbOption(cmd$2);
4361
- cmd$2.action((output, opts) => {
5208
+ var cmd$4 = new Command("export-memories").configureHelp(helpStyle).description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--project <project>", "filter by project (defaults to git repo root)").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp");
5209
+ addDbOption(cmd$4);
5210
+ cmd$4.action((output, opts) => {
4362
5211
  if (output !== "-" && invokedAsTopLevelAlias("export-memories")) emitDeprecationWarning("codemem export-memories", "codemem memory export");
4363
5212
  const payload = exportMemories({
4364
5213
  dbPath: resolveDbPath(resolveDbOpt(opts)),
@@ -4372,7 +5221,7 @@ cmd$2.action((output, opts) => {
4372
5221
  process.stdout.write(text);
4373
5222
  return;
4374
5223
  }
4375
- const outputPath = expandUserPath(output);
5224
+ const outputPath = expandUserPath$1(output);
4376
5225
  writeFileSync(outputPath, text, "utf8");
4377
5226
  p.intro("codemem export-memories");
4378
5227
  p.log.success([
@@ -4384,13 +5233,13 @@ cmd$2.action((output, opts) => {
4384
5233
  ].join("\n"));
4385
5234
  p.outro("done");
4386
5235
  });
4387
- var exportMemoriesCommand = cmd$2;
5236
+ var exportMemoriesCommand = cmd$4;
4388
5237
  //#endregion
4389
5238
  //#region src/commands/import-memories.ts
4390
- var cmd$1 = new Command("import-memories").configureHelp(helpStyle).description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing");
4391
- addDbOption(cmd$1);
4392
- addJsonOption(cmd$1);
4393
- cmd$1.action((inputFile, opts) => {
5239
+ var cmd$3 = new Command("import-memories").configureHelp(helpStyle).description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing");
5240
+ addDbOption(cmd$3);
5241
+ addJsonOption(cmd$3);
5242
+ cmd$3.action((inputFile, opts) => {
4394
5243
  if (!opts.json && invokedAsTopLevelAlias("import-memories")) emitDeprecationWarning("codemem import-memories", "codemem memory import");
4395
5244
  let payload;
4396
5245
  try {
@@ -4451,7 +5300,7 @@ cmd$1.action((inputFile, opts) => {
4451
5300
  ].join("\n"));
4452
5301
  p.outro("done");
4453
5302
  });
4454
- var importMemoriesCommand = cmd$1;
5303
+ var importMemoriesCommand = cmd$3;
4455
5304
  //#endregion
4456
5305
  //#region src/maintenance-worker-runtime.ts
4457
5306
  var defaultLogger = {
@@ -4957,7 +5806,7 @@ function forgetMemoryAction(idStr, opts) {
4957
5806
  }
4958
5807
  const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
4959
5808
  try {
4960
- if (!store.get(memoryId)) {
5809
+ if (!store.forgetForUser(memoryId)) {
4961
5810
  if (opts.json) emitJsonError("not_found", `Memory ${memoryId} not found`);
4962
5811
  else {
4963
5812
  p.log.error(`Memory ${memoryId} not found`);
@@ -4965,7 +5814,6 @@ function forgetMemoryAction(idStr, opts) {
4965
5814
  }
4966
5815
  return;
4967
5816
  }
4968
- store.forget(memoryId);
4969
5817
  if (opts.json) console.log(JSON.stringify({
4970
5818
  id: memoryId,
4971
5819
  status: "forgotten"
@@ -4980,39 +5828,35 @@ function parseConfidence(value) {
4980
5828
  if (!Number.isFinite(n)) return .5;
4981
5829
  return Math.min(1, Math.max(0, n));
4982
5830
  }
4983
- function rollbackManualMemory(store, sessionId, memoryId) {
4984
- store.db.transaction(() => {
4985
- const row = store.db.prepare("SELECT import_key FROM memory_items WHERE id = ?").get(memoryId);
4986
- store.db.prepare("DELETE FROM memory_vectors WHERE memory_id = ?").run(memoryId);
4987
- store.db.prepare("DELETE FROM memory_file_refs WHERE memory_id = ?").run(memoryId);
4988
- store.db.prepare("DELETE FROM memory_concept_refs WHERE memory_id = ?").run(memoryId);
4989
- store.db.prepare("DELETE FROM replication_ops WHERE entity_type = 'memory_item' AND (entity_id = ? OR entity_id = ?)").run(row?.import_key ?? "", String(memoryId));
4990
- store.db.prepare("DELETE FROM memory_items WHERE id = ?").run(memoryId);
4991
- store.db.prepare(`DELETE FROM sessions
4992
- WHERE id = ?
4993
- AND NOT EXISTS (SELECT 1 FROM memory_items WHERE session_id = ?)`).run(sessionId, sessionId);
4994
- })();
4995
- }
4996
- async function rememberMemoryAction(opts) {
4997
- const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
4998
- let sessionId = null;
4999
- try {
5000
- const project = resolveProject(process.cwd(), opts.project ?? null);
5001
- sessionId = store.startSession({
5831
+ function createManualMemory(store, opts) {
5832
+ const project = resolveProject(process.cwd(), opts.project ?? null);
5833
+ const createdMemories = [];
5834
+ return store.db.transaction(() => {
5835
+ const newSessionId = store.startSession({
5002
5836
  cwd: process.cwd(),
5003
5837
  project,
5004
5838
  user: process.env.USER ?? "unknown",
5005
5839
  toolVersion: "manual",
5006
5840
  metadata: { manual: true }
5007
5841
  });
5008
- const memId = store.remember(sessionId, opts.kind, opts.title, opts.body, parseConfidence(opts.confidence), opts.tags);
5009
- if (!store.get(memId)) {
5010
- await store.flushPendingVectorWrites();
5011
- rollbackManualMemory(store, sessionId, memId);
5012
- sessionId = null;
5013
- throw new Error("unauthorized_scope");
5014
- }
5015
- store.endSession(sessionId, { manual: true });
5842
+ const memId = store.rememberForUser(newSessionId, opts.kind, opts.title, opts.body, parseConfidence(opts.confidence), opts.tags, void 0, { createdMemories });
5843
+ if (!store.get(memId)) throw new Error("unauthorized_scope");
5844
+ store.endSession(newSessionId, { manual: true });
5845
+ return {
5846
+ memId,
5847
+ sessionId: newSessionId,
5848
+ createdMemories
5849
+ };
5850
+ }).immediate();
5851
+ }
5852
+ async function rememberMemoryAction(opts) {
5853
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
5854
+ let sessionId = null;
5855
+ try {
5856
+ const result = createManualMemory(store, opts);
5857
+ sessionId = result.sessionId;
5858
+ const memId = result.memId;
5859
+ store.enqueueCommittedVectorWrites(result.createdMemories);
5016
5860
  await store.flushPendingVectorWrites();
5017
5861
  if (opts.json) console.log(JSON.stringify({ id: memId }));
5018
5862
  else p.log.success(`Stored memory ${memId}`);
@@ -7172,6 +8016,218 @@ var piHookInjectCommand = piHookInjectCmd.action(async (opts) => {
7172
8016
  }
7173
8017
  });
7174
8018
  //#endregion
8019
+ //#region src/commands/pi-import-sessions.ts
8020
+ /**
8021
+ * codemem pi-import-sessions — backfill pi agent session history (design D3/D4).
8022
+ *
8023
+ * Thin wrapper over core importPiSessions: walks every session JSONL file under
8024
+ * <pi-agent-dir>/sessions (project subdirs included, PI_CODING_AGENT_DIR honored).
8025
+ *
8026
+ * Extraction is opt-in (--extract, off by default): it runs the observer model over
8027
+ * the imported history, which costs tokens proportional to backlog size. With
8028
+ * --extract the STANDARD flush path (flushRawEvents — the same function the
8029
+ * raw-event sweeper calls each tick) runs over the imported pi sessions; there is
8030
+ * no bespoke extraction pipeline. Without it, imported events are stored searchable
8031
+ * and the running viewer's sweeper picks them up like any other backlog.
8032
+ */
8033
+ var EXTRACT_COST_NOTE = "also extract memories (runs the observer model over imported history — LLM cost scales with backlog; default: off, imported events stay searchable)";
8034
+ /**
8035
+ * Import sessions, then — when --extract is set — drain the imported pi
8036
+ * sessions through the standard flush path. Returns the import summary plus
8037
+ * extraction counts and failures.
8038
+ */
8039
+ async function runPiImportSessions(opts, deps = {}) {
8040
+ const dbPath = resolveDbPath(resolveDbOpt(opts));
8041
+ return {
8042
+ summary: importPiSessions({
8043
+ dbPath,
8044
+ onProgress: deps.onProgress
8045
+ }),
8046
+ extraction: opts.extract ? await flushImportedPiSessions(dbPath, deps.observer) : {
8047
+ requested: false,
8048
+ flushedEvents: 0,
8049
+ failedSessions: 0,
8050
+ pendingSessions: null,
8051
+ error: null
8052
+ }
8053
+ };
8054
+ }
8055
+ /**
8056
+ * Run the existing raw-event flush (the sweeper's own flushRawEvents) over
8057
+ * every pending source-"pi" session — never a bespoke extraction path. Fails
8058
+ * open per session: a failed flush batch stays retryable by the sweeper's
8059
+ * pending-queue phase, exactly like live-session flush failures.
8060
+ */
8061
+ async function flushImportedPiSessions(dbPath, observer) {
8062
+ const store = new MemoryStore(dbPath);
8063
+ const extraction = {
8064
+ requested: true,
8065
+ flushedEvents: 0,
8066
+ failedSessions: 0,
8067
+ pendingSessions: 0,
8068
+ error: null
8069
+ };
8070
+ try {
8071
+ let resolved;
8072
+ try {
8073
+ resolved = observer ?? new ObserverClient();
8074
+ } catch (err) {
8075
+ extraction.error = `observer init failed: ${err instanceof Error ? err.message : String(err)}`;
8076
+ extraction.pendingSessions = piPendingSessions(store).length;
8077
+ return extraction;
8078
+ }
8079
+ for (const streamId of piPendingSessions(store)) try {
8080
+ const result = await flushRawEvents(store, { observer: resolved }, {
8081
+ opencodeSessionId: streamId,
8082
+ source: "pi",
8083
+ cwd: null,
8084
+ project: null,
8085
+ startedAt: null,
8086
+ maxEvents: null
8087
+ });
8088
+ extraction.flushedEvents += result.flushed;
8089
+ } catch (err) {
8090
+ extraction.failedSessions++;
8091
+ extraction.error ??= `session ${streamId}: ${err instanceof Error ? err.message : String(err)}`;
8092
+ }
8093
+ extraction.pendingSessions = piPendingSessions(store).length;
8094
+ return extraction;
8095
+ } finally {
8096
+ store.close();
8097
+ }
8098
+ }
8099
+ /** Pending-flush sessions attributed to source "pi", without a page cap. */
8100
+ function piPendingSessions(store) {
8101
+ return store.rawEventSessionsPendingFlush(null, "pi").map((session) => session.streamId);
8102
+ }
8103
+ /** Human-readable summary line; per-file progress is streamed by the caller. */
8104
+ function formatPiImportHuman(result) {
8105
+ const { summary, extraction } = result;
8106
+ const lines = [`Scanned ${summary.filesScanned} files: ${summary.filesImported} imported, ${summary.filesUnchanged} unchanged, ${summary.filesEmpty} empty, ${summary.filesErrored} errored — ${summary.inserted} events inserted, ${summary.skipped} skipped.`];
8107
+ if (extraction.requested) {
8108
+ lines.push(`Extraction: observer flushed ${extraction.flushedEvents} events into memories.`);
8109
+ if (extraction.failedSessions > 0 || (extraction.pendingSessions ?? 0) > 0) lines.push(`${extraction.failedSessions} sessions failed; ${extraction.pendingSessions} remain pending.`);
8110
+ if (extraction.error) lines.push(`Extraction error: ${extraction.error}`);
8111
+ }
8112
+ return lines.join("\n");
8113
+ }
8114
+ function logProgress(progress) {
8115
+ const name = progress.file;
8116
+ if (progress.status === "imported") p.log.info(`imported ${name} (+${progress.inserted} events)`);
8117
+ else if (progress.status === "unchanged") p.log.info(`unchanged ${name}`);
8118
+ else if (progress.status === "empty") p.log.info(`empty ${name}`);
8119
+ else p.log.warn(`error ${name}: ${progress.error ?? "unknown error"}`);
8120
+ }
8121
+ var cmd$2 = new Command("pi-import-sessions").configureHelp(helpStyle).description("Import pi agent session history into the searchable store").option("--extract", EXTRACT_COST_NOTE);
8122
+ addDbOption(cmd$2);
8123
+ addJsonOption(cmd$2);
8124
+ var piImportSessionsCommand = cmd$2.action(async (opts) => {
8125
+ try {
8126
+ const result = await runPiImportSessions(opts, opts.json ? {} : { onProgress: logProgress });
8127
+ const { extraction } = result;
8128
+ let extractionFailure;
8129
+ if (extraction.error || extraction.failedSessions > 0 || (extraction.pendingSessions ?? 0) > 0) {
8130
+ extractionFailure = extraction.error ?? `${extraction.failedSessions} sessions failed; ${extraction.pendingSessions} remain pending.`;
8131
+ process.exitCode = 1;
8132
+ }
8133
+ if (opts.json) {
8134
+ console.log(JSON.stringify({
8135
+ ...result.summary,
8136
+ extraction,
8137
+ error: extractionFailure ? "pi_extraction_incomplete" : void 0,
8138
+ message: extractionFailure
8139
+ }, null, 2));
8140
+ return;
8141
+ }
8142
+ p.log.message(formatPiImportHuman(result));
8143
+ } catch (error) {
8144
+ const message = error instanceof Error ? error.message : "pi-import-sessions failed";
8145
+ if (opts.json) emitJsonError("pi_import_sessions_failed", message);
8146
+ else {
8147
+ p.log.error(message);
8148
+ process.exitCode = 1;
8149
+ }
8150
+ }
8151
+ });
8152
+ //#endregion
8153
+ //#region src/commands/pi-session-search.ts
8154
+ /**
8155
+ * codemem pi-session-search — search stored pi session conversations.
8156
+ *
8157
+ * Queries the raw-event store (source "pi") via core searchPiSessions — the
8158
+ * same contract as GET /api/pi/sessions/search and the pi-extension
8159
+ * memory_session_search tool (design D5). --json prints the exact response
8160
+ * object those surfaces return, which is what the extension's CLI fallback
8161
+ * parses.
8162
+ *
8163
+ * Empty results usually mean the index only covers sessions captured since
8164
+ * codemem was installed; `codemem pi-import-sessions` backfills history.
8165
+ */
8166
+ var EMPTY_INDEX_HINT = "The index may only cover sessions captured since codemem was installed; backfill history with: codemem pi-import-sessions";
8167
+ function intOption(value) {
8168
+ const parsed = Number.parseInt(String(value ?? ""), 10);
8169
+ return Number.isFinite(parsed) ? parsed : void 0;
8170
+ }
8171
+ /**
8172
+ * Run the session search and return the shared response object.
8173
+ * Throws on invalid input or store failure; printing stays with the caller.
8174
+ */
8175
+ function runPiSessionSearch(query, opts) {
8176
+ if (!query.trim()) throw new Error("query required");
8177
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
8178
+ try {
8179
+ return searchPiSessions(store.db, query, {
8180
+ project: opts.project,
8181
+ session_id: opts.sessionId,
8182
+ limit: intOption(opts.limit),
8183
+ snippet_chars: intOption(opts.snippetChars)
8184
+ });
8185
+ } finally {
8186
+ store.close();
8187
+ }
8188
+ }
8189
+ /** Human-readable output: "Found N results" header + attributed snippet lines. */
8190
+ function formatPiSessionSearchHuman(response) {
8191
+ const warning = response.query_truncated ? "Warning: matching used only part of the query. Shorten it to at most 8,192 characters and 64 search terms, then retry." : null;
8192
+ if (response.results.length === 0) return `No results found for "${response.query}". ${warning ?? EMPTY_INDEX_HINT}`;
8193
+ const lines = [`Found ${response.returned} results for "${response.query}"`];
8194
+ if (warning) lines.push(warning);
8195
+ for (const [index, match] of response.results.entries()) {
8196
+ const attribution = [
8197
+ match.role,
8198
+ match.project ?? "no project",
8199
+ `session ${match.session_id}`,
8200
+ match.timestamp ?? "unknown time"
8201
+ ].join(" · ");
8202
+ const snippet = match.snippet_truncated ? `${match.snippet}…` : match.snippet;
8203
+ lines.push(`${index + 1}. ${attribution}`, snippet);
8204
+ }
8205
+ if (response.truncated) lines.push(`Showing ${response.returned} of ${response.total_matches} matches.`);
8206
+ return lines.join("\n");
8207
+ }
8208
+ var cmd$1 = new Command("pi-session-search").configureHelp(helpStyle).description("Search stored pi session conversations by text query").argument("<query>", "search query").option("--project <project>", "filter by project label").option("--session-id <id>", "filter by pi session id").option("--limit <n>", "max results (1-20)", "10").option("--snippet-chars <n>", "per-result snippet cap in chars (100-4000)", "1200");
8209
+ addDbOption(cmd$1);
8210
+ addJsonOption(cmd$1);
8211
+ var piSessionSearchCommand = cmd$1.action((query, opts) => {
8212
+ try {
8213
+ const response = runPiSessionSearch(query, opts);
8214
+ if (opts.json) {
8215
+ console.log(JSON.stringify(response, null, 2));
8216
+ return;
8217
+ }
8218
+ const text = formatPiSessionSearchHuman(response);
8219
+ if (response.results.length === 0) p.log.warn(text);
8220
+ else p.log.message(text);
8221
+ } catch (error) {
8222
+ const message = error instanceof Error ? error.message : "pi-session-search failed";
8223
+ if (opts.json) emitJsonError("pi_session_search_failed", message);
8224
+ else {
8225
+ p.log.error(message);
8226
+ process.exitCode = 1;
8227
+ }
8228
+ }
8229
+ });
8230
+ //#endregion
7175
8231
  //#region src/commands/recent.ts
7176
8232
  var cmd = new Command("recent").configureHelp(helpStyle).description("Show recent memories").option("--limit <n>", "max results", "5").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").option("--kind <kind>", "filter by memory kind");
7177
8233
  addDbOption(cmd);
@@ -8020,6 +9076,49 @@ function createRawEventTargetState(store) {
8020
9076
  stop
8021
9077
  };
8022
9078
  }
9079
+ function createViewerAccountLink(store, invocation, serverModule) {
9080
+ const configPath = resolveCodememConfigPath(invocation.configPath ?? void 0).resolved.path;
9081
+ const keysDir = dirname(resolveKeyPaths(process.env.CODEMEM_KEYS_DIR?.trim() || void 0)[0]);
9082
+ const readContext = serverModule.createAccountContextReader({
9083
+ getProfile: () => ({
9084
+ dbPath: store.dbPath,
9085
+ deviceId: store.deviceId,
9086
+ identityId: store.actorId,
9087
+ identityLabel: store.actorDisplayName
9088
+ }),
9089
+ readConfig: () => readCodememConfigFileAtPath(configPath),
9090
+ configPath,
9091
+ keysDir
9092
+ });
9093
+ const registry = serverModule.createAccountLinkRegistry({ readContext });
9094
+ let actualBoundPort = null;
9095
+ return {
9096
+ registry,
9097
+ setBoundPort: (port) => {
9098
+ actualBoundPort = port;
9099
+ },
9100
+ options: {
9101
+ readContext,
9102
+ registry,
9103
+ getListenerAddress: () => actualBoundPort === null ? null : {
9104
+ host: invocation.host,
9105
+ port: actualBoundPort
9106
+ }
9107
+ }
9108
+ };
9109
+ }
9110
+ function announceViewerStartup(options) {
9111
+ const { pidPath, invocation, preparedDb, info } = options;
9112
+ writeFileSync(pidPath, JSON.stringify({
9113
+ pid: process.pid,
9114
+ host: invocation.host,
9115
+ port: invocation.port
9116
+ }), "utf-8");
9117
+ p.intro("codemem viewer");
9118
+ p.log.success(`Listening on http://${info.address}:${info.port}`);
9119
+ p.log.info(`Database: ${preparedDb}`);
9120
+ p.log.step("Raw event sweeper started");
9121
+ }
8023
9122
  async function startForegroundViewer(invocation) {
8024
9123
  const serverModule = await import("@codemem/server");
8025
9124
  const { createApp, createSyncApp, closeStore, createViewerRawEventInbox, getStore } = serverModule;
@@ -8053,11 +9152,13 @@ async function startForegroundViewer(invocation) {
8053
9152
  const config = invocation.configPath ? readCodememConfigFileAtPath(invocation.configPath) : readCodememConfigFile();
8054
9153
  const syncConfig = readCoordinatorSyncConfig(config);
8055
9154
  const syncEnabled = syncConfig.syncEnabled;
9155
+ const accountLink = createViewerAccountLink(store, invocation, serverModule);
8056
9156
  const syncRuntimeStatus = {
8057
9157
  phase: syncEnabled ? "starting" : "disabled",
8058
9158
  detail: syncEnabled ? "Waiting for viewer startup to finish" : "Sync is disabled"
8059
9159
  };
8060
9160
  const appOpts = {
9161
+ accountLink: accountLink.options,
8061
9162
  storeFactory: () => store,
8062
9163
  ...rawEventProcessing.appOptions,
8063
9164
  getSyncRuntimeStatus: () => syncRuntimeStatus
@@ -8086,15 +9187,13 @@ async function startForegroundViewer(invocation) {
8086
9187
  hostname: invocation.host,
8087
9188
  port: invocation.port
8088
9189
  }, (info) => {
8089
- writeFileSync(pidPath, JSON.stringify({
8090
- pid: process.pid,
8091
- host: invocation.host,
8092
- port: invocation.port
8093
- }), "utf-8");
8094
- p.intro("codemem viewer");
8095
- p.log.success(`Listening on http://${info.address}:${info.port}`);
8096
- p.log.info(`Database: ${preparedDb}`);
8097
- p.log.step("Raw event sweeper started");
9190
+ accountLink.setBoundPort(info.port);
9191
+ announceViewerStartup({
9192
+ pidPath,
9193
+ invocation,
9194
+ preparedDb,
9195
+ info
9196
+ });
8098
9197
  maintenanceWorker = startMaintenanceWorkerProcess(invocation);
8099
9198
  if (syncEnabled) {
8100
9199
  const syncStartDelayMs = 3e3;
@@ -8143,6 +9242,7 @@ async function startForegroundViewer(invocation) {
8143
9242
  const shutdown = async () => {
8144
9243
  p.outro("shutting down");
8145
9244
  syncAbort.abort();
9245
+ await accountLink.registry.close();
8146
9246
  await stopMaintenanceWorkerProcess(maintenanceWorker, dbPath);
8147
9247
  await new Promise((resolve) => {
8148
9248
  let remaining = syncServer ? 2 : 1;
@@ -9422,6 +10522,7 @@ function installPi(options = {}) {
9422
10522
  p.log.info(" - Start (or restart) pi to load the extension package");
9423
10523
  if (!options.piMcp) p.log.info(" - Optional MCP surface: install pi-mcp-adapter, then re-run with --pi-mcp");
9424
10524
  p.log.info(" - To disable: remove the @codemem/pi-extension entry from pi settings.json packages");
10525
+ p.log.info(" - Pre-install pi session history is not imported automatically; backfill it with: codemem pi-import-sessions");
9425
10526
  }
9426
10527
  return ok;
9427
10528
  }
@@ -10470,37 +11571,99 @@ doctorCmd.action(async (opts) => {
10470
11571
  }
10471
11572
  });
10472
11573
  syncCommand.addCommand(doctorCmd);
10473
- var statusCmd = new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary");
10474
- addDbOption(statusCmd);
10475
- addConfigOption(statusCmd);
10476
- addJsonOption(statusCmd);
10477
- statusCmd.action((opts) => {
10478
- const config = readCliConfig(opts.config);
10479
- const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
10480
- try {
10481
- const d = drizzle(store.db, { schema });
10482
- const deviceRow = d.select({
10483
- device_id: schema.syncDevice.device_id,
10484
- fingerprint: schema.syncDevice.fingerprint
10485
- }).from(schema.syncDevice).limit(1).get();
10486
- const peers = d.select({
10487
- peer_device_id: schema.syncPeers.peer_device_id,
10488
- name: schema.syncPeers.name,
10489
- last_sync_at: schema.syncPeers.last_sync_at,
10490
- last_error: schema.syncPeers.last_error
10491
- }).from(schema.syncPeers).all();
10492
- const semanticIndex = getSemanticIndexDiagnostics(store.db);
10493
- const localDeviceId = deviceRow?.device_id ?? null;
10494
- const peersWithScopes = peers.map((peer) => {
11574
+ function syncStatusIdentityAndPeers(store) {
11575
+ const d = drizzle(store.db, { schema });
11576
+ const deviceRow = d.select({
11577
+ device_id: schema.syncDevice.device_id,
11578
+ fingerprint: schema.syncDevice.fingerprint
11579
+ }).from(schema.syncDevice).limit(1).get();
11580
+ const peers = d.select({
11581
+ peer_device_id: schema.syncPeers.peer_device_id,
11582
+ name: schema.syncPeers.name,
11583
+ last_sync_at: schema.syncPeers.last_sync_at,
11584
+ last_error: schema.syncPeers.last_error,
11585
+ public_key: schema.syncPeers.public_key
11586
+ }).from(schema.syncPeers).all();
11587
+ const localDeviceId = deviceRow?.device_id ?? null;
11588
+ const localSigningPublicKey = localDeviceId && peers.length > 0 && store.db.prepare(`SELECT 1 FROM scope_memberships m
11589
+ JOIN replication_scopes s ON s.scope_id = m.scope_id
11590
+ WHERE m.device_id = ? AND m.status = 'active'
11591
+ AND s.authority_type = 'coordinator' LIMIT 1`).get(localDeviceId) ? loadRuntimeSigningPublicKey(store.db, {
11592
+ deviceId: localDeviceId,
11593
+ keysDir: process.env.CODEMEM_KEYS_DIR?.trim() || void 0
11594
+ }) : null;
11595
+ return {
11596
+ deviceRow,
11597
+ peers,
11598
+ peersWithScopes: peers.map((peer) => {
10495
11599
  const peerDeviceId = String(peer.peer_device_id ?? "").trim();
10496
11600
  return {
10497
11601
  peer,
10498
11602
  scopes: listPerPeerScopeSyncState(store.db, {
10499
11603
  localDeviceId,
10500
- peerDeviceId
11604
+ peerDeviceId,
11605
+ localSigningPublicKey,
11606
+ authenticatedPeerSigningKey: peer.public_key
10501
11607
  })
10502
11608
  };
11609
+ })
11610
+ };
11611
+ }
11612
+ function printSyncStatusPeers(peersWithScopes) {
11613
+ if (peersWithScopes.length === 0) {
11614
+ p.log.info("Peers: none");
11615
+ return;
11616
+ }
11617
+ for (const { peer, scopes } of peersWithScopes) {
11618
+ const peerHeader = ` ${peer.name || peer.peer_device_id}: last_sync=${peer.last_sync_at ?? "never"}, status=${peer.last_error ?? "ok"}`;
11619
+ if (scopes.length === 0) {
11620
+ p.log.message(peerHeader);
11621
+ continue;
11622
+ }
11623
+ const scopeLines = scopes.map((scope) => {
11624
+ const state = scope.bootstrapped ? "received" : "pending";
11625
+ return ` - ${scope.label} (${scope.scope_id}): ${state}`;
10503
11626
  });
11627
+ p.log.message([
11628
+ peerHeader,
11629
+ " Spaces:",
11630
+ ...scopeLines
11631
+ ].join("\n"));
11632
+ }
11633
+ }
11634
+ function printSyncStatus(config, identityAndPeers, semanticIndex) {
11635
+ const { deviceRow, peers, peersWithScopes } = identityAndPeers;
11636
+ p.intro("codemem sync status");
11637
+ p.log.info([
11638
+ `Enabled: ${config.sync_enabled === true ? "yes" : "no"}`,
11639
+ `Host: ${config.sync_host ?? "0.0.0.0"}`,
11640
+ `Port: ${config.sync_port ?? 7337}`,
11641
+ `Interval: ${config.sync_interval_s ?? 120}s`,
11642
+ `Coordinator: ${config.sync_coordinator_url ?? "(not configured)"}`
11643
+ ].join("\n"));
11644
+ if (deviceRow) p.log.info(`Device ID: ${deviceRow.device_id}\nFingerprint: ${deviceRow.fingerprint}`);
11645
+ else p.log.warn("Device identity not initialized (run `codemem sync enable`)");
11646
+ printSyncStatusPeers(peersWithScopes);
11647
+ p.log.info([
11648
+ "Semantic index:",
11649
+ ` State: ${semanticIndex.state}`,
11650
+ ` Summary: ${semanticIndex.summary}`,
11651
+ ` Coverage: ${semanticIndex.indexed_memory_count}/${semanticIndex.embeddable_memory_count}`,
11652
+ ` Mode: ${semanticIndex.mode === "semantic" ? `semantic (${semanticIndex.semantic_search_model ?? semanticIndex.current_model})` : "keyword-only"}`
11653
+ ].join("\n"));
11654
+ p.outro(`${peers.length} peer(s)`);
11655
+ }
11656
+ var statusCmd = new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary");
11657
+ addDbOption(statusCmd);
11658
+ addConfigOption(statusCmd);
11659
+ addJsonOption(statusCmd);
11660
+ statusCmd.action(function showSyncStatus(opts) {
11661
+ const config = readCliConfig(opts.config);
11662
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
11663
+ try {
11664
+ const identityAndPeers = syncStatusIdentityAndPeers(store);
11665
+ const { deviceRow, peersWithScopes } = identityAndPeers;
11666
+ const semanticIndex = getSemanticIndexDiagnostics(store.db);
10504
11667
  if (opts.json) {
10505
11668
  console.log(JSON.stringify({
10506
11669
  enabled: config.sync_enabled === true,
@@ -10521,40 +11684,7 @@ statusCmd.action((opts) => {
10521
11684
  }, null, 2));
10522
11685
  return;
10523
11686
  }
10524
- p.intro("codemem sync status");
10525
- p.log.info([
10526
- `Enabled: ${config.sync_enabled === true ? "yes" : "no"}`,
10527
- `Host: ${config.sync_host ?? "0.0.0.0"}`,
10528
- `Port: ${config.sync_port ?? 7337}`,
10529
- `Interval: ${config.sync_interval_s ?? 120}s`,
10530
- `Coordinator: ${config.sync_coordinator_url ?? "(not configured)"}`
10531
- ].join("\n"));
10532
- if (deviceRow) p.log.info(`Device ID: ${deviceRow.device_id}\nFingerprint: ${deviceRow.fingerprint}`);
10533
- else p.log.warn("Device identity not initialized (run `codemem sync enable`)");
10534
- if (peers.length === 0) p.log.info("Peers: none");
10535
- else for (const { peer, scopes } of peersWithScopes) {
10536
- const peerHeader = ` ${peer.name || peer.peer_device_id}: last_sync=${peer.last_sync_at ?? "never"}, status=${peer.last_error ?? "ok"}`;
10537
- if (scopes.length === 0) p.log.message(peerHeader);
10538
- else {
10539
- const scopeLines = scopes.map((scope) => {
10540
- const state = scope.bootstrapped ? "received" : "pending";
10541
- return ` - ${scope.label} (${scope.scope_id}): ${state}`;
10542
- });
10543
- p.log.message([
10544
- peerHeader,
10545
- " Spaces:",
10546
- ...scopeLines
10547
- ].join("\n"));
10548
- }
10549
- }
10550
- p.log.info([
10551
- "Semantic index:",
10552
- ` State: ${semanticIndex.state}`,
10553
- ` Summary: ${semanticIndex.summary}`,
10554
- ` Coverage: ${semanticIndex.indexed_memory_count}/${semanticIndex.embeddable_memory_count}`,
10555
- ` Mode: ${semanticIndex.mode === "semantic" ? `semantic (${semanticIndex.semantic_search_model ?? semanticIndex.current_model})` : "keyword-only"}`
10556
- ].join("\n"));
10557
- p.outro(`${peers.length} peer(s)`);
11687
+ printSyncStatus(config, identityAndPeers, semanticIndex);
10558
11688
  } finally {
10559
11689
  store.close();
10560
11690
  }
@@ -10902,6 +12032,7 @@ connectCmd.action((url, opts) => {
10902
12032
  syncCommand.addCommand(connectCmd);
10903
12033
  var syncCoordinatorAlias = buildCoordinatorCommand();
10904
12034
  syncCoordinatorAlias.hook("preAction", (_thisCmd, actionCmd) => {
12035
+ if (actionCmd.optsWithGlobals().json) return;
10905
12036
  const subName = actionCmd.name();
10906
12037
  emitDeprecationWarning(`codemem sync coordinator ${subName}`, `codemem coordinator ${subName}`);
10907
12038
  });
@@ -11726,6 +12857,8 @@ var ROOT_COMPLETION_COMMANDS = [
11726
12857
  "mcp",
11727
12858
  "memory",
11728
12859
  "pack",
12860
+ "pi-import-sessions",
12861
+ "pi-session-search",
11729
12862
  "recent",
11730
12863
  "search",
11731
12864
  "serve",
@@ -11790,6 +12923,8 @@ function registerRootCommands(program) {
11790
12923
  program.addCommand(recentCommand);
11791
12924
  program.addCommand(searchCommand);
11792
12925
  program.addCommand(packCommand);
12926
+ program.addCommand(piSessionSearchCommand);
12927
+ program.addCommand(piImportSessionsCommand);
11793
12928
  program.addCommand(promptPackLedgerCommand, { hidden: true });
11794
12929
  program.addCommand(showMemoryCommand, { hidden: true });
11795
12930
  program.addCommand(forgetMemoryCommand, { hidden: true });