codemem 0.40.2 → 0.41.0

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,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, REF_BACKFILL_JOB, RawEventSweeper, RefBackfillRunner, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, applyDistillRule, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildDistillReport, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, clonePromptPackAttempt, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, draftDistillRule, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getAttributionDiagnostics, getExtractionBenchmarkProfile, getExtractionModelPricing, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryArtifactReport, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, judgeDistillReport, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadSqliteVec, mdnsEnabled, planReplicationOpsAgePrune, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, recordPromptPackArtifacts, recordPromptPackTerminal, recordRetrievalSurface, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, resolveProjectRoot, resolveRetrievalSession, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
2
+ import { DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, DeviceIdentityError, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, REF_BACKFILL_JOB, RawEventSweeper, RefBackfillRunner, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, applyDistillRule, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildDistillReport, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, 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, detectInstallKind, draftDistillRule, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getAttributionDiagnostics, getExtractionBenchmarkProfile, getExtractionModelPricing, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryArtifactReport, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getUpdateStatus, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, isStableReleaseVersion, judgeDistillReport, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadSqliteVec, mdnsEnabled, planReplicationOpsAgePrune, probeCodememViewerLiveness, probeCodememViewerLiveness as probeCodememViewerLiveness$1, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, recordPromptPackArtifacts, recordPromptPackTerminal, recordRetrievalSurface, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, resolveProjectRoot, resolveRetrievalSession, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, vacuumDatabase, viewerUrl, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { randomInt, randomUUID } from "node:crypto";
@@ -13,6 +13,7 @@ import { execFileSync, spawn, spawnSync } from "node:child_process";
13
13
  import net from "node:net";
14
14
  import { desc, eq } from "drizzle-orm";
15
15
  import { drizzle } from "drizzle-orm/better-sqlite3";
16
+ import { mkdir, open, readFile, stat, unlink } from "node:fs/promises";
16
17
  //#region src/help-style.ts
17
18
  /**
18
19
  * Shared Commander help style configuration.
@@ -1482,7 +1483,7 @@ var EMPTY_PACK$1 = {
1482
1483
  packTokens: 0
1483
1484
  };
1484
1485
  var DEFAULT_VIEWER_HOST$1 = "127.0.0.1";
1485
- var DEFAULT_VIEWER_PORT$1 = 38888;
1486
+ var DEFAULT_VIEWER_PORT$2 = 38888;
1486
1487
  var DEFAULT_MAX_CHARS$1 = 16e3;
1487
1488
  var DEFAULT_HTTP_MAX_TIME_S$1 = 2;
1488
1489
  function emitJson$1(value) {
@@ -1551,7 +1552,7 @@ async function tryHttpPack$1(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME
1551
1552
  packTokens: 0
1552
1553
  };
1553
1554
  const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST$1;
1554
- const port = parsePositiveInt$2(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT$1);
1555
+ const port = parsePositiveInt$2(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT$2);
1555
1556
  const url = new URL(`http://${host}:${port}/api/pack`);
1556
1557
  url.searchParams.set("context", context);
1557
1558
  url.searchParams.set("limit", String(parsePositiveInt$2(process.env.CODEMEM_INJECT_LIMIT, 8)));
@@ -2182,7 +2183,7 @@ var EMPTY_PACK = {
2182
2183
  packTokens: 0
2183
2184
  };
2184
2185
  var DEFAULT_VIEWER_HOST = "127.0.0.1";
2185
- var DEFAULT_VIEWER_PORT = 38888;
2186
+ var DEFAULT_VIEWER_PORT$1 = 38888;
2186
2187
  var DEFAULT_MAX_CHARS = 16e3;
2187
2188
  var DEFAULT_HTTP_MAX_TIME_S = 2;
2188
2189
  var CODEMEM_CONTEXT_HEADER = `## codemem memory context
@@ -2256,7 +2257,7 @@ async function buildLocalPack(context, project, dbPath) {
2256
2257
  }
2257
2258
  async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S * 1e3) {
2258
2259
  const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
2259
- const port = parsePositiveInt$1(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
2260
+ const port = parsePositiveInt$1(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT$1);
2260
2261
  const url = new URL(`http://${host}:${port}/api/pack`);
2261
2262
  url.searchParams.set("context", context);
2262
2263
  url.searchParams.set("limit", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8)));
@@ -4267,6 +4268,7 @@ function expandUserPath(value) {
4267
4268
  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");
4268
4269
  addDbOption(cmd$2);
4269
4270
  cmd$2.action((output, opts) => {
4271
+ if (output !== "-" && invokedAsTopLevelAlias("export-memories")) emitDeprecationWarning("codemem export-memories", "codemem memory export");
4270
4272
  const payload = exportMemories({
4271
4273
  dbPath: resolveDbPath(resolveDbOpt(opts)),
4272
4274
  project: opts.project,
@@ -4298,6 +4300,7 @@ var cmd$1 = new Command("import-memories").configureHelp(helpStyle).description(
4298
4300
  addDbOption(cmd$1);
4299
4301
  addJsonOption(cmd$1);
4300
4302
  cmd$1.action((inputFile, opts) => {
4303
+ if (!opts.json && invokedAsTopLevelAlias("import-memories")) emitDeprecationWarning("codemem import-memories", "codemem memory import");
4301
4304
  let payload;
4302
4305
  try {
4303
4306
  payload = readImportPayload(inputFile);
@@ -6040,6 +6043,90 @@ function timeSince(isoDate) {
6040
6043
  return `${Math.floor(days / 30)}mo ago`;
6041
6044
  }
6042
6045
  //#endregion
6046
+ //#region src/viewer-runtime.ts
6047
+ function isValidPid(value) {
6048
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
6049
+ }
6050
+ function isValidPort(value) {
6051
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535;
6052
+ }
6053
+ function parseViewerPidRecord(raw) {
6054
+ if (raw == null) return { state: "missing" };
6055
+ const trimmed = raw.trim();
6056
+ if (!trimmed) return { state: "malformed" };
6057
+ try {
6058
+ const parsed = JSON.parse(trimmed);
6059
+ if (isValidPid(parsed)) return {
6060
+ state: "legacy",
6061
+ pid: parsed
6062
+ };
6063
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { state: "malformed" };
6064
+ const candidate = parsed;
6065
+ if (!isValidPid(candidate.pid) || typeof candidate.host !== "string" || !candidate.host.trim() || !isValidPort(candidate.port)) return { state: "malformed" };
6066
+ return {
6067
+ state: "valid",
6068
+ record: {
6069
+ pid: candidate.pid,
6070
+ host: candidate.host.trim(),
6071
+ port: candidate.port
6072
+ }
6073
+ };
6074
+ } catch {
6075
+ if (!/^\d+$/.test(trimmed)) return { state: "malformed" };
6076
+ const pid = Number(trimmed);
6077
+ return isValidPid(pid) ? {
6078
+ state: "legacy",
6079
+ pid
6080
+ } : { state: "malformed" };
6081
+ }
6082
+ }
6083
+ function isLoopbackHost(host) {
6084
+ const normalized = host.trim().toLowerCase().replace(/^\[(.*)\]$/, "$1");
6085
+ const ipv4Parts = normalized.split(".");
6086
+ const isIpv4Loopback = ipv4Parts.length >= 1 && ipv4Parts.length <= 4 && ipv4Parts[0] === "127" && ipv4Parts.every((part) => /^\d+$/.test(part) && Number(part) <= 255);
6087
+ return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || isIpv4Loopback;
6088
+ }
6089
+ async function observeViewerRuntime(parsed, deps, defaultTarget = {
6090
+ host: "127.0.0.1",
6091
+ port: 38888
6092
+ }) {
6093
+ if (parsed.state === "malformed") return {
6094
+ state: "unknown",
6095
+ attention_code: "viewer_pid_malformed"
6096
+ };
6097
+ const record = parsed.state === "valid" ? parsed.record : parsed.state === "legacy" ? {
6098
+ ...defaultTarget,
6099
+ pid: parsed.pid
6100
+ } : defaultTarget;
6101
+ if (!isLoopbackHost(record.host)) return {
6102
+ state: "unknown",
6103
+ pid: record.pid,
6104
+ attention_code: "viewer_non_loopback"
6105
+ };
6106
+ if (record.pid && deps.isProcessRunning(record.pid) === false) return {
6107
+ state: "stopped",
6108
+ pid: record.pid
6109
+ };
6110
+ const probe = await probeCodememViewerLiveness(record, deps);
6111
+ if (probe.state === "live") return probe.degraded ? {
6112
+ state: "running",
6113
+ pid: record.pid,
6114
+ attention_code: "viewer_not_ready"
6115
+ } : record.pid ? {
6116
+ state: "running",
6117
+ pid: record.pid
6118
+ } : { state: "running" };
6119
+ if (probe.reason === "unreachable") return {
6120
+ state: "unreachable",
6121
+ pid: record.pid
6122
+ };
6123
+ return {
6124
+ state: "unknown",
6125
+ pid: record.pid,
6126
+ attention_code: probe.reason === "wrong_service" ? "viewer_wrong_service" : "viewer_unexpected_response"
6127
+ };
6128
+ }
6129
+ //#endregion
6043
6130
  //#region src/commands/serve-invocation.ts
6044
6131
  /**
6045
6132
  * Parse and validate a port string. Throws a user-friendly message (no stack trace)
@@ -6102,8 +6189,7 @@ function isLocalHost(host) {
6102
6189
  return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1" || normalized === "0.0.0.0" || normalized === "::";
6103
6190
  }
6104
6191
  function isLoopbackOnlyHost(host) {
6105
- const normalized = host.trim().toLowerCase();
6106
- return normalized === "localhost" || /^127(?:\.\d{1,3}){0,3}$/.test(normalized) || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1";
6192
+ return isLoopbackHost(host);
6107
6193
  }
6108
6194
  function warnIfViewerExposed(host, port) {
6109
6195
  if (isLoopbackOnlyHost(host)) return;
@@ -6203,22 +6289,13 @@ function commandHasExactDbPath(command, dbPath) {
6203
6289
  function readViewerPidRecord(dbPath) {
6204
6290
  const pidPath = pidFilePath(dbPath);
6205
6291
  if (!existsSync(pidPath)) return null;
6206
- const raw = readFileSync(pidPath, "utf-8").trim();
6207
- try {
6208
- const parsed = JSON.parse(raw);
6209
- if (typeof parsed.pid === "number" && typeof parsed.host === "string" && typeof parsed.port === "number") return {
6210
- pid: parsed.pid,
6211
- host: parsed.host,
6212
- port: parsed.port
6213
- };
6214
- } catch {
6215
- const pid = Number.parseInt(raw, 10);
6216
- if (Number.isFinite(pid) && pid > 0) return {
6217
- pid,
6218
- host: "127.0.0.1",
6219
- port: 38888
6220
- };
6221
- }
6292
+ const parsed = parseViewerPidRecord(readFileSync(pidPath, "utf-8"));
6293
+ if (parsed.state === "valid") return parsed.record;
6294
+ if (parsed.state === "legacy") return {
6295
+ pid: parsed.pid,
6296
+ host: "127.0.0.1",
6297
+ port: 38888
6298
+ };
6222
6299
  return null;
6223
6300
  }
6224
6301
  function normalizeViewerHost(host) {
@@ -6242,23 +6319,18 @@ function isProcessRunning(pid) {
6242
6319
  return false;
6243
6320
  }
6244
6321
  }
6245
- async function respondsLikeCodememViewer(record) {
6246
- try {
6247
- const controller = new AbortController();
6248
- const timer = setTimeout(() => controller.abort(), 1e3);
6249
- const res = await fetch(`http://${record.host}:${record.port}/api/stats`, { signal: controller.signal });
6250
- clearTimeout(timer);
6251
- return res.ok;
6252
- } catch {
6253
- return false;
6254
- }
6322
+ async function respondsLikeCodememViewer(record, fetchImpl = fetch) {
6323
+ return (await probeCodememViewerLiveness$1(record, {
6324
+ fetch: fetchImpl,
6325
+ timeoutMs: 1e3
6326
+ })).state === "live";
6255
6327
  }
6256
6328
  async function lookupViewerPidFromStats(host, port) {
6257
6329
  try {
6258
- const controller = new AbortController();
6259
- const timer = setTimeout(() => controller.abort(), 1e3);
6260
- const res = await fetch(`http://${host}:${port}/api/stats`, { signal: controller.signal });
6261
- clearTimeout(timer);
6330
+ const res = await fetch(viewerUrl({
6331
+ host,
6332
+ port
6333
+ }, "/api/stats"), { signal: AbortSignal.timeout(1e3) });
6262
6334
  if (!res.ok) return null;
6263
6335
  return extractViewerPid(await res.json());
6264
6336
  } catch {
@@ -6467,11 +6539,14 @@ async function runServeCoordinatorMaintenance(store, dependencies) {
6467
6539
  const enrollmentIssues = coordinatorEnrollment.issues ?? 0;
6468
6540
  const recipientPolicies = await dependencies.reconcileRecipientPolicyProjects(store, { limit: 3 });
6469
6541
  if (projectShares.failed > 0) throw new Error(`share operation maintenance failed for ${projectShares.failed} of ${projectShares.processed} operations`);
6470
- if (coordinatorEnrollment.failedGroups > 0 || enrollmentIssues > 0) {
6542
+ if (coordinatorEnrollment.failedGroups > 0) {
6471
6543
  const groupLabel = coordinatorEnrollment.failedGroups === 1 ? "group" : "groups";
6472
6544
  const issueLabel = enrollmentIssues === 1 ? "issue" : "issues";
6473
- throw new Error(`coordinator enrollment maintenance failed for ${coordinatorEnrollment.failedGroups} ${groupLabel} with ${enrollmentIssues} reconciliation ${issueLabel}`);
6545
+ const failures = coordinatorEnrollment.failures ?? [];
6546
+ const failureDetail = failures.length ? ` [${failures.slice(0, 3).map((failure) => `${failure.groupId}:${failure.stage}:${failure.code}`).join(", ")}${failures.length > 3 ? `, +${failures.length - 3} more` : ""}]` : "";
6547
+ throw new Error(`coordinator enrollment maintenance failed for ${coordinatorEnrollment.failedGroups} ${groupLabel} with ${enrollmentIssues} reconciliation ${issueLabel}${failureDetail}`);
6474
6548
  }
6549
+ if (enrollmentIssues > 0) throw new Error(`coordinator enrollment reconciliation found ${enrollmentIssues} ${enrollmentIssues === 1 ? "issue" : "issues"}`);
6475
6550
  if (recipientPolicies.failed > 0) throw new Error(`recipient policy maintenance failed for ${recipientPolicies.failed} of ${recipientPolicies.processed} projects`);
6476
6551
  return {
6477
6552
  projectShares,
@@ -7290,6 +7365,341 @@ var statsCommand = statsCmd.action((opts) => {
7290
7365
  }
7291
7366
  });
7292
7367
  //#endregion
7368
+ //#region src/commands/status.ts
7369
+ var MAX_ATTENTION = 20;
7370
+ var DEFAULT_VIEWER_PORT = 38888;
7371
+ var RECENT_FAILURE_WINDOW_MS = 1440 * 60 * 1e3;
7372
+ var OBSERVER_IDENTITY_KEYS = [
7373
+ "observer_provider",
7374
+ "observer_model",
7375
+ "observer_runtime",
7376
+ "observer_base_url",
7377
+ "observer_simple_provider",
7378
+ "observer_simple_model",
7379
+ "observer_rich_provider",
7380
+ "observer_rich_model"
7381
+ ];
7382
+ var defaultDependencies = {
7383
+ now: () => /* @__PURE__ */ new Date(),
7384
+ exists: existsSync,
7385
+ readText: (path) => {
7386
+ try {
7387
+ return readFileSync(path, "utf8");
7388
+ } catch {
7389
+ return null;
7390
+ }
7391
+ },
7392
+ readConfig: (path) => path ? readCodememConfigFileAtPath(path) : readCodememConfigFile(),
7393
+ resolveDbPath,
7394
+ connectReadOnly,
7395
+ collectDatabase: collectOperationalStatus,
7396
+ embeddingDisabled: isEmbeddingDisabled,
7397
+ fetch,
7398
+ isProcessRunning: (pid) => {
7399
+ try {
7400
+ process.kill(pid, 0);
7401
+ return true;
7402
+ } catch (error) {
7403
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "EPERM") return null;
7404
+ return false;
7405
+ }
7406
+ },
7407
+ env: process.env,
7408
+ writeStdout: (text) => console.log(text),
7409
+ writeStderr: (text) => console.error(text),
7410
+ setExitCode: (code) => {
7411
+ process.exitCode = code;
7412
+ }
7413
+ };
7414
+ function boolValue(value) {
7415
+ if (typeof value === "boolean") return value;
7416
+ if (typeof value !== "string") return null;
7417
+ const normalized = value.trim().toLowerCase();
7418
+ if ([
7419
+ "1",
7420
+ "true",
7421
+ "yes",
7422
+ "on"
7423
+ ].includes(normalized)) return true;
7424
+ if ([
7425
+ "0",
7426
+ "false",
7427
+ "no",
7428
+ "off"
7429
+ ].includes(normalized)) return false;
7430
+ return null;
7431
+ }
7432
+ function syncEnabled(config, env) {
7433
+ return boolValue(env.CODEMEM_SYNC_ENABLED) ?? boolValue(config.sync_enabled) ?? false;
7434
+ }
7435
+ function observerConfigured(config, env) {
7436
+ const fileConfigured = OBSERVER_IDENTITY_KEYS.some((key) => {
7437
+ const value = config[key];
7438
+ return typeof value === "string" && value.trim() !== "";
7439
+ });
7440
+ const envConfigured = OBSERVER_IDENTITY_KEYS.some((key) => {
7441
+ const value = env[`CODEMEM_${key.toUpperCase()}`];
7442
+ return typeof value === "string" && value.trim() !== "";
7443
+ });
7444
+ return fileConfigured || envConfigured;
7445
+ }
7446
+ function viewerDefaultTarget(env) {
7447
+ const host = env.CODEMEM_VIEWER_HOST?.trim() || "127.0.0.1";
7448
+ const parsedPort = Number.parseInt(env.CODEMEM_VIEWER_PORT ?? "", 10);
7449
+ return {
7450
+ host,
7451
+ port: Number.isFinite(parsedPort) && parsedPort > 0 && parsedPort <= 65535 ? parsedPort : DEFAULT_VIEWER_PORT
7452
+ };
7453
+ }
7454
+ function boundAttention(items) {
7455
+ return items.slice(0, MAX_ATTENTION).map((item) => ({
7456
+ code: item.code.replace(/[^a-z0-9_]/gi, "_").slice(0, 64),
7457
+ severity: item.severity,
7458
+ message: item.message.slice(0, 500)
7459
+ }));
7460
+ }
7461
+ function projectSync(enabled, snapshot, attention) {
7462
+ if (!enabled) return "disabled";
7463
+ if (!snapshot) return "unknown";
7464
+ if (snapshot.sync.daemon_error || snapshot.sync.needs_attention) {
7465
+ attention.push({
7466
+ code: "sync_daemon_error",
7467
+ severity: "error",
7468
+ message: "Sync requires attention; run `codemem sync doctor`"
7469
+ });
7470
+ return "error";
7471
+ }
7472
+ if (snapshot.sync.peer_errors > 0) {
7473
+ attention.push({
7474
+ code: "sync_degraded",
7475
+ severity: "warning",
7476
+ message: "Sync has recent peer failures; run `codemem sync status`"
7477
+ });
7478
+ return "degraded";
7479
+ }
7480
+ if (!snapshot.sync.available) return "unknown";
7481
+ return "healthy";
7482
+ }
7483
+ function addDatabaseAttention(state, attention) {
7484
+ if (state === "missing") attention.push({
7485
+ code: "database_missing",
7486
+ severity: "error",
7487
+ message: "Database does not exist"
7488
+ });
7489
+ else if (state === "unavailable") attention.push({
7490
+ code: "database_unavailable",
7491
+ severity: "error",
7492
+ message: "Database could not be read"
7493
+ });
7494
+ }
7495
+ function addRuntimeAttention(runtime, attention) {
7496
+ if (runtime.state === "stopped") {
7497
+ attention.push({
7498
+ code: "viewer_stopped",
7499
+ severity: "warning",
7500
+ message: "Viewer is stopped; run `codemem serve start` if needed"
7501
+ });
7502
+ return;
7503
+ }
7504
+ if (runtime.state === "unreachable") {
7505
+ attention.push({
7506
+ code: "viewer_unreachable",
7507
+ severity: "warning",
7508
+ message: "Viewer did not answer its local health check"
7509
+ });
7510
+ return;
7511
+ }
7512
+ if (runtime.attention_code) attention.push({
7513
+ code: runtime.attention_code,
7514
+ severity: "warning",
7515
+ message: {
7516
+ viewer_pid_malformed: "Viewer PID record is malformed",
7517
+ viewer_non_loopback: "Viewer PID record is not loopback; no request was made",
7518
+ viewer_not_ready: "Viewer is running but its database is not ready",
7519
+ viewer_unexpected_response: "Viewer returned an unexpected local health response",
7520
+ viewer_wrong_service: "Local health endpoint did not identify codemem viewer"
7521
+ }[runtime.attention_code]
7522
+ });
7523
+ }
7524
+ function projectDatabaseSubsystems(snapshot, configuredObserver, attention) {
7525
+ if (!snapshot) return {
7526
+ maintenance: { state: "unknown" },
7527
+ semantic_index: { state: "unknown" },
7528
+ raw_events: {
7529
+ state: "unknown",
7530
+ pending: 0
7531
+ },
7532
+ observer: { state: configuredObserver ? "unknown" : "unconfigured" }
7533
+ };
7534
+ if (snapshot.maintenance.state === "failed") attention.push({
7535
+ code: "maintenance_failed",
7536
+ severity: "error",
7537
+ message: "Maintenance has failed jobs; run `codemem maintenance status`"
7538
+ });
7539
+ else if (snapshot.maintenance.state === "running") attention.push({
7540
+ code: "maintenance_running",
7541
+ severity: "warning",
7542
+ message: "Maintenance work is in progress"
7543
+ });
7544
+ if (snapshot.semantic_index.state === "failed") attention.push({
7545
+ code: "semantic_index_failed",
7546
+ severity: "error",
7547
+ message: "Semantic index maintenance failed; run `codemem maintenance status`"
7548
+ });
7549
+ else if (snapshot.semantic_index.state === "pending") attention.push({
7550
+ code: "semantic_index_pending",
7551
+ severity: "warning",
7552
+ message: "Semantic index maintenance is pending"
7553
+ });
7554
+ else if (snapshot.semantic_index.state === "degraded") attention.push({
7555
+ code: "semantic_index_degraded",
7556
+ severity: "warning",
7557
+ message: "Semantic search is unavailable; keyword search remains available"
7558
+ });
7559
+ const pending = Math.max(0, Math.trunc(snapshot.raw_events.pending));
7560
+ let rawState = snapshot.raw_events.available ? "healthy" : "unknown";
7561
+ if (snapshot.raw_events.failed_batches > 0) {
7562
+ rawState = "failing";
7563
+ attention.push({
7564
+ code: "raw_events_failing",
7565
+ severity: "error",
7566
+ message: "Raw-event ingestion exhausted retries recently; run `codemem db raw-events-gate`"
7567
+ });
7568
+ } else if (pending > 0) {
7569
+ rawState = "backlogged";
7570
+ attention.push({
7571
+ code: "raw_events_backlogged",
7572
+ severity: "warning",
7573
+ message: `${pending} raw events pending; run \`codemem db raw-events-status\``
7574
+ });
7575
+ }
7576
+ let observerState = "unconfigured";
7577
+ if (configuredObserver) observerState = snapshot.observer.available ? "idle" : "unknown";
7578
+ if (configuredObserver && snapshot.observer.failed_batches > 0) {
7579
+ observerState = "failed";
7580
+ attention.push({
7581
+ code: "observer_failed",
7582
+ severity: "error",
7583
+ message: "Observer exhausted retries recently; run `codemem db raw-events-gate`"
7584
+ });
7585
+ } else if (configuredObserver && snapshot.observer.backoff_batches > 0) {
7586
+ observerState = "backoff";
7587
+ attention.push({
7588
+ code: "observer_backoff",
7589
+ severity: "warning",
7590
+ message: "Observer has retryable failures; run `codemem db raw-events-status`"
7591
+ });
7592
+ }
7593
+ return {
7594
+ maintenance: { state: snapshot.maintenance.state },
7595
+ semantic_index: { state: snapshot.semantic_index.state },
7596
+ raw_events: {
7597
+ state: rawState,
7598
+ pending
7599
+ },
7600
+ observer: { state: observerState }
7601
+ };
7602
+ }
7603
+ async function collectStatusReport(opts, deps = defaultDependencies) {
7604
+ const checkedAt = deps.now();
7605
+ const config = deps.readConfig(opts.config);
7606
+ const dbPath = deps.resolveDbPath(resolveDbOpt(opts));
7607
+ let databaseState = "missing";
7608
+ let snapshot = null;
7609
+ if (deps.exists(dbPath)) {
7610
+ let db = null;
7611
+ try {
7612
+ db = deps.connectReadOnly(dbPath, { warn: opts.json ? () => {} : deps.writeStderr });
7613
+ } catch {
7614
+ databaseState = "unavailable";
7615
+ }
7616
+ if (db) try {
7617
+ snapshot = deps.collectDatabase(db, {
7618
+ embeddingDisabled: deps.embeddingDisabled(),
7619
+ recentFailureCutoff: (/* @__PURE__ */ new Date(checkedAt.getTime() - RECENT_FAILURE_WINDOW_MS)).toISOString()
7620
+ });
7621
+ databaseState = "ready";
7622
+ } finally {
7623
+ db.close();
7624
+ }
7625
+ }
7626
+ const pidPath = join(dirname(dbPath), "viewer.pid");
7627
+ const runtime = await observeViewerRuntime(parseViewerPidRecord(deps.exists(pidPath) ? deps.readText(pidPath) : null), {
7628
+ fetch: deps.fetch,
7629
+ isProcessRunning: deps.isProcessRunning
7630
+ }, viewerDefaultTarget(deps.env));
7631
+ const attention = [];
7632
+ addDatabaseAttention(databaseState, attention);
7633
+ addRuntimeAttention(runtime, attention);
7634
+ const sync = projectSync(syncEnabled(config, deps.env), snapshot, attention);
7635
+ const subsystems = projectDatabaseSubsystems(snapshot, observerConfigured(config, deps.env), attention);
7636
+ const ok = !attention.some((item) => item.severity === "error");
7637
+ const boundedAttention = boundAttention(attention);
7638
+ return {
7639
+ checked_at: checkedAt.toISOString(),
7640
+ ok,
7641
+ version: VERSION,
7642
+ database: { state: databaseState },
7643
+ runtime: runtime.pid ? {
7644
+ viewer: runtime.state,
7645
+ pid: runtime.pid
7646
+ } : { viewer: runtime.state },
7647
+ sync: { state: sync },
7648
+ ...subsystems,
7649
+ attention: boundedAttention
7650
+ };
7651
+ }
7652
+ function renderStatusReport(report) {
7653
+ const lines = [
7654
+ `codemem status ${report.ok ? "OK" : "ATTENTION"}`,
7655
+ `Database: ${report.database.state}`,
7656
+ `Viewer: ${report.runtime.viewer}${report.runtime.pid ? ` (pid ${report.runtime.pid})` : ""}`,
7657
+ `Sync: ${report.sync.state}`,
7658
+ `Maintenance: ${report.maintenance.state}`,
7659
+ `Semantic index: ${report.semantic_index.state}`,
7660
+ `Raw events: ${report.raw_events.state}${report.raw_events.pending > 0 ? ` (${report.raw_events.pending} pending)` : ""}`,
7661
+ `Observer: ${report.observer.state}`
7662
+ ];
7663
+ if (report.attention.length > 0) lines.push("Attention:", ...report.attention.map((item) => `- ${item.severity}: ${item.message}`));
7664
+ return lines.join("\n");
7665
+ }
7666
+ function createStatusCommand(overrides = {}) {
7667
+ const deps = {
7668
+ ...defaultDependencies,
7669
+ ...overrides
7670
+ };
7671
+ const command = new Command("status").configureHelp(helpStyle).description("Show local operational status").allowUnknownOption(true).allowExcessArguments(true);
7672
+ addDbOption(command);
7673
+ addConfigOption(command);
7674
+ addJsonOption(command);
7675
+ return command.action(async (opts, actionCommand) => {
7676
+ if (actionCommand.args.length > 0) {
7677
+ const message = "status accepts only documented options and no positional arguments";
7678
+ if (opts.json) deps.writeStdout(JSON.stringify({
7679
+ error: "usage_error",
7680
+ message
7681
+ }));
7682
+ else deps.writeStderr(message);
7683
+ deps.setExitCode(2);
7684
+ return;
7685
+ }
7686
+ try {
7687
+ const report = await collectStatusReport(opts, deps);
7688
+ deps.writeStdout(opts.json ? JSON.stringify(report) : renderStatusReport(report));
7689
+ deps.setExitCode(0);
7690
+ } catch {
7691
+ const error = {
7692
+ error: "status_failed",
7693
+ message: "Unable to collect operational status"
7694
+ };
7695
+ if (opts.json) deps.writeStdout(JSON.stringify(error));
7696
+ else deps.writeStderr(error.message);
7697
+ deps.setExitCode(1);
7698
+ }
7699
+ });
7700
+ }
7701
+ var statusCommand = createStatusCommand();
7702
+ //#endregion
7293
7703
  //#region src/commands/sync-helpers.ts
7294
7704
  function formatSyncAttempt(row) {
7295
7705
  const status = row.ok ? "ok" : "error";
@@ -7523,6 +7933,7 @@ onceCmd.action(async (opts) => {
7523
7933
  for (const row of rows) {
7524
7934
  const result = await runSyncPass(store.db, row.peer_device_id, {
7525
7935
  keysDir,
7936
+ dbPath: store.dbPath,
7526
7937
  scanner: store.scanner
7527
7938
  });
7528
7939
  if (!result.ok) hadFailure = true;
@@ -7738,6 +8149,15 @@ pairCmd.action(async (opts) => {
7738
8149
  console.log("For machine-friendly output next time, run:");
7739
8150
  console.log(" codemem sync pair --payload-only");
7740
8151
  console.log("On the accepting device, --include/--exclude control both what it sends and what it accepts from that peer.");
8152
+ } catch (error) {
8153
+ const code = error instanceof DeviceIdentityError ? error.code : "pairing_failed";
8154
+ const message = error instanceof Error ? error.message : "Pairing failed";
8155
+ if (opts.json) {
8156
+ emitJsonError(code, message);
8157
+ return;
8158
+ }
8159
+ p.log.error(message);
8160
+ process.exitCode = 1;
7741
8161
  } finally {
7742
8162
  store.close();
7743
8163
  }
@@ -7762,12 +8182,19 @@ doctorCmd.action(async (opts) => {
7762
8182
  public_key: schema.syncPeers.public_key
7763
8183
  }).from(schema.syncPeers).all();
7764
8184
  const issues = [];
8185
+ let identityError = null;
7765
8186
  const syncHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
7766
8187
  const syncPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
7767
8188
  const viewerBinding = readViewerBinding(dbPath);
7768
8189
  const reachable = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
7769
8190
  if (!reachable) issues.push("daemon not running");
7770
8191
  if (!device) issues.push("identity missing");
8192
+ if (device) try {
8193
+ ensureDeviceIdentity(store.db, { keysDir: process.env.CODEMEM_KEYS_DIR?.trim() || void 0 });
8194
+ } catch (error) {
8195
+ identityError = error instanceof Error ? error.message : "device_identity_unavailable";
8196
+ issues.push(identityError);
8197
+ }
7771
8198
  if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) issues.push("daemon error");
7772
8199
  if (peers.length === 0) issues.push("no peers");
7773
8200
  if (!config.sync_enabled) issues.push("sync is disabled");
@@ -7799,6 +8226,7 @@ doctorCmd.action(async (opts) => {
7799
8226
  mdns_source: mdnsSource,
7800
8227
  daemon: reachable ? "running" : "not running",
7801
8228
  identity: device?.device_id ?? null,
8229
+ identity_error: identityError,
7802
8230
  daemon_error: daemonState?.last_error ?? null,
7803
8231
  peers: peerDetails,
7804
8232
  issues: [...new Set(issues)],
@@ -7812,7 +8240,10 @@ doctorCmd.action(async (opts) => {
7812
8240
  console.log(`- mDNS: ${mdnsLabel}`);
7813
8241
  console.log(`- Daemon: ${reachable ? "running" : "not running"}`);
7814
8242
  if (!device) console.log("- Identity: missing (run `codemem sync enable`)");
7815
- else console.log(`- Identity: ${device.device_id}`);
8243
+ else {
8244
+ console.log(`- Identity: ${device.device_id}`);
8245
+ if (identityError) console.log(`- Identity error: ${identityError}`);
8246
+ }
7816
8247
  if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) console.log(`- Daemon error: ${daemonState.last_error} (at ${daemonState.last_error_at ?? "unknown"})`);
7817
8248
  if (peers.length === 0) console.log("- Peers: none (pair a device first)");
7818
8249
  else {
@@ -7821,6 +8252,15 @@ doctorCmd.action(async (opts) => {
7821
8252
  }
7822
8253
  if (issues.length > 0) console.log(`WARN: ${[...new Set(issues)].slice(0, 3).join(", ")}`);
7823
8254
  else console.log("OK: sync looks healthy");
8255
+ } catch (error) {
8256
+ const code = error instanceof DeviceIdentityError ? error.code : "doctor_failed";
8257
+ const message = error instanceof Error ? error.message : "Sync diagnostics failed";
8258
+ if (opts.json) {
8259
+ emitJsonError(code, message);
8260
+ return;
8261
+ }
8262
+ p.log.error(message);
8263
+ process.exitCode = 1;
7824
8264
  } finally {
7825
8265
  store.close();
7826
8266
  }
@@ -8118,6 +8558,7 @@ bootstrapCmd.action(async (peerArg, opts) => {
8118
8558
  const statusUrl = `${candidate}/v1/status`;
8119
8559
  const headers = buildAuthHeaders({
8120
8560
  deviceId,
8561
+ dbPath: store.dbPath,
8121
8562
  method: "GET",
8122
8563
  url: statusUrl,
8123
8564
  bodyBytes: Buffer.alloc(0),
@@ -8191,6 +8632,7 @@ bootstrapCmd.action(async (peerArg, opts) => {
8191
8632
  };
8192
8633
  const { items } = await fetchAllSnapshotPages(baseUrl, resetInfo, deviceId, {
8193
8634
  keysDir,
8635
+ dbPath: store.dbPath,
8194
8636
  bootstrapGrantId: opts.bootstrapGrant,
8195
8637
  pageSize
8196
8638
  });
@@ -8207,6 +8649,15 @@ bootstrapCmd.action(async (peerArg, opts) => {
8207
8649
  p.outro(result.ok ? "Bootstrap complete" : "Bootstrap failed");
8208
8650
  }
8209
8651
  if (!result.ok) process.exitCode = 1;
8652
+ } catch (error) {
8653
+ const code = error instanceof DeviceIdentityError ? error.code : "bootstrap_failed";
8654
+ const message = error instanceof Error ? error.message : "Bootstrap failed";
8655
+ if (opts.json) {
8656
+ emitJsonError(code, message);
8657
+ return;
8658
+ }
8659
+ p.log.error(message);
8660
+ process.exitCode = 1;
8210
8661
  } finally {
8211
8662
  store.close();
8212
8663
  }
@@ -8230,13 +8681,411 @@ syncCoordinatorAlias.hook("preAction", (_thisCmd, actionCmd) => {
8230
8681
  const subName = actionCmd.name();
8231
8682
  emitDeprecationWarning(`codemem sync coordinator ${subName}`, `codemem coordinator ${subName}`);
8232
8683
  });
8233
- syncCommand.addCommand(syncCoordinatorAlias);
8684
+ syncCommand.addCommand(syncCoordinatorAlias, { hidden: true });
8685
+ //#endregion
8686
+ //#region src/commands/update.ts
8687
+ var INSTALL_TIMEOUT_MS = 300 * 1e3;
8688
+ var VERIFY_TIMEOUT_MS = 3e4;
8689
+ var PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
8690
+ var UPDATE_LOCK_FILE = "update-install.lock";
8691
+ var UpdateInstallLockedError = class extends Error {};
8692
+ function updateInstallLockPath() {
8693
+ return join(process.env.HOME?.trim() || homedir(), ".codemem", UPDATE_LOCK_FILE);
8694
+ }
8695
+ function isProcessAlive(pid) {
8696
+ try {
8697
+ process.kill(pid, 0);
8698
+ return true;
8699
+ } catch (error) {
8700
+ return error.code === "EPERM";
8701
+ }
8702
+ }
8703
+ async function removeStaleInstallLock(lockPath) {
8704
+ try {
8705
+ const [rawPid, lockStat] = await Promise.all([readFile(lockPath, "utf8"), stat(lockPath)]);
8706
+ const pid = Number.parseInt(rawPid.trim(), 10);
8707
+ if (Number.isSafeInteger(pid) && pid > 0) {
8708
+ if (isProcessAlive(pid)) return false;
8709
+ } else if (Date.now() - lockStat.mtimeMs <= 33e4) return false;
8710
+ await unlink(lockPath);
8711
+ return true;
8712
+ } catch {
8713
+ return false;
8714
+ }
8715
+ }
8716
+ async function acquireInstallLock() {
8717
+ const lockPath = updateInstallLockPath();
8718
+ await mkdir(dirname(lockPath), {
8719
+ recursive: true,
8720
+ mode: 448
8721
+ });
8722
+ for (let attempt = 0; attempt < 2; attempt += 1) try {
8723
+ const handle = await open(lockPath, "wx", 384);
8724
+ await handle.writeFile(`${process.pid}\n`, "utf8");
8725
+ return async () => {
8726
+ await handle.close().catch(() => void 0);
8727
+ await unlink(lockPath).catch(() => void 0);
8728
+ };
8729
+ } catch (error) {
8730
+ if (error.code !== "EEXIST") throw error;
8731
+ if (attempt > 0 || !await removeStaleInstallLock(lockPath)) throw new UpdateInstallLockedError("another codemem update installation is already running");
8732
+ }
8733
+ throw new UpdateInstallLockedError("another codemem update installation is already running");
8734
+ }
8735
+ function runCommand(command, args, timeoutMs, options = {}) {
8736
+ return new Promise((resolve, reject) => {
8737
+ const child = spawn(command, args, {
8738
+ cwd: options.cwd,
8739
+ detached: process.platform !== "win32",
8740
+ shell: false,
8741
+ stdio: [
8742
+ "ignore",
8743
+ "pipe",
8744
+ "pipe"
8745
+ ],
8746
+ windowsVerbatimArguments: options.windowsVerbatimArguments
8747
+ });
8748
+ let settled = false;
8749
+ let timedOut = false;
8750
+ let stdout = "";
8751
+ let stderr = "";
8752
+ let timer;
8753
+ const finish = (result) => {
8754
+ if (settled) return;
8755
+ settled = true;
8756
+ clearTimeout(timer);
8757
+ resolve(result);
8758
+ };
8759
+ const terminate = (signal) => {
8760
+ if (process.platform === "win32" && child.pid) {
8761
+ const systemRoot = process.env.SystemRoot?.trim();
8762
+ if (systemRoot && isAbsolute(systemRoot)) {
8763
+ spawnSync(join(systemRoot, "System32", "taskkill.exe"), [
8764
+ "/PID",
8765
+ String(child.pid),
8766
+ "/T",
8767
+ "/F"
8768
+ ], {
8769
+ stdio: "ignore",
8770
+ windowsHide: true
8771
+ });
8772
+ return;
8773
+ }
8774
+ }
8775
+ if (process.platform !== "win32" && child.pid) try {
8776
+ process.kill(-child.pid, signal);
8777
+ return;
8778
+ } catch {}
8779
+ child.kill(signal);
8780
+ };
8781
+ timer = setTimeout(() => {
8782
+ timedOut = true;
8783
+ terminate("SIGTERM");
8784
+ setTimeout(() => terminate("SIGKILL"), 5e3).unref();
8785
+ }, timeoutMs);
8786
+ child.stdout.setEncoding("utf8");
8787
+ child.stderr.setEncoding("utf8");
8788
+ child.stdout.on("data", (chunk) => {
8789
+ stdout += chunk;
8790
+ });
8791
+ child.stderr.on("data", (chunk) => {
8792
+ stderr += chunk;
8793
+ });
8794
+ child.once("error", (error) => {
8795
+ clearTimeout(timer);
8796
+ if (!settled) reject(error);
8797
+ });
8798
+ child.once("close", (code) => finish({
8799
+ exitCode: code ?? 1,
8800
+ stdout,
8801
+ stderr: timedOut ? "command timed out" : stderr
8802
+ }));
8803
+ });
8804
+ }
8805
+ async function resolveWindowsShim(name) {
8806
+ const systemRoot = process.env.SystemRoot?.trim();
8807
+ if (!systemRoot || !isAbsolute(systemRoot)) throw new Error("Windows SystemRoot is unavailable");
8808
+ const result = await runCommand(join(systemRoot, "System32", "where.exe"), [name], 1e4, { cwd: join(systemRoot, "System32") });
8809
+ const shim = result.stdout.split(/\r?\n/).map((value) => value.trim()).find((value) => isAbsolute(value));
8810
+ if (result.exitCode !== 0 || !shim) throw new Error(`unable to resolve ${name} from PATH`);
8811
+ return shim;
8812
+ }
8813
+ function windowsCommandLine(shim, args) {
8814
+ return `""${shim}" ${args.join(" ")}"`;
8815
+ }
8816
+ async function resolveInstallCommand() {
8817
+ if (process.platform !== "win32") return {
8818
+ command: "npm",
8819
+ args: []
8820
+ };
8821
+ const systemRoot = process.env.SystemRoot?.trim();
8822
+ if (!systemRoot || !isAbsolute(systemRoot)) throw new Error("Windows SystemRoot is unavailable");
8823
+ return {
8824
+ command: join(systemRoot, "System32", "cmd.exe"),
8825
+ args: [
8826
+ "/d",
8827
+ "/s",
8828
+ "/c",
8829
+ await resolveWindowsShim("npm.cmd")
8830
+ ],
8831
+ cwd: join(systemRoot, "System32")
8832
+ };
8833
+ }
8834
+ async function resolveVerificationCommand() {
8835
+ if (process.platform !== "win32") return {
8836
+ command: "codemem",
8837
+ args: []
8838
+ };
8839
+ const systemRoot = process.env.SystemRoot?.trim();
8840
+ if (!systemRoot || !isAbsolute(systemRoot)) throw new Error("Windows SystemRoot is unavailable");
8841
+ return {
8842
+ command: join(systemRoot, "System32", "cmd.exe"),
8843
+ args: [
8844
+ "/d",
8845
+ "/s",
8846
+ "/c",
8847
+ await resolveWindowsShim("codemem.cmd")
8848
+ ],
8849
+ cwd: join(systemRoot, "System32")
8850
+ };
8851
+ }
8852
+ function failInstall(options, code, message) {
8853
+ if (options.json) emitJsonError(code, message);
8854
+ else {
8855
+ console.error(message);
8856
+ process.exitCode = 1;
8857
+ }
8858
+ }
8859
+ function cacheQualifier(stale) {
8860
+ return stale ? " (cached result)" : "";
8861
+ }
8862
+ function renderStatusWarning(error) {
8863
+ return error ? ` Warning: ${error}` : "";
8864
+ }
8865
+ function renderHumanStatus(status) {
8866
+ const warning = renderStatusWarning(status.error);
8867
+ if (status.update_available) return `Update available${cacheQualifier(status.stale)}: ${status.current_version} → ${status.latest_version}. ${status.recommended_action}${warning}`;
8868
+ if (!isStableReleaseVersion(status.current_version)) return `Unable to compare current version ${status.current_version} with ${status.latest_version}. ${status.recommended_action}${warning}`;
8869
+ return `${status.current_version} is up to date${cacheQualifier(status.stale)}.${warning}`;
8870
+ }
8871
+ var checkCommand = addJsonOption(new Command("check").description("Check for a newer stable codemem release")).addOption(new Option("-r, --refresh", "bypass the six-hour release cache")).configureHelp(helpStyle).action(async (options) => {
8872
+ try {
8873
+ const status = await getUpdateStatus({
8874
+ currentVersion: VERSION,
8875
+ installKind: detectInstallKind({
8876
+ entryPath: process.argv[1] ?? "",
8877
+ env: process.env
8878
+ }),
8879
+ refresh: options.refresh
8880
+ });
8881
+ if (status.latest_version === null) {
8882
+ const message = status.error ?? "release status is unavailable";
8883
+ if (options.json) emitJsonError("update_check_unavailable", message);
8884
+ else {
8885
+ console.error(`Unable to check for updates: ${message}`);
8886
+ process.exitCode = 1;
8887
+ }
8888
+ return;
8889
+ }
8890
+ if (options.json) {
8891
+ console.log(JSON.stringify(status));
8892
+ return;
8893
+ }
8894
+ console.log(renderHumanStatus(status));
8895
+ } catch (error) {
8896
+ const message = error instanceof Error ? error.message : "release status is unavailable";
8897
+ if (options.json) emitJsonError("update_check_unavailable", message);
8898
+ else {
8899
+ console.error(`Unable to check for updates: ${message}`);
8900
+ process.exitCode = 1;
8901
+ }
8902
+ }
8903
+ });
8904
+ var installCommand = addJsonOption(new Command("install").description("Install an eligible stable codemem update")).configureHelp(helpStyle).action(async (options) => {
8905
+ let releaseInstallLock = null;
8906
+ try {
8907
+ const status = await getUpdateStatus({
8908
+ currentVersion: VERSION,
8909
+ installKind: detectInstallKind({
8910
+ entryPath: process.argv[1] ?? "",
8911
+ env: process.env
8912
+ }),
8913
+ refresh: true
8914
+ });
8915
+ if (!status.auto_update_eligible || !status.latest_version) {
8916
+ failInstall(options, "update_install_refused", status.recommended_action);
8917
+ return;
8918
+ }
8919
+ const targetVersion = status.latest_version;
8920
+ if (!isStableReleaseVersion(targetVersion)) {
8921
+ failInstall(options, "update_install_refused", "release version is not a stable semantic version");
8922
+ return;
8923
+ }
8924
+ releaseInstallLock = await acquireInstallLock();
8925
+ const npm = await resolveInstallCommand();
8926
+ const installation = await runCommand(npm.command, process.platform === "win32" ? [...npm.args.slice(0, 3), windowsCommandLine(npm.args[3] ?? "", [
8927
+ "install",
8928
+ "-g",
8929
+ "--registry",
8930
+ PUBLIC_NPM_REGISTRY,
8931
+ `codemem@${targetVersion}`
8932
+ ])] : [
8933
+ "install",
8934
+ "-g",
8935
+ "--registry",
8936
+ PUBLIC_NPM_REGISTRY,
8937
+ `codemem@${targetVersion}`
8938
+ ], INSTALL_TIMEOUT_MS, {
8939
+ cwd: npm.cwd,
8940
+ windowsVerbatimArguments: process.platform === "win32"
8941
+ });
8942
+ if (installation.exitCode !== 0) {
8943
+ failInstall(options, "update_install_failed", installation.stderr.trim() || "npm installation failed");
8944
+ return;
8945
+ }
8946
+ const codemem = await resolveVerificationCommand();
8947
+ const verification = await runCommand(codemem.command, process.platform === "win32" ? [...codemem.args.slice(0, 3), windowsCommandLine(codemem.args[3] ?? "", ["version"])] : ["version"], VERIFY_TIMEOUT_MS, {
8948
+ cwd: codemem.cwd,
8949
+ windowsVerbatimArguments: process.platform === "win32"
8950
+ });
8951
+ if (verification.exitCode !== 0 || verification.stdout.trim() !== targetVersion) {
8952
+ failInstall(options, "update_verification_failed", "installed version verification failed");
8953
+ return;
8954
+ }
8955
+ const result = {
8956
+ previous_version: VERSION,
8957
+ installed_version: targetVersion
8958
+ };
8959
+ if (options.json) console.log(JSON.stringify(result));
8960
+ else console.log(`Updated codemem from ${VERSION} to ${targetVersion}.`);
8961
+ } catch (error) {
8962
+ failInstall(options, error instanceof UpdateInstallLockedError ? "update_install_locked" : "update_install_failed", error instanceof Error ? error.message : "update installation failed");
8963
+ } finally {
8964
+ await releaseInstallLock?.();
8965
+ }
8966
+ });
8967
+ var updateCommand = new Command("update").description("Inspect and manage codemem updates").configureHelp(helpStyle).addCommand(checkCommand).addCommand(installCommand);
8234
8968
  //#endregion
8235
8969
  //#region src/commands/version.ts
8236
8970
  var versionCommand = new Command("version").configureHelp(helpStyle).description("Print codemem version").action(() => {
8237
8971
  console.log(VERSION);
8238
8972
  });
8239
8973
  //#endregion
8974
+ //#region src/command-tree.ts
8975
+ /**
8976
+ * Assembled root command tree — single source of truth shared by the runtime
8977
+ * entrypoint (`index.ts`) and the command-tree parity tests.
8978
+ *
8979
+ * `index.ts` stays the only module with import-time side effects (omelette
8980
+ * completion init, `program.parse()`); everything here is side-effect free
8981
+ * until `registerRootCommands` is called.
8982
+ */
8983
+ var ROOT_COMPLETION_COMMANDS = [
8984
+ "claude-hook-file-context",
8985
+ "claude-hook-inject",
8986
+ "claude-hook-ingest",
8987
+ "codex-hook-inject",
8988
+ "codex-hook-ingest",
8989
+ "config",
8990
+ "coordinator",
8991
+ "db",
8992
+ "distill",
8993
+ "embed",
8994
+ "enqueue-raw-event",
8995
+ "export-memories",
8996
+ "import-memories",
8997
+ "maintenance",
8998
+ "mcp",
8999
+ "memory",
9000
+ "pack",
9001
+ "recent",
9002
+ "search",
9003
+ "serve",
9004
+ "setup",
9005
+ "stats",
9006
+ "status",
9007
+ "sync",
9008
+ "update",
9009
+ "version",
9010
+ "help",
9011
+ "--help",
9012
+ "--version"
9013
+ ];
9014
+ /**
9015
+ * True when the CLI process was invoked through the named top-level
9016
+ * compatibility alias (first non-flag token of the real argv). Forwarding
9017
+ * wrappers such as `memory export` re-parse the alias command with synthetic
9018
+ * argv, so this stays false for canonical invocations.
9019
+ */
9020
+ function invokedAsTopLevelAlias(name, argv = process.argv.slice(2)) {
9021
+ for (const token of argv) {
9022
+ if (token === "--") return false;
9023
+ if (!token.startsWith("-")) return token === name;
9024
+ }
9025
+ return false;
9026
+ }
9027
+ function registerMemoryGroupWrappers() {
9028
+ if (memoryCommand.commands.some((command) => command.name() === "export")) return;
9029
+ const forwardedTail = (subcommand) => {
9030
+ const argv = process.argv.slice(2);
9031
+ const groupIdx = argv.indexOf("memory");
9032
+ const searchFrom = groupIdx >= 0 ? groupIdx + 1 : 0;
9033
+ const subIdx = argv.indexOf(subcommand, searchFrom);
9034
+ return subIdx >= 0 ? argv.slice(subIdx + 1) : [];
9035
+ };
9036
+ const memExport = new Command("export").description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--project <project>", "filter by project").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp").configureHelp(helpStyle).allowUnknownOption(true).allowExcessArguments(true).action(async () => {
9037
+ await exportMemoriesCommand.parseAsync([
9038
+ "node",
9039
+ "export-memories",
9040
+ ...forwardedTail("export")
9041
+ ]);
9042
+ });
9043
+ const memImport = new Command("import").description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing").configureHelp(helpStyle).allowUnknownOption(true).allowExcessArguments(true).action(async () => {
9044
+ await importMemoriesCommand.parseAsync([
9045
+ "node",
9046
+ "import-memories",
9047
+ ...forwardedTail("import")
9048
+ ]);
9049
+ });
9050
+ memoryCommand.addCommand(memExport);
9051
+ memoryCommand.addCommand(memImport);
9052
+ }
9053
+ /** Register every root command (visible and hidden) on the given program. */
9054
+ function registerRootCommands(program) {
9055
+ registerMemoryGroupWrappers();
9056
+ program.addCommand(serveCommand);
9057
+ program.addCommand(configCommand);
9058
+ program.addCommand(coordinatorCommand);
9059
+ program.addCommand(mcpCommand);
9060
+ program.addCommand(claudeHookInjectCommand);
9061
+ program.addCommand(claudeHookIngestCommand);
9062
+ program.addCommand(claudeHookFileContextCommand);
9063
+ program.addCommand(codexHookInjectCommand);
9064
+ program.addCommand(codexHookIngestCommand);
9065
+ program.addCommand(dbCommand);
9066
+ program.addCommand(distillCommand);
9067
+ program.addCommand(exportMemoriesCommand);
9068
+ program.addCommand(importMemoriesCommand);
9069
+ program.addCommand(statsCommand);
9070
+ program.addCommand(statusCommand);
9071
+ program.addCommand(maintenanceCommand);
9072
+ program.addCommand(embedCommand);
9073
+ program.addCommand(recentCommand);
9074
+ program.addCommand(searchCommand);
9075
+ program.addCommand(packCommand);
9076
+ program.addCommand(promptPackLedgerCommand, { hidden: true });
9077
+ program.addCommand(showMemoryCommand, { hidden: true });
9078
+ program.addCommand(forgetMemoryCommand, { hidden: true });
9079
+ program.addCommand(rememberMemoryCommand, { hidden: true });
9080
+ program.addCommand(memoryCommand);
9081
+ program.addCommand(syncCommand);
9082
+ program.addCommand(setupCommand);
9083
+ program.addCommand(enqueueRawEventCommand);
9084
+ program.addCommand(updateCommand);
9085
+ program.addCommand(versionCommand);
9086
+ return program;
9087
+ }
9088
+ //#endregion
8240
9089
  //#region src/index.ts
8241
9090
  /**
8242
9091
  * @codemem/cli — CLI entry point.
@@ -8250,34 +9099,7 @@ var versionCommand = new Command("version").configureHelp(helpStyle).description
8250
9099
  */
8251
9100
  var completion = omelette("codemem <command>");
8252
9101
  completion.on("command", ({ reply }) => {
8253
- reply([
8254
- "claude-hook-file-context",
8255
- "claude-hook-inject",
8256
- "claude-hook-ingest",
8257
- "codex-hook-inject",
8258
- "codex-hook-ingest",
8259
- "config",
8260
- "coordinator",
8261
- "db",
8262
- "distill",
8263
- "embed",
8264
- "enqueue-raw-event",
8265
- "export-memories",
8266
- "import-memories",
8267
- "mcp",
8268
- "memory",
8269
- "pack",
8270
- "recent",
8271
- "search",
8272
- "serve",
8273
- "setup",
8274
- "stats",
8275
- "sync",
8276
- "version",
8277
- "help",
8278
- "--help",
8279
- "--version"
8280
- ]);
9102
+ reply([...ROOT_COMPLETION_COMMANDS]);
8281
9103
  });
8282
9104
  completion.init();
8283
9105
  function hasRootFlag(flag) {
@@ -8306,59 +9128,7 @@ if (hasRootFlag("--cleanup-completion")) {
8306
9128
  completion.cleanupShellInitFile();
8307
9129
  process.exit(0);
8308
9130
  }
8309
- {
8310
- const forwardedTail = (subcommand) => {
8311
- const argv = process.argv.slice(2);
8312
- const groupIdx = argv.indexOf("memory");
8313
- const searchFrom = groupIdx >= 0 ? groupIdx + 1 : 0;
8314
- const subIdx = argv.indexOf(subcommand, searchFrom);
8315
- return subIdx >= 0 ? argv.slice(subIdx + 1) : [];
8316
- };
8317
- const memExport = new Command("export").description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--project <project>", "filter by project").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp").configureHelp(helpStyle).allowUnknownOption(true).allowExcessArguments(true).action(async () => {
8318
- await exportMemoriesCommand.parseAsync([
8319
- "node",
8320
- "export-memories",
8321
- ...forwardedTail("export")
8322
- ]);
8323
- });
8324
- const memImport = new Command("import").description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing").configureHelp(helpStyle).allowUnknownOption(true).allowExcessArguments(true).action(async () => {
8325
- await importMemoriesCommand.parseAsync([
8326
- "node",
8327
- "import-memories",
8328
- ...forwardedTail("import")
8329
- ]);
8330
- });
8331
- memoryCommand.addCommand(memExport);
8332
- memoryCommand.addCommand(memImport);
8333
- }
8334
- program.addCommand(serveCommand);
8335
- program.addCommand(configCommand);
8336
- program.addCommand(coordinatorCommand);
8337
- program.addCommand(mcpCommand);
8338
- program.addCommand(claudeHookInjectCommand);
8339
- program.addCommand(claudeHookIngestCommand);
8340
- program.addCommand(claudeHookFileContextCommand);
8341
- program.addCommand(codexHookInjectCommand);
8342
- program.addCommand(codexHookIngestCommand);
8343
- program.addCommand(dbCommand);
8344
- program.addCommand(distillCommand);
8345
- program.addCommand(exportMemoriesCommand);
8346
- program.addCommand(importMemoriesCommand);
8347
- program.addCommand(statsCommand);
8348
- program.addCommand(maintenanceCommand);
8349
- program.addCommand(embedCommand);
8350
- program.addCommand(recentCommand);
8351
- program.addCommand(searchCommand);
8352
- program.addCommand(packCommand);
8353
- program.addCommand(promptPackLedgerCommand, { hidden: true });
8354
- program.addCommand(showMemoryCommand, { hidden: true });
8355
- program.addCommand(forgetMemoryCommand, { hidden: true });
8356
- program.addCommand(rememberMemoryCommand, { hidden: true });
8357
- program.addCommand(memoryCommand);
8358
- program.addCommand(syncCommand);
8359
- program.addCommand(setupCommand);
8360
- program.addCommand(enqueueRawEventCommand);
8361
- program.addCommand(versionCommand);
9131
+ registerRootCommands(program);
8362
9132
  program.parse();
8363
9133
  //#endregion
8364
9134
  export {};