codemem 0.33.0 → 0.35.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,12 +1,12 @@
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, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, 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, ensureDeviceIdentity, ensureSchemaBootstrapped, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, listMaintenanceJobs, listPerPeerScopeSyncState, loadObserverConfig, loadPublicKey, loadSqliteVec, mdnsEnabled, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
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, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, 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, ensureDeviceIdentity, ensureSchemaBootstrapped, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, listMaintenanceJobs, listPerPeerScopeSyncState, loadObserverConfig, loadPublicKey, loadSqliteVec, mdnsEnabled, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import { homedir, networkInterfaces } from "node:os";
7
7
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { styleText } from "node:util";
9
- import { randomInt } from "node:crypto";
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 { spawn, spawnSync } from "node:child_process";
@@ -101,7 +101,7 @@ var BOOLEAN_TOGGLE_VALUES = new Set([
101
101
  "on",
102
102
  "no"
103
103
  ]);
104
- function expandHome$3(value) {
104
+ function expandHome$4(value) {
105
105
  const home = process.env.HOME?.trim() || homedir();
106
106
  if (value === "~") return home;
107
107
  if (value.startsWith("~/")) return join(home, value.slice(2));
@@ -110,8 +110,8 @@ function expandHome$3(value) {
110
110
  function pluginLogPath() {
111
111
  const raw = process.env.CODEMEM_PLUGIN_LOG_PATH ?? process.env.CODEMEM_PLUGIN_LOG ?? "";
112
112
  const normalized = raw.trim().toLowerCase();
113
- if (BOOLEAN_TOGGLE_VALUES.has(normalized)) return expandHome$3("~/.codemem/plugin.log");
114
- return expandHome$3(raw.trim());
113
+ if (BOOLEAN_TOGGLE_VALUES.has(normalized)) return expandHome$4("~/.codemem/plugin.log");
114
+ return expandHome$4(raw.trim());
115
115
  }
116
116
  /**
117
117
  * Append a single timestamped line to the plugin log. Best-effort: any
@@ -146,24 +146,24 @@ var KIND_ICONS = {
146
146
  change: "✅",
147
147
  exploration: "🔬"
148
148
  };
149
- function emitJson$1(value) {
149
+ function emitJson$2(value) {
150
150
  console.log(JSON.stringify(value));
151
151
  }
152
- function emitError$1(value) {
152
+ function emitError$2(value) {
153
153
  process.stderr.write(`${JSON.stringify(value)}\n`);
154
154
  }
155
- function continueResult$1() {
155
+ function continueResult$2() {
156
156
  return { continue: true };
157
157
  }
158
- function envNotDisabled$1(value) {
158
+ function envNotDisabled$2(value) {
159
159
  const normalized = String(value ?? "").trim().toLowerCase();
160
160
  return normalized !== "0" && normalized !== "false" && normalized !== "off";
161
161
  }
162
- function envTruthy$2(value) {
162
+ function envTruthy$3(value) {
163
163
  const normalized = String(value ?? "").trim().toLowerCase();
164
164
  return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
165
165
  }
166
- function expandHome$2(value) {
166
+ function expandHome$3(value) {
167
167
  if (value === "~") return homedir();
168
168
  if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
169
169
  return value;
@@ -288,30 +288,30 @@ function resolveProject$1(payload) {
288
288
  return resolveHookProject(typeof payload.cwd === "string" ? payload.cwd : null, payload.project);
289
289
  }
290
290
  async function buildClaudeFileContext(payload, opts, deps = {}) {
291
- if (envTruthy$2(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$1();
292
- if (!envNotDisabled$1(process.env.CODEMEM_FILE_CONTEXT || "1")) return continueResult$1();
291
+ if (envTruthy$3(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$2();
292
+ if (!envNotDisabled$2(process.env.CODEMEM_FILE_CONTEXT || "1")) return continueResult$2();
293
293
  const filePath = extractFilePath(payload);
294
- if (!filePath) return continueResult$1();
294
+ if (!filePath) return continueResult$2();
295
295
  const cwd = typeof payload.cwd === "string" && payload.cwd.trim() ? payload.cwd : process.cwd();
296
- const expandedPath = expandHome$2(filePath);
296
+ const expandedPath = expandHome$3(filePath);
297
297
  const absolutePath = isAbsolute(expandedPath) ? expandedPath : resolve(cwd, expandedPath);
298
298
  const relativePath = relative(cwd, absolutePath).split(sep).join("/");
299
299
  const escapesCwd = relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath);
300
300
  if (!relativePath || escapesCwd) {
301
301
  logHookEvent(`file_context.skip reason=outside_cwd path=${JSON.stringify(filePath)} cwd=${JSON.stringify(cwd)}`);
302
- return continueResult$1();
302
+ return continueResult$2();
303
303
  }
304
304
  const minBytes = Number.parseInt(process.env.CODEMEM_FILE_CONTEXT_MIN_BYTES ?? `${FILE_GATE_MIN_BYTES}`, 10);
305
305
  const minBytesEffective = Number.isFinite(minBytes) && minBytes >= 0 ? minBytes : FILE_GATE_MIN_BYTES;
306
306
  const stat = (deps.statFile ?? statFile)(absolutePath);
307
307
  if (!stat) {
308
308
  logHookEvent(`file_context.skip reason=stat_failed path=${JSON.stringify(relativePath)}`);
309
- return continueResult$1();
309
+ return continueResult$2();
310
310
  }
311
311
  const bypassSizeGate = SMALL_FILE_BYPASS_PATTERNS.some((p) => p.test(relativePath));
312
312
  if (stat.sizeBytes < minBytesEffective && !bypassSizeGate) {
313
313
  logHookEvent(`file_context.skip reason=below_size_gate path=${JSON.stringify(relativePath)} size=${stat.sizeBytes} gate=${minBytesEffective}`);
314
- return continueResult$1();
314
+ return continueResult$2();
315
315
  }
316
316
  const project = resolveProject$1(payload);
317
317
  const resolveDb = deps.resolveDb ?? resolveDbPath;
@@ -321,16 +321,16 @@ async function buildClaudeFileContext(payload, opts, deps = {}) {
321
321
  rows = queryFn(resolveDb(resolveDbOpt(opts)), relativePath, project, FETCH_LIMIT);
322
322
  } catch (err) {
323
323
  logHookEvent(`codemem claude-hook-file-context query failed: ${err instanceof Error ? err.message : String(err)}`);
324
- return continueResult$1();
324
+ return continueResult$2();
325
325
  }
326
326
  if (rows.length === 0) {
327
327
  logHookEvent(`file_context.skip reason=no_observations path=${JSON.stringify(relativePath)} project=${JSON.stringify(project ?? "")}`);
328
- return continueResult$1();
328
+ return continueResult$2();
329
329
  }
330
330
  const top = scoreAndDedupe(rows, relativePath, DISPLAY_LIMIT);
331
331
  if (top.length === 0) {
332
332
  logHookEvent(`file_context.skip reason=no_top_after_dedupe path=${JSON.stringify(relativePath)} candidates=${rows.length}`);
333
- return continueResult$1();
333
+ return continueResult$2();
334
334
  }
335
335
  let staleness = null;
336
336
  if (stat.mtimeMs > 0) {
@@ -358,14 +358,14 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
358
358
  for await (const chunk of process.stdin) raw += String(chunk);
359
359
  const trimmed = raw.trim();
360
360
  if (!trimmed) {
361
- emitJson$1(continueResult$1());
361
+ emitJson$2(continueResult$2());
362
362
  return;
363
363
  }
364
364
  let payload;
365
365
  try {
366
366
  const parsed = JSON.parse(trimmed);
367
367
  if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
368
- emitError$1({
368
+ emitError$2({
369
369
  error: "parse_error",
370
370
  message: "payload must be a JSON object"
371
371
  });
@@ -374,14 +374,14 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
374
374
  }
375
375
  payload = parsed;
376
376
  } catch {
377
- emitError$1({
377
+ emitError$2({
378
378
  error: "parse_error",
379
379
  message: "invalid JSON"
380
380
  });
381
381
  process.exitCode = 1;
382
382
  return;
383
383
  }
384
- emitJson$1(await buildClaudeFileContext(payload, opts));
384
+ emitJson$2(await buildClaudeFileContext(payload, opts));
385
385
  });
386
386
  //#endregion
387
387
  //#region src/commands/claude-hook-ingest-spool.ts
@@ -391,28 +391,28 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
391
391
  * payloads when both HTTP and direct ingestion paths fail, and a
392
392
  * recovery routine that promotes stale temp files back into the queue.
393
393
  */
394
- var DEFAULT_LOCK_TTL_S = 300;
395
- var DEFAULT_LOCK_GRACE_S = 2;
396
- var LOCK_ACQUIRE_ATTEMPTS = 100;
397
- var LOCK_ACQUIRE_BACKOFF_MS = 50;
394
+ var DEFAULT_LOCK_TTL_S$1 = 300;
395
+ var DEFAULT_LOCK_GRACE_S$1 = 2;
396
+ var LOCK_ACQUIRE_ATTEMPTS$1 = 100;
397
+ var LOCK_ACQUIRE_BACKOFF_MS$1 = 50;
398
398
  var LockBusyError = class extends Error {
399
399
  constructor() {
400
400
  super("claude-hook-ingest lock busy");
401
401
  this.name = "LockBusyError";
402
402
  }
403
403
  };
404
- function expandHome$1(value) {
404
+ function expandHome$2(value) {
405
405
  if (value === "~") return homedir();
406
406
  if (value.startsWith("~/")) return join(homedir(), value.slice(2));
407
407
  return value;
408
408
  }
409
- function envInt(name, fallback) {
409
+ function envInt$1(name, fallback) {
410
410
  const raw = process.env[name];
411
411
  if (raw === void 0) return fallback;
412
412
  const parsed = Number.parseInt(raw, 10);
413
413
  return Number.isFinite(parsed) ? parsed : fallback;
414
414
  }
415
- function envTruthy$1(name, fallback) {
415
+ function envTruthy$2(name, fallback) {
416
416
  const raw = process.env[name];
417
417
  if (raw === void 0) return fallback;
418
418
  const normalized = raw.trim().toLowerCase();
@@ -430,15 +430,15 @@ function envTruthy$1(name, fallback) {
430
430
  ].includes(normalized)) return false;
431
431
  return fallback;
432
432
  }
433
- function lockConfig() {
433
+ function lockConfig$1() {
434
434
  return {
435
- lockDir: expandHome$1(process.env.CODEMEM_CLAUDE_HOOK_LOCK_DIR?.trim() || "~/.codemem/claude-hook-ingest.lock"),
436
- ttlSeconds: Math.max(1, envInt("CODEMEM_CLAUDE_HOOK_LOCK_TTL_S", DEFAULT_LOCK_TTL_S)),
437
- graceSeconds: Math.max(1, envInt("CODEMEM_CLAUDE_HOOK_LOCK_GRACE_S", DEFAULT_LOCK_GRACE_S))
435
+ lockDir: expandHome$2(process.env.CODEMEM_CLAUDE_HOOK_LOCK_DIR?.trim() || "~/.codemem/claude-hook-ingest.lock"),
436
+ ttlSeconds: Math.max(1, envInt$1("CODEMEM_CLAUDE_HOOK_LOCK_TTL_S", DEFAULT_LOCK_TTL_S$1)),
437
+ graceSeconds: Math.max(1, envInt$1("CODEMEM_CLAUDE_HOOK_LOCK_GRACE_S", DEFAULT_LOCK_GRACE_S$1))
438
438
  };
439
439
  }
440
440
  function spoolDir() {
441
- return expandHome$1(process.env.CODEMEM_CLAUDE_HOOK_SPOOL_DIR?.trim() || "~/.codemem/claude-hook-spool");
441
+ return expandHome$2(process.env.CODEMEM_CLAUDE_HOOK_SPOOL_DIR?.trim() || "~/.codemem/claude-hook-spool");
442
442
  }
443
443
  /**
444
444
  * Cheap pre-check used by the unlocked HTTP-success path to decide
@@ -470,7 +470,7 @@ function readFileTrimmedOrEmpty(path) {
470
470
  return "";
471
471
  }
472
472
  }
473
- function readLockMetadata(lockDir) {
473
+ function readLockMetadata$1(lockDir) {
474
474
  const pid = readFileTrimmedOrEmpty(join(lockDir, "pid"));
475
475
  const owner = readFileTrimmedOrEmpty(join(lockDir, "owner"));
476
476
  const tsRaw = readFileTrimmedOrEmpty(join(lockDir, "ts"));
@@ -481,7 +481,7 @@ function readLockMetadata(lockDir) {
481
481
  owner
482
482
  };
483
483
  }
484
- function isPidAlive(pidText) {
484
+ function isPidAlive$1(pidText) {
485
485
  const pid = Number.parseInt(pidText, 10);
486
486
  if (!Number.isFinite(pid) || pid <= 0) return false;
487
487
  try {
@@ -491,11 +491,11 @@ function isPidAlive(pidText) {
491
491
  return false;
492
492
  }
493
493
  }
494
- function lockIsStale(cfg) {
495
- const snapshot = readLockMetadata(cfg.lockDir);
494
+ function lockIsStale$1(cfg) {
495
+ const snapshot = readLockMetadata$1(cfg.lockDir);
496
496
  const nowS = Math.floor(Date.now() / 1e3);
497
497
  if (snapshot.pid) {
498
- if (isPidAlive(snapshot.pid)) {
498
+ if (isPidAlive$1(snapshot.pid)) {
499
499
  if (snapshot.ts === null) return {
500
500
  stale: false,
501
501
  snapshot
@@ -528,7 +528,7 @@ function lockIsStale(cfg) {
528
528
  snapshot
529
529
  };
530
530
  }
531
- function cleanupLockDir(lockDir) {
531
+ function cleanupLockDir$1(lockDir) {
532
532
  for (const name of [
533
533
  "pid",
534
534
  "ts",
@@ -543,13 +543,13 @@ function cleanupLockDir(lockDir) {
543
543
  function snapshotsEqual(a, b) {
544
544
  return a.pid === b.pid && a.ts === b.ts && a.owner === b.owner;
545
545
  }
546
- function cleanupLockDirIfUnchanged(lockDir, snapshot) {
547
- if (snapshotsEqual(readLockMetadata(lockDir), snapshot)) cleanupLockDir(lockDir);
546
+ function cleanupLockDirIfUnchanged$1(lockDir, snapshot) {
547
+ if (snapshotsEqual(readLockMetadata$1(lockDir), snapshot)) cleanupLockDir$1(lockDir);
548
548
  }
549
- function isErrnoException(err) {
549
+ function isErrnoException$1(err) {
550
550
  return typeof err === "object" && err !== null && "code" in err;
551
551
  }
552
- async function sleep(ms) {
552
+ async function sleep$1(ms) {
553
553
  return new Promise((resolve) => setTimeout(resolve, ms));
554
554
  }
555
555
  /**
@@ -563,21 +563,21 @@ async function sleep(ms) {
563
563
  * race between mkdir and writing pid/ts.
564
564
  */
565
565
  async function withClaudeHookIngestLock(fn) {
566
- const cfg = lockConfig();
566
+ const cfg = lockConfig$1();
567
567
  mkdirSync(dirname(cfg.lockDir), { recursive: true });
568
568
  const ownerToken = `${process.pid}-${Math.floor(Date.now() / 1e3)}-${randomInt(1e3, 1e4)}`;
569
569
  let acquired = false;
570
- for (let attempt = 0; attempt < LOCK_ACQUIRE_ATTEMPTS; attempt++) {
570
+ for (let attempt = 0; attempt < LOCK_ACQUIRE_ATTEMPTS$1; attempt++) {
571
571
  try {
572
572
  mkdirSync(cfg.lockDir);
573
573
  } catch (err) {
574
- if (isErrnoException(err) && err.code === "EEXIST") {
575
- const { stale, snapshot } = lockIsStale(cfg);
576
- if (stale) cleanupLockDirIfUnchanged(cfg.lockDir, snapshot);
577
- await sleep(LOCK_ACQUIRE_BACKOFF_MS);
574
+ if (isErrnoException$1(err) && err.code === "EEXIST") {
575
+ const { stale, snapshot } = lockIsStale$1(cfg);
576
+ if (stale) cleanupLockDirIfUnchanged$1(cfg.lockDir, snapshot);
577
+ await sleep$1(LOCK_ACQUIRE_BACKOFF_MS$1);
578
578
  continue;
579
579
  }
580
- await sleep(LOCK_ACQUIRE_BACKOFF_MS);
580
+ await sleep$1(LOCK_ACQUIRE_BACKOFF_MS$1);
581
581
  continue;
582
582
  }
583
583
  try {
@@ -587,15 +587,15 @@ async function withClaudeHookIngestLock(fn) {
587
587
  acquired = true;
588
588
  break;
589
589
  } catch {
590
- cleanupLockDir(cfg.lockDir);
591
- await sleep(LOCK_ACQUIRE_BACKOFF_MS);
590
+ cleanupLockDir$1(cfg.lockDir);
591
+ await sleep$1(LOCK_ACQUIRE_BACKOFF_MS$1);
592
592
  }
593
593
  }
594
594
  if (!acquired) throw new LockBusyError();
595
595
  try {
596
596
  return await fn();
597
597
  } finally {
598
- if (readFileTrimmedOrEmpty(join(cfg.lockDir, "owner")) === ownerToken) cleanupLockDir(cfg.lockDir);
598
+ if (readFileTrimmedOrEmpty(join(cfg.lockDir, "owner")) === ownerToken) cleanupLockDir$1(cfg.lockDir);
599
599
  }
600
600
  }
601
601
  /**
@@ -676,7 +676,7 @@ function recoverStaleTmpSpool(ttlSeconds) {
676
676
  * place with a `.bad-<reason>-` prefix so an operator can inspect or
677
677
  * delete it manually.
678
678
  */
679
- function quarantineSpoolEntry(dir, name, reason) {
679
+ function quarantineSpoolEntry$1(dir, name, reason) {
680
680
  const sourcePath = join(dir, name);
681
681
  const quarantineName = `.bad-${reason}-${Date.now()}-${randomInt(1e3, 1e4)}-${name}`;
682
682
  try {
@@ -735,12 +735,12 @@ async function drainSpool(handler) {
735
735
  try {
736
736
  parsed = JSON.parse(raw);
737
737
  } catch {
738
- quarantineSpoolEntry(dir, name, "parse-error");
738
+ quarantineSpoolEntry$1(dir, name, "parse-error");
739
739
  result.failed++;
740
740
  continue;
741
741
  }
742
742
  if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
743
- quarantineSpoolEntry(dir, name, "wrong-shape");
743
+ quarantineSpoolEntry$1(dir, name, "wrong-shape");
744
744
  continue;
745
745
  }
746
746
  let ok = false;
@@ -769,9 +769,9 @@ async function drainSpool(handler) {
769
769
  function shouldForceBoundaryFlush(payload) {
770
770
  const eventName = typeof payload.hook_event_name === "string" ? payload.hook_event_name.trim() : "";
771
771
  if (eventName !== "Stop" && eventName !== "SessionEnd") return false;
772
- if (eventName === "SessionEnd") return envTruthy$1("CODEMEM_CLAUDE_HOOK_FLUSH", true);
773
- if (!envTruthy$1("CODEMEM_CLAUDE_HOOK_FLUSH", false)) return false;
774
- return envTruthy$1("CODEMEM_CLAUDE_HOOK_FLUSH_ON_STOP", false);
772
+ if (eventName === "SessionEnd") return envTruthy$2("CODEMEM_CLAUDE_HOOK_FLUSH", true);
773
+ if (!envTruthy$2("CODEMEM_CLAUDE_HOOK_FLUSH", false)) return false;
774
+ return envTruthy$2("CODEMEM_CLAUDE_HOOK_FLUSH_ON_STOP", false);
775
775
  }
776
776
  /**
777
777
  * Returns the configured lock TTL so callers (`claude-hook-ingest`)
@@ -779,7 +779,7 @@ function shouldForceBoundaryFlush(payload) {
779
779
  * the env.
780
780
  */
781
781
  function lockTtlSeconds() {
782
- return lockConfig().ttlSeconds;
782
+ return lockConfig$1().ttlSeconds;
783
783
  }
784
784
  //#endregion
785
785
  //#region src/commands/claude-hook-session-state.ts
@@ -812,14 +812,14 @@ function defaultSessionState() {
812
812
  updated_at: ""
813
813
  };
814
814
  }
815
- function expandHome(value) {
815
+ function expandHome$1(value) {
816
816
  if (value === "~") return homedir();
817
817
  if (value.startsWith("~/")) return join(homedir(), value.slice(2));
818
818
  return value;
819
819
  }
820
820
  function contextDir() {
821
821
  const override = process.env.CODEMEM_CLAUDE_HOOK_CONTEXT_DIR;
822
- return expandHome(override?.trim() ? override : "~/.codemem/claude-hook-context");
822
+ return expandHome$1(override?.trim() ? override : "~/.codemem/claude-hook-context");
823
823
  }
824
824
  function sessionFileStem(sessionId) {
825
825
  const trimmed = sessionId.trim();
@@ -1014,7 +1014,7 @@ function workingSetPathsFromState(state) {
1014
1014
  * echo '{"hook_event_name":"Stop","session_id":"...","last_assistant_message":"..."}' \
1015
1015
  * | codemem claude-hook-ingest
1016
1016
  */
1017
- function emitStructuredError$1(errorCode, message) {
1017
+ function emitStructuredError$2(errorCode, message) {
1018
1018
  console.log(JSON.stringify({
1019
1019
  error: errorCode,
1020
1020
  message
@@ -1037,7 +1037,7 @@ function emitStructuredError$1(errorCode, message) {
1037
1037
  * transient, we'll need a reason field in the response and updated
1038
1038
  * client handling — not an unconditional fail-over.
1039
1039
  */
1040
- async function tryHttpIngest(payload, host, port) {
1040
+ async function tryHttpIngest$1(payload, host, port) {
1041
1041
  const url = `http://${host}:${port}/api/claude-hooks`;
1042
1042
  const controller = new AbortController();
1043
1043
  const timeout = setTimeout(() => controller.abort(), 5e3);
@@ -1191,7 +1191,7 @@ async function flushBoundaryRawEvents(payload, dbPath) {
1191
1191
  * disk spool durability.
1192
1192
  */
1193
1193
  async function ingestClaudeHookPayload(payload, opts, deps = {}) {
1194
- const httpIngest = deps.httpIngest ?? tryHttpIngest;
1194
+ const httpIngest = deps.httpIngest ?? tryHttpIngest$1;
1195
1195
  const directIngest = deps.directIngest ?? directEnqueue;
1196
1196
  const resolveDb = deps.resolveDb ?? resolveDbPath;
1197
1197
  const boundaryFlush = deps.boundaryFlush ?? flushBoundaryRawEvents;
@@ -1305,84 +1305,84 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
1305
1305
  var claudeHookCmd = new Command("claude-hook-ingest").configureHelp(helpStyle).description("Ingest Claude hook payload: HTTP first, direct DB fallback");
1306
1306
  addDbOption(claudeHookCmd);
1307
1307
  addViewerHostOptions(claudeHookCmd);
1308
- function envTruthyValue(value) {
1308
+ function envTruthyValue$1(value) {
1309
1309
  const normalized = String(value ?? "").trim().toLowerCase();
1310
1310
  return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
1311
1311
  }
1312
1312
  var claudeHookIngestCommand = claudeHookCmd.action(async (opts) => {
1313
- if (envTruthyValue(process.env.CODEMEM_PLUGIN_IGNORE)) return;
1313
+ if (envTruthyValue$1(process.env.CODEMEM_PLUGIN_IGNORE)) return;
1314
1314
  let raw;
1315
1315
  try {
1316
1316
  raw = readFileSync(0, "utf8").trim();
1317
1317
  } catch {
1318
- emitStructuredError$1("read_error", "failed to read stdin");
1318
+ emitStructuredError$2("read_error", "failed to read stdin");
1319
1319
  return;
1320
1320
  }
1321
1321
  if (!raw) {
1322
- emitStructuredError$1("read_error", "empty stdin");
1322
+ emitStructuredError$2("read_error", "empty stdin");
1323
1323
  return;
1324
1324
  }
1325
1325
  let payload;
1326
1326
  try {
1327
1327
  const parsed = JSON.parse(raw);
1328
1328
  if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
1329
- emitStructuredError$1("parse_error", "payload must be a JSON object");
1329
+ emitStructuredError$2("parse_error", "payload must be a JSON object");
1330
1330
  return;
1331
1331
  }
1332
1332
  payload = parsed;
1333
1333
  } catch {
1334
- emitStructuredError$1("parse_error", "invalid JSON");
1334
+ emitStructuredError$2("parse_error", "invalid JSON");
1335
1335
  return;
1336
1336
  }
1337
1337
  try {
1338
1338
  const result = await ingestClaudeHookPayload(payload, opts);
1339
1339
  console.log(JSON.stringify(result));
1340
1340
  } catch (err) {
1341
- emitStructuredError$1("ingest_error", err instanceof Error ? err.message : String(err));
1341
+ emitStructuredError$2("ingest_error", err instanceof Error ? err.message : String(err));
1342
1342
  }
1343
1343
  });
1344
1344
  //#endregion
1345
1345
  //#region src/commands/claude-hook-inject.ts
1346
- var HOOK_EVENT_NAME = "UserPromptSubmit";
1347
- var EMPTY_PACK = {
1346
+ var HOOK_EVENT_NAME$1 = "UserPromptSubmit";
1347
+ var EMPTY_PACK$1 = {
1348
1348
  packText: "",
1349
1349
  items: 0,
1350
1350
  packTokens: 0
1351
1351
  };
1352
- var DEFAULT_VIEWER_HOST = "127.0.0.1";
1353
- var DEFAULT_VIEWER_PORT = 38888;
1354
- var DEFAULT_MAX_CHARS = 16e3;
1355
- var DEFAULT_HTTP_MAX_TIME_S = 2;
1356
- function emitJson(value) {
1352
+ var DEFAULT_VIEWER_HOST$1 = "127.0.0.1";
1353
+ var DEFAULT_VIEWER_PORT$1 = 38888;
1354
+ var DEFAULT_MAX_CHARS$1 = 16e3;
1355
+ var DEFAULT_HTTP_MAX_TIME_S$1 = 2;
1356
+ function emitJson$1(value) {
1357
1357
  console.log(JSON.stringify(value));
1358
1358
  }
1359
- function emitError(value) {
1359
+ function emitError$1(value) {
1360
1360
  process.stderr.write(`${JSON.stringify(value)}\n`);
1361
1361
  }
1362
- function envNotDisabled(value) {
1362
+ function envNotDisabled$1(value) {
1363
1363
  const normalized = String(value ?? "").trim().toLowerCase();
1364
1364
  return normalized !== "0" && normalized !== "false" && normalized !== "off";
1365
1365
  }
1366
- function envTruthy(value) {
1366
+ function envTruthy$1(value) {
1367
1367
  const normalized = String(value ?? "").trim().toLowerCase();
1368
1368
  return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
1369
1369
  }
1370
- function parsePositiveInt$1(value, fallback) {
1370
+ function parsePositiveInt$2(value, fallback) {
1371
1371
  const parsed = Number.parseInt(String(value ?? ""), 10);
1372
1372
  if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
1373
1373
  return parsed;
1374
1374
  }
1375
- function continueResult(additionalContext) {
1375
+ function continueResult$1(additionalContext) {
1376
1376
  if (!additionalContext) return { continue: true };
1377
1377
  return {
1378
1378
  continue: true,
1379
1379
  hookSpecificOutput: {
1380
- hookEventName: HOOK_EVENT_NAME,
1380
+ hookEventName: HOOK_EVENT_NAME$1,
1381
1381
  additionalContext
1382
1382
  }
1383
1383
  };
1384
1384
  }
1385
- function truncateAdditionalContext(text, maxChars) {
1385
+ function truncateAdditionalContext$1(text, maxChars) {
1386
1386
  const normalized = text.trim();
1387
1387
  if (!normalized) return "";
1388
1388
  if (!Number.isFinite(maxChars) || maxChars <= 0 || normalized.length <= maxChars) return normalized;
@@ -1391,14 +1391,14 @@ function truncateAdditionalContext(text, maxChars) {
1391
1391
  function extractInjectContext(payload) {
1392
1392
  return normalizePromptText(payload.prompt) || null;
1393
1393
  }
1394
- function resolveInjectProject(payload) {
1394
+ function resolveInjectProject$1(payload) {
1395
1395
  return resolveHookProject(typeof payload.cwd === "string" ? payload.cwd : null, payload.project);
1396
1396
  }
1397
- async function buildLocalPack(context, project, dbPath, workingSetPaths = []) {
1397
+ async function buildLocalPack$1(context, project, dbPath, workingSetPaths = []) {
1398
1398
  const store = new MemoryStore(dbPath);
1399
1399
  try {
1400
- const limit = parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8);
1401
- const budget = parsePositiveInt$1(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
1400
+ const limit = parsePositiveInt$2(process.env.CODEMEM_INJECT_LIMIT, 8);
1401
+ const budget = parsePositiveInt$2(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
1402
1402
  const filters = {};
1403
1403
  if (project) filters.project = project;
1404
1404
  if (workingSetPaths.length > 0) filters.working_set_paths = workingSetPaths;
@@ -1412,18 +1412,18 @@ async function buildLocalPack(context, project, dbPath, workingSetPaths = []) {
1412
1412
  store.close();
1413
1413
  }
1414
1414
  }
1415
- async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S * 1e3) {
1415
+ async function tryHttpPack$1(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S$1 * 1e3) {
1416
1416
  const empty = {
1417
1417
  packText: "",
1418
1418
  items: 0,
1419
1419
  packTokens: 0
1420
1420
  };
1421
- const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
1422
- const port = parsePositiveInt$1(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
1421
+ const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST$1;
1422
+ const port = parsePositiveInt$2(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT$1);
1423
1423
  const url = new URL(`http://${host}:${port}/api/pack`);
1424
1424
  url.searchParams.set("context", context);
1425
- url.searchParams.set("limit", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8)));
1426
- url.searchParams.set("token_budget", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800)));
1425
+ url.searchParams.set("limit", String(parsePositiveInt$2(process.env.CODEMEM_INJECT_LIMIT, 8)));
1426
+ url.searchParams.set("token_budget", String(parsePositiveInt$2(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800)));
1427
1427
  if (project) url.searchParams.set("project", project);
1428
1428
  const controller = new AbortController();
1429
1429
  const timeout = setTimeout(() => controller.abort(), maxTimeMs);
@@ -1443,8 +1443,8 @@ async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S
1443
1443
  }
1444
1444
  }
1445
1445
  async function buildClaudeHookInjection(payload, opts, deps = {}) {
1446
- if (envTruthy(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult();
1447
- if (!envNotDisabled(process.env.CODEMEM_INJECT_CONTEXT || "1")) return continueResult();
1446
+ if (envTruthy$1(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$1();
1447
+ if (!envNotDisabled$1(process.env.CODEMEM_INJECT_CONTEXT || "1")) return continueResult$1();
1448
1448
  let state = null;
1449
1449
  try {
1450
1450
  state = trackHookSessionState(payload);
@@ -1452,20 +1452,20 @@ async function buildClaudeHookInjection(payload, opts, deps = {}) {
1452
1452
  state = null;
1453
1453
  }
1454
1454
  const promptText = extractInjectContext(payload);
1455
- if (!promptText) return continueResult();
1456
- const buildPack = deps.buildLocalPack ?? buildLocalPack;
1457
- const httpPack = deps.httpPack ?? tryHttpPack;
1455
+ if (!promptText) return continueResult$1();
1456
+ const buildPack = deps.buildLocalPack ?? buildLocalPack$1;
1457
+ const httpPack = deps.httpPack ?? tryHttpPack$1;
1458
1458
  const resolveDb = deps.resolveDb ?? resolveDbPath;
1459
- const project = resolveInjectProject(payload);
1459
+ const project = resolveInjectProject$1(payload);
1460
1460
  const query = buildInjectQuery({
1461
1461
  prompt: promptText,
1462
1462
  project,
1463
1463
  state
1464
1464
  });
1465
1465
  const workingSetPaths = workingSetPathsFromState(state);
1466
- const maxChars = parsePositiveInt$1(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS);
1467
- const httpMaxTimeMs = parsePositiveInt$1(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S) * 1e3;
1468
- let pack = EMPTY_PACK;
1466
+ const maxChars = parsePositiveInt$2(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS$1);
1467
+ const httpMaxTimeMs = parsePositiveInt$2(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S$1) * 1e3;
1468
+ let pack = EMPTY_PACK$1;
1469
1469
  let origin = "none";
1470
1470
  try {
1471
1471
  pack = await buildPack(query, project, resolveDb(resolveDbOpt(opts)), workingSetPaths);
@@ -1473,7 +1473,7 @@ async function buildClaudeHookInjection(payload, opts, deps = {}) {
1473
1473
  } catch (err) {
1474
1474
  logHookEvent(`codemem claude-hook-inject local pack failed: ${err instanceof Error ? err.message : String(err)}`);
1475
1475
  }
1476
- if (!pack.packText && envNotDisabled(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) {
1476
+ if (!pack.packText && envNotDisabled$1(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) {
1477
1477
  pack = await httpPack(query, project, httpMaxTimeMs);
1478
1478
  if (pack.packText) origin = "http";
1479
1479
  }
@@ -1488,7 +1488,7 @@ async function buildClaudeHookInjection(payload, opts, deps = {}) {
1488
1488
  ];
1489
1489
  if (project) fields.push(`project=${JSON.stringify(project)}`);
1490
1490
  logHookEvent(fields.join(" "));
1491
- return continueResult(truncateAdditionalContext(pack.packText, maxChars));
1491
+ return continueResult$1(truncateAdditionalContext$1(pack.packText, maxChars));
1492
1492
  }
1493
1493
  var claudeHookInjectCmd = new Command("claude-hook-inject").configureHelp(helpStyle).description("Return Claude hook additionalContext from local pack generation");
1494
1494
  addDbOption(claudeHookInjectCmd);
@@ -1497,14 +1497,14 @@ var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
1497
1497
  for await (const chunk of process.stdin) raw += String(chunk);
1498
1498
  const trimmed = raw.trim();
1499
1499
  if (!trimmed) {
1500
- emitJson(continueResult());
1500
+ emitJson$1(continueResult$1());
1501
1501
  return;
1502
1502
  }
1503
1503
  let payload;
1504
1504
  try {
1505
1505
  const parsed = JSON.parse(trimmed);
1506
1506
  if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
1507
- emitError({
1507
+ emitError$1({
1508
1508
  error: "parse_error",
1509
1509
  message: "payload must be a JSON object"
1510
1510
  });
@@ -1513,14 +1513,695 @@ var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
1513
1513
  }
1514
1514
  payload = parsed;
1515
1515
  } catch {
1516
- emitError({
1516
+ emitError$1({
1517
1517
  error: "parse_error",
1518
1518
  message: "invalid JSON"
1519
1519
  });
1520
1520
  process.exitCode = 1;
1521
1521
  return;
1522
1522
  }
1523
- emitJson(await buildClaudeHookInjection(payload, opts));
1523
+ emitJson$1(await buildClaudeHookInjection(payload, opts));
1524
+ });
1525
+ //#endregion
1526
+ //#region src/commands/codex-hook-ingest-spool.ts
1527
+ var DEFAULT_LOCK_TTL_S = 120;
1528
+ var DEFAULT_LOCK_GRACE_S = 2;
1529
+ var LOCK_ACQUIRE_ATTEMPTS = 20;
1530
+ var LOCK_ACQUIRE_BACKOFF_MS = 50;
1531
+ var CodexHookLockBusyError = class extends Error {
1532
+ constructor() {
1533
+ super("codex-hook-ingest lock busy");
1534
+ this.name = "CodexHookLockBusyError";
1535
+ }
1536
+ };
1537
+ function expandHome(value) {
1538
+ if (value === "~") return homedir();
1539
+ if (value.startsWith("~/")) return join(homedir(), value.slice(2));
1540
+ return value;
1541
+ }
1542
+ function envInt(name, fallback) {
1543
+ const parsed = Number.parseInt(process.env[name] ?? "", 10);
1544
+ return Number.isFinite(parsed) ? parsed : fallback;
1545
+ }
1546
+ function lockConfig() {
1547
+ return {
1548
+ lockDir: expandHome(process.env.CODEMEM_CODEX_HOOK_LOCK_DIR?.trim() || "~/.codemem/codex-hook-ingest.lock"),
1549
+ ttlSeconds: Math.max(1, envInt("CODEMEM_CODEX_HOOK_LOCK_TTL_S", DEFAULT_LOCK_TTL_S)),
1550
+ graceSeconds: Math.max(1, envInt("CODEMEM_CODEX_HOOK_LOCK_GRACE_S", DEFAULT_LOCK_GRACE_S))
1551
+ };
1552
+ }
1553
+ function codexHookSpoolDir() {
1554
+ return expandHome(process.env.CODEMEM_CODEX_HOOK_SPOOL_DIR?.trim() || "~/.codemem/codex-hook-spool");
1555
+ }
1556
+ function codexHookLockTtlSeconds() {
1557
+ return lockConfig().ttlSeconds;
1558
+ }
1559
+ function hasCodexHookSpooledEntries() {
1560
+ let entries;
1561
+ try {
1562
+ entries = readdirSync(codexHookSpoolDir());
1563
+ } catch {
1564
+ return false;
1565
+ }
1566
+ return entries.some((name) => name.endsWith(".json") && !name.startsWith(".hook-tmp-") && !name.startsWith(".bad-"));
1567
+ }
1568
+ function readTrimmed(path) {
1569
+ try {
1570
+ return readFileSync(path, "utf8").trim();
1571
+ } catch {
1572
+ return "";
1573
+ }
1574
+ }
1575
+ function readLockMetadata(lockDir) {
1576
+ const rawTs = readTrimmed(join(lockDir, "ts"));
1577
+ const ts = rawTs === "" ? null : Number.parseInt(rawTs, 10);
1578
+ return {
1579
+ pid: readTrimmed(join(lockDir, "pid")),
1580
+ ts: ts === null || !Number.isFinite(ts) ? null : ts,
1581
+ owner: readTrimmed(join(lockDir, "owner"))
1582
+ };
1583
+ }
1584
+ function isPidAlive(pidText) {
1585
+ const pid = Number.parseInt(pidText, 10);
1586
+ if (!Number.isFinite(pid) || pid <= 0) return false;
1587
+ try {
1588
+ process.kill(pid, 0);
1589
+ return true;
1590
+ } catch {
1591
+ return false;
1592
+ }
1593
+ }
1594
+ function lockIsStale(cfg) {
1595
+ const snapshot = readLockMetadata(cfg.lockDir);
1596
+ const nowS = Math.floor(Date.now() / 1e3);
1597
+ if (snapshot.pid) {
1598
+ if (isPidAlive(snapshot.pid)) return {
1599
+ stale: snapshot.ts !== null && nowS - snapshot.ts > cfg.ttlSeconds,
1600
+ snapshot
1601
+ };
1602
+ return {
1603
+ stale: true,
1604
+ snapshot
1605
+ };
1606
+ }
1607
+ if (snapshot.ts !== null) return {
1608
+ stale: nowS - snapshot.ts > cfg.graceSeconds,
1609
+ snapshot
1610
+ };
1611
+ try {
1612
+ return {
1613
+ stale: nowS - Math.floor(statSync(cfg.lockDir).mtimeMs / 1e3) > cfg.graceSeconds,
1614
+ snapshot
1615
+ };
1616
+ } catch {
1617
+ return {
1618
+ stale: true,
1619
+ snapshot
1620
+ };
1621
+ }
1622
+ }
1623
+ function cleanupLockDir(lockDir) {
1624
+ for (const name of [
1625
+ "pid",
1626
+ "ts",
1627
+ "owner"
1628
+ ]) try {
1629
+ unlinkSync(join(lockDir, name));
1630
+ } catch {}
1631
+ try {
1632
+ rmdirSync(lockDir);
1633
+ } catch {}
1634
+ }
1635
+ function cleanupLockDirIfUnchanged(lockDir, snapshot) {
1636
+ const current = readLockMetadata(lockDir);
1637
+ if (current.pid === snapshot.pid && current.ts === snapshot.ts && current.owner === snapshot.owner) cleanupLockDir(lockDir);
1638
+ }
1639
+ function isErrnoException(err) {
1640
+ return typeof err === "object" && err !== null && "code" in err;
1641
+ }
1642
+ function sleep(ms) {
1643
+ return new Promise((resolve) => setTimeout(resolve, ms));
1644
+ }
1645
+ async function withCodexHookIngestLock(fn) {
1646
+ const cfg = lockConfig();
1647
+ mkdirSync(dirname(cfg.lockDir), { recursive: true });
1648
+ const ownerToken = `${process.pid}-${Math.floor(Date.now() / 1e3)}-${randomInt(1e3, 1e4)}`;
1649
+ let acquired = false;
1650
+ for (let attempt = 0; attempt < LOCK_ACQUIRE_ATTEMPTS; attempt++) {
1651
+ try {
1652
+ mkdirSync(cfg.lockDir);
1653
+ } catch (err) {
1654
+ if (isErrnoException(err) && err.code === "EEXIST") {
1655
+ const { stale, snapshot } = lockIsStale(cfg);
1656
+ if (stale) cleanupLockDirIfUnchanged(cfg.lockDir, snapshot);
1657
+ }
1658
+ await sleep(LOCK_ACQUIRE_BACKOFF_MS);
1659
+ continue;
1660
+ }
1661
+ try {
1662
+ writeFileSync(join(cfg.lockDir, "ts"), String(Math.floor(Date.now() / 1e3)), "utf8");
1663
+ writeFileSync(join(cfg.lockDir, "pid"), String(process.pid), "utf8");
1664
+ writeFileSync(join(cfg.lockDir, "owner"), ownerToken, "utf8");
1665
+ acquired = true;
1666
+ break;
1667
+ } catch {
1668
+ cleanupLockDir(cfg.lockDir);
1669
+ await sleep(LOCK_ACQUIRE_BACKOFF_MS);
1670
+ }
1671
+ }
1672
+ if (!acquired) throw new CodexHookLockBusyError();
1673
+ try {
1674
+ return await fn();
1675
+ } finally {
1676
+ if (readTrimmed(join(cfg.lockDir, "owner")) === ownerToken) cleanupLockDir(cfg.lockDir);
1677
+ }
1678
+ }
1679
+ function spoolCodexHookPayload(payload) {
1680
+ const dir = codexHookSpoolDir();
1681
+ try {
1682
+ mkdirSync(dir, { recursive: true });
1683
+ } catch {
1684
+ logHookEvent("codemem codex-hook-ingest failed to create spool dir");
1685
+ return false;
1686
+ }
1687
+ const tmpPath = join(dir, `.hook-tmp-${process.pid}-${Date.now()}-${randomInt(1e3, 1e4)}.json`);
1688
+ try {
1689
+ writeFileSync(tmpPath, JSON.stringify(payload), "utf8");
1690
+ } catch {
1691
+ logHookEvent("codemem codex-hook-ingest failed to allocate spool temp file");
1692
+ return false;
1693
+ }
1694
+ const finalPath = join(dir, `hook-${Math.floor(Date.now() / 1e3)}-${process.pid}-${randomInt(1e3, 1e4)}.json`);
1695
+ try {
1696
+ renameSync(tmpPath, finalPath);
1697
+ } catch {
1698
+ try {
1699
+ unlinkSync(tmpPath);
1700
+ } catch {}
1701
+ logHookEvent("codemem codex-hook-ingest failed to spool payload");
1702
+ return false;
1703
+ }
1704
+ logHookEvent(`codemem codex-hook-ingest spooled payload: ${finalPath}`);
1705
+ return true;
1706
+ }
1707
+ function recoverStaleCodexHookTmpSpool(ttlSeconds) {
1708
+ const dir = codexHookSpoolDir();
1709
+ try {
1710
+ mkdirSync(dir, { recursive: true });
1711
+ } catch {
1712
+ return;
1713
+ }
1714
+ let entries;
1715
+ try {
1716
+ entries = readdirSync(dir);
1717
+ } catch {
1718
+ return;
1719
+ }
1720
+ const nowS = Date.now() / 1e3;
1721
+ for (const name of entries) {
1722
+ if (!name.startsWith(".hook-tmp-") || !name.endsWith(".json")) continue;
1723
+ const tmpPath = join(dir, name);
1724
+ try {
1725
+ if (nowS - statSync(tmpPath).mtimeMs / 1e3 <= ttlSeconds) continue;
1726
+ renameSync(tmpPath, join(dir, `hook-recovered-${Math.floor(nowS)}-${process.pid}-${randomInt(1e3, 1e4)}.json`));
1727
+ } catch {}
1728
+ }
1729
+ }
1730
+ function quarantineSpoolEntry(dir, name, reason) {
1731
+ try {
1732
+ renameSync(join(dir, name), join(dir, `.bad-${reason}-${Date.now()}-${randomInt(1e3, 1e4)}-${name}`));
1733
+ } catch {
1734
+ try {
1735
+ unlinkSync(join(dir, name));
1736
+ } catch {}
1737
+ }
1738
+ }
1739
+ async function drainCodexHookSpool(handler) {
1740
+ const dir = codexHookSpoolDir();
1741
+ try {
1742
+ mkdirSync(dir, { recursive: true });
1743
+ } catch {
1744
+ return {
1745
+ processed: 0,
1746
+ failed: 0
1747
+ };
1748
+ }
1749
+ let entries;
1750
+ try {
1751
+ entries = readdirSync(dir);
1752
+ } catch {
1753
+ return {
1754
+ processed: 0,
1755
+ failed: 0
1756
+ };
1757
+ }
1758
+ const result = {
1759
+ processed: 0,
1760
+ failed: 0
1761
+ };
1762
+ for (const name of entries.filter((entry) => entry.endsWith(".json") && !entry.startsWith(".hook-tmp-") && !entry.startsWith(".bad-")).sort()) {
1763
+ const path = join(dir, name);
1764
+ let parsed;
1765
+ try {
1766
+ parsed = JSON.parse(readFileSync(path, "utf8"));
1767
+ } catch {
1768
+ quarantineSpoolEntry(dir, name, "parse-error");
1769
+ result.failed++;
1770
+ continue;
1771
+ }
1772
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
1773
+ quarantineSpoolEntry(dir, name, "wrong-shape");
1774
+ result.failed++;
1775
+ continue;
1776
+ }
1777
+ let ok = false;
1778
+ try {
1779
+ ok = await handler(parsed);
1780
+ } catch {
1781
+ ok = false;
1782
+ }
1783
+ if (!ok) {
1784
+ result.failed++;
1785
+ continue;
1786
+ }
1787
+ try {
1788
+ unlinkSync(path);
1789
+ result.processed++;
1790
+ } catch {}
1791
+ }
1792
+ return result;
1793
+ }
1794
+ //#endregion
1795
+ //#region src/commands/codex-hook-ingest.ts
1796
+ /**
1797
+ * codemem codex-hook-ingest — read a single Codex hook payload from stdin and
1798
+ * enqueue it for raw-event processing.
1799
+ */
1800
+ var DEFAULT_HTTP_TIMEOUT_MS = 1e3;
1801
+ function httpTimeoutMs() {
1802
+ const parsed = Number.parseInt(process.env.CODEMEM_CODEX_HOOK_HTTP_TIMEOUT_MS ?? "", 10);
1803
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_HTTP_TIMEOUT_MS;
1804
+ }
1805
+ function emitStructuredError$1(errorCode, message) {
1806
+ console.log(JSON.stringify({
1807
+ error: errorCode,
1808
+ message
1809
+ }));
1810
+ process.exitCode = 1;
1811
+ }
1812
+ function envTruthyValue(value) {
1813
+ const normalized = String(value ?? "").trim().toLowerCase();
1814
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
1815
+ }
1816
+ function hasPayloadTimestamp(payload) {
1817
+ return typeof payload.timestamp === "string" && payload.timestamp.trim() !== "" || typeof payload.ts === "string" && payload.ts.trim() !== "";
1818
+ }
1819
+ function normalizePayloadForIngest(payload) {
1820
+ if (hasPayloadTimestamp(payload)) return payload;
1821
+ return {
1822
+ ...payload,
1823
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1824
+ codemem_generated_event_nonce: randomUUID()
1825
+ };
1826
+ }
1827
+ async function tryHttpIngest(payload, host, port) {
1828
+ const url = `http://${host}:${port}/api/codex-hooks`;
1829
+ const controller = new AbortController();
1830
+ const timeout = setTimeout(() => controller.abort(), httpTimeoutMs());
1831
+ try {
1832
+ const res = await fetch(url, {
1833
+ method: "POST",
1834
+ headers: { "Content-Type": "application/json" },
1835
+ body: JSON.stringify(payload),
1836
+ signal: controller.signal
1837
+ });
1838
+ if (!res.ok) return {
1839
+ ok: false,
1840
+ inserted: 0,
1841
+ skipped: 0
1842
+ };
1843
+ const body = await res.json();
1844
+ if (body == null || typeof body !== "object" || Array.isArray(body)) {
1845
+ logHookEvent("codemem codex-hook-ingest HTTP accepted with invalid response type");
1846
+ return {
1847
+ ok: false,
1848
+ inserted: 0,
1849
+ skipped: 0
1850
+ };
1851
+ }
1852
+ const obj = body;
1853
+ if (typeof obj.inserted !== "number" || typeof obj.skipped !== "number") {
1854
+ logHookEvent("codemem codex-hook-ingest HTTP accepted with unexpected response body");
1855
+ return {
1856
+ ok: false,
1857
+ inserted: 0,
1858
+ skipped: 0
1859
+ };
1860
+ }
1861
+ return {
1862
+ ok: true,
1863
+ inserted: obj.inserted,
1864
+ skipped: obj.skipped
1865
+ };
1866
+ } catch {
1867
+ return {
1868
+ ok: false,
1869
+ inserted: 0,
1870
+ skipped: 0
1871
+ };
1872
+ } finally {
1873
+ clearTimeout(timeout);
1874
+ }
1875
+ }
1876
+ function directEnqueueCodexHook(payload, dbPath) {
1877
+ const envelope = buildRawEventEnvelopeFromCodexHook(payload);
1878
+ if (!envelope) return {
1879
+ inserted: 0,
1880
+ skipped: 1
1881
+ };
1882
+ const db = connect(dbPath);
1883
+ try {
1884
+ try {
1885
+ loadSqliteVec(db);
1886
+ } catch {}
1887
+ ensureSchemaBootstrapped(db);
1888
+ const strippedPayload = stripPrivateObj(envelope.payload);
1889
+ if (db.prepare("SELECT 1 FROM raw_events WHERE source = ? AND stream_id = ? AND event_id = ? LIMIT 1").get(envelope.source, envelope.session_stream_id, envelope.event_id)) return {
1890
+ inserted: 0,
1891
+ skipped: 0
1892
+ };
1893
+ db.prepare(`INSERT INTO raw_events(
1894
+ source, stream_id, opencode_session_id, event_id, event_seq,
1895
+ event_type, ts_wall_ms, payload_json, created_at
1896
+ ) VALUES (?, ?, ?, ?, (
1897
+ SELECT COALESCE(MAX(event_seq), -1) + 1
1898
+ FROM raw_events WHERE source = ? AND stream_id = ?
1899
+ ), ?, ?, ?, datetime('now'))`).run(envelope.source, envelope.session_stream_id, envelope.opencode_session_id, envelope.event_id, envelope.source, envelope.session_stream_id, envelope.event_type, envelope.ts_wall_ms, JSON.stringify(strippedPayload));
1900
+ const maxSeqRow = db.prepare("SELECT COALESCE(MAX(event_seq), 0) AS max_seq FROM raw_events WHERE source = ? AND stream_id = ?").get(envelope.source, envelope.session_stream_id);
1901
+ db.prepare(`INSERT INTO raw_event_sessions(
1902
+ source, stream_id, opencode_session_id, cwd, project, started_at,
1903
+ last_seen_ts_wall_ms, last_received_event_seq, last_flushed_event_seq, updated_at
1904
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, -1, datetime('now'))
1905
+ ON CONFLICT(source, stream_id) DO UPDATE SET
1906
+ cwd = COALESCE(excluded.cwd, cwd),
1907
+ project = COALESCE(excluded.project, project),
1908
+ started_at = COALESCE(excluded.started_at, started_at),
1909
+ last_seen_ts_wall_ms = MAX(COALESCE(excluded.last_seen_ts_wall_ms, 0), COALESCE(last_seen_ts_wall_ms, 0)),
1910
+ last_received_event_seq = MAX(excluded.last_received_event_seq, last_received_event_seq),
1911
+ updated_at = datetime('now')`).run(envelope.source, envelope.session_stream_id, envelope.opencode_session_id, envelope.cwd, envelope.project, envelope.started_at, envelope.ts_wall_ms, maxSeqRow.max_seq);
1912
+ return {
1913
+ inserted: 1,
1914
+ skipped: 0
1915
+ };
1916
+ } finally {
1917
+ db.close();
1918
+ }
1919
+ }
1920
+ async function ingestCodexHookPayload(payload, opts, deps = {}) {
1921
+ const httpIngest = deps.httpIngest ?? tryHttpIngest;
1922
+ const directIngest = deps.directIngest ?? directEnqueueCodexHook;
1923
+ const resolveDb = deps.resolveDb ?? resolveDbPath;
1924
+ const port = typeof opts.port === "number" ? opts.port : Number.parseInt(opts.port, 10);
1925
+ const ingestPayload = normalizePayloadForIngest(payload);
1926
+ let cachedDbPath = null;
1927
+ const getDbPath = () => {
1928
+ if (cachedDbPath === null) cachedDbPath = resolveDb(resolveDbOpt(opts));
1929
+ return cachedDbPath;
1930
+ };
1931
+ const tryDirectFallback = (queuedPayload) => {
1932
+ try {
1933
+ directIngest(queuedPayload, getDbPath());
1934
+ return true;
1935
+ } catch (err) {
1936
+ logHookEvent(`codemem codex-hook-ingest direct fallback failed: ${err instanceof Error ? err.message : String(err)}`);
1937
+ return false;
1938
+ }
1939
+ };
1940
+ const drainBacklogIfPresent = async () => {
1941
+ if (!hasCodexHookSpooledEntries()) return;
1942
+ try {
1943
+ await withCodexHookIngestLock(async () => {
1944
+ recoverStaleCodexHookTmpSpool(codexHookLockTtlSeconds());
1945
+ await drainCodexHookSpool(async (queuedPayload) => {
1946
+ return (await httpIngest(queuedPayload, opts.host, port)).ok || tryDirectFallback(queuedPayload);
1947
+ });
1948
+ });
1949
+ } catch (err) {
1950
+ if (err instanceof CodexHookLockBusyError) return;
1951
+ logHookEvent(`codemem codex-hook-ingest backlog drain failed: ${err instanceof Error ? err.message : String(err)}`);
1952
+ }
1953
+ };
1954
+ const httpResult = await httpIngest(ingestPayload, opts.host, port);
1955
+ if (httpResult.ok) {
1956
+ await drainBacklogIfPresent();
1957
+ return {
1958
+ inserted: httpResult.inserted,
1959
+ skipped: httpResult.skipped,
1960
+ via: "http"
1961
+ };
1962
+ }
1963
+ try {
1964
+ return await withCodexHookIngestLock(async () => {
1965
+ recoverStaleCodexHookTmpSpool(codexHookLockTtlSeconds());
1966
+ let currentResult;
1967
+ try {
1968
+ currentResult = {
1969
+ ...directIngest(ingestPayload, getDbPath()),
1970
+ via: "direct"
1971
+ };
1972
+ } catch (err) {
1973
+ logHookEvent(`codemem codex-hook-ingest direct fallback failed: ${err instanceof Error ? err.message : String(err)}`);
1974
+ if (!spoolCodexHookPayload(ingestPayload)) throw new Error("codex-hook-ingest: fallback and spool both failed");
1975
+ currentResult = {
1976
+ inserted: 0,
1977
+ skipped: 0,
1978
+ via: "spool"
1979
+ };
1980
+ }
1981
+ await drainCodexHookSpool((queuedPayload) => tryDirectFallback(queuedPayload));
1982
+ return currentResult;
1983
+ });
1984
+ } catch (err) {
1985
+ if (!(err instanceof CodexHookLockBusyError)) throw err;
1986
+ logHookEvent("codemem codex-hook-ingest lock busy; trying unlocked fallback");
1987
+ try {
1988
+ return {
1989
+ ...directIngest(ingestPayload, getDbPath()),
1990
+ via: "direct"
1991
+ };
1992
+ } catch (directErr) {
1993
+ logHookEvent(`codemem codex-hook-ingest unlocked direct fallback failed: ${directErr instanceof Error ? directErr.message : String(directErr)}`);
1994
+ }
1995
+ if (spoolCodexHookPayload(ingestPayload)) return {
1996
+ inserted: 0,
1997
+ skipped: 0,
1998
+ via: "spool_lock_busy"
1999
+ };
2000
+ throw err;
2001
+ }
2002
+ }
2003
+ var codexHookCmd = new Command("codex-hook-ingest").configureHelp(helpStyle).description("Ingest Codex hook payload: HTTP first, direct DB fallback");
2004
+ addDbOption(codexHookCmd);
2005
+ addViewerHostOptions(codexHookCmd);
2006
+ var codexHookIngestCommand = codexHookCmd.action(async (opts) => {
2007
+ if (envTruthyValue(process.env.CODEMEM_PLUGIN_IGNORE)) return;
2008
+ let raw;
2009
+ try {
2010
+ raw = readFileSync(0, "utf8").trim();
2011
+ } catch {
2012
+ emitStructuredError$1("read_error", "failed to read stdin");
2013
+ return;
2014
+ }
2015
+ if (!raw) {
2016
+ emitStructuredError$1("read_error", "empty stdin");
2017
+ return;
2018
+ }
2019
+ let payload;
2020
+ try {
2021
+ const parsed = JSON.parse(raw);
2022
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
2023
+ emitStructuredError$1("parse_error", "payload must be a JSON object");
2024
+ return;
2025
+ }
2026
+ payload = parsed;
2027
+ } catch {
2028
+ emitStructuredError$1("parse_error", "invalid JSON");
2029
+ return;
2030
+ }
2031
+ try {
2032
+ const result = await ingestCodexHookPayload(payload, opts);
2033
+ console.log(JSON.stringify(result));
2034
+ } catch (err) {
2035
+ emitStructuredError$1("ingest_error", err instanceof Error ? err.message : String(err));
2036
+ }
2037
+ });
2038
+ //#endregion
2039
+ //#region src/commands/codex-hook-inject.ts
2040
+ var HOOK_EVENT_NAME = "UserPromptSubmit";
2041
+ var EMPTY_PACK = {
2042
+ packText: "",
2043
+ items: 0,
2044
+ packTokens: 0
2045
+ };
2046
+ var DEFAULT_VIEWER_HOST = "127.0.0.1";
2047
+ var DEFAULT_VIEWER_PORT = 38888;
2048
+ var DEFAULT_MAX_CHARS = 16e3;
2049
+ var DEFAULT_HTTP_MAX_TIME_S = 2;
2050
+ function emitJson(value) {
2051
+ console.log(JSON.stringify(value));
2052
+ }
2053
+ function emitError(value) {
2054
+ process.stderr.write(`${JSON.stringify(value)}\n`);
2055
+ }
2056
+ function envNotDisabled(value) {
2057
+ const normalized = String(value ?? "").trim().toLowerCase();
2058
+ return normalized !== "0" && normalized !== "false" && normalized !== "off";
2059
+ }
2060
+ function envTruthy(value) {
2061
+ const normalized = String(value ?? "").trim().toLowerCase();
2062
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
2063
+ }
2064
+ function parsePositiveInt$1(value, fallback) {
2065
+ const parsed = Number.parseInt(String(value ?? ""), 10);
2066
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
2067
+ }
2068
+ function continueResult(additionalContext) {
2069
+ if (!additionalContext) return { continue: true };
2070
+ return {
2071
+ continue: true,
2072
+ hookSpecificOutput: {
2073
+ hookEventName: HOOK_EVENT_NAME,
2074
+ additionalContext
2075
+ }
2076
+ };
2077
+ }
2078
+ function truncateAdditionalContext(text, maxChars) {
2079
+ const normalized = text.trim();
2080
+ if (!normalized) return "";
2081
+ if (!Number.isFinite(maxChars) || maxChars <= 0 || normalized.length <= maxChars) return normalized;
2082
+ return `${normalized.slice(0, maxChars).trimEnd()}\n\n[pack truncated]`;
2083
+ }
2084
+ function resolveInjectProject(payload) {
2085
+ return resolveHookProject(typeof payload.cwd === "string" ? payload.cwd : null, payload.project);
2086
+ }
2087
+ function buildCodexInjectQuery(prompt, project) {
2088
+ return [prompt, project ?? ""].filter((part) => part.trim().length > 0).join(" ").slice(0, 500) || "recent work";
2089
+ }
2090
+ async function buildLocalPack(context, project, dbPath) {
2091
+ const store = new MemoryStore(dbPath);
2092
+ try {
2093
+ const limit = parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8);
2094
+ const budget = parsePositiveInt$1(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
2095
+ const filters = {};
2096
+ if (project) filters.project = project;
2097
+ const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
2098
+ return {
2099
+ packText: String(pack.pack_text ?? "").trim(),
2100
+ items: Array.isArray(pack.items) ? pack.items.length : 0,
2101
+ packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics?.pack_tokens) : 0
2102
+ };
2103
+ } finally {
2104
+ store.close();
2105
+ }
2106
+ }
2107
+ async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S * 1e3) {
2108
+ const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
2109
+ const port = parsePositiveInt$1(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
2110
+ const url = new URL(`http://${host}:${port}/api/pack`);
2111
+ url.searchParams.set("context", context);
2112
+ url.searchParams.set("limit", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8)));
2113
+ url.searchParams.set("token_budget", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800)));
2114
+ if (project) url.searchParams.set("project", project);
2115
+ const controller = new AbortController();
2116
+ const timeout = setTimeout(() => controller.abort(), maxTimeMs);
2117
+ try {
2118
+ const res = await fetch(url, { signal: controller.signal });
2119
+ if (!res.ok) return EMPTY_PACK;
2120
+ const body = await res.json();
2121
+ return {
2122
+ packText: String(body.pack_text ?? "").trim(),
2123
+ items: Array.isArray(body.items) ? body.items.length : 0,
2124
+ packTokens: Number.isFinite(Number(body.metrics?.pack_tokens)) ? Number(body.metrics?.pack_tokens) : 0
2125
+ };
2126
+ } catch {
2127
+ return EMPTY_PACK;
2128
+ } finally {
2129
+ clearTimeout(timeout);
2130
+ }
2131
+ }
2132
+ async function buildCodexHookInjection(payload, opts, deps = {}) {
2133
+ if (envTruthy(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult();
2134
+ if (!envNotDisabled(process.env.CODEMEM_INJECT_CONTEXT || "1")) return continueResult();
2135
+ const promptText = normalizePromptText(payload.prompt);
2136
+ if (!promptText) return continueResult();
2137
+ const buildPack = deps.buildLocalPack ?? buildLocalPack;
2138
+ const httpPack = deps.httpPack ?? tryHttpPack;
2139
+ const resolveDb = deps.resolveDb ?? resolveDbPath;
2140
+ const project = resolveInjectProject(payload);
2141
+ const query = buildCodexInjectQuery(promptText, project);
2142
+ const maxChars = parsePositiveInt$1(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS);
2143
+ const httpMaxTimeMs = parsePositiveInt$1(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S) * 1e3;
2144
+ let pack = EMPTY_PACK;
2145
+ let origin = "none";
2146
+ try {
2147
+ pack = await buildPack(query, project, resolveDb(resolveDbOpt(opts)));
2148
+ if (pack.packText) origin = "local";
2149
+ } catch (err) {
2150
+ logHookEvent(`codemem codex-hook-inject local pack failed: ${err instanceof Error ? err.message : String(err)}`);
2151
+ }
2152
+ if (!pack.packText && envNotDisabled(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) {
2153
+ pack = await httpPack(query, project, httpMaxTimeMs);
2154
+ if (pack.packText) origin = "http";
2155
+ }
2156
+ const fields = [
2157
+ "inject.pack.ok",
2158
+ "source=codex",
2159
+ `origin=${origin}`,
2160
+ `items=${pack.items}`,
2161
+ `pack_tokens=${pack.packTokens}`,
2162
+ `query_len=${query.length}`,
2163
+ `empty=${pack.packText ? "false" : "true"}`
2164
+ ];
2165
+ if (project) fields.push(`project=${JSON.stringify(project)}`);
2166
+ logHookEvent(fields.join(" "));
2167
+ return continueResult(truncateAdditionalContext(pack.packText, maxChars));
2168
+ }
2169
+ var codexHookInjectCmd = new Command("codex-hook-inject").configureHelp(helpStyle).description("Return Codex hook additionalContext from local pack generation");
2170
+ addDbOption(codexHookInjectCmd);
2171
+ var codexHookInjectCommand = codexHookInjectCmd.action(async (opts) => {
2172
+ let raw = "";
2173
+ for await (const chunk of process.stdin) raw += String(chunk);
2174
+ const trimmed = raw.trim();
2175
+ if (!trimmed) {
2176
+ emitJson(continueResult());
2177
+ return;
2178
+ }
2179
+ let payload;
2180
+ try {
2181
+ const parsed = JSON.parse(trimmed);
2182
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
2183
+ emitError({
2184
+ error: "parse_error",
2185
+ message: "payload must be a JSON object"
2186
+ });
2187
+ process.exitCode = 1;
2188
+ return;
2189
+ }
2190
+ payload = parsed;
2191
+ } catch {
2192
+ emitError({
2193
+ error: "parse_error",
2194
+ message: "invalid JSON"
2195
+ });
2196
+ process.exitCode = 1;
2197
+ return;
2198
+ }
2199
+ try {
2200
+ emitJson(await buildCodexHookInjection(payload, opts));
2201
+ } catch (err) {
2202
+ logHookEvent(`codemem codex-hook-inject failed: ${err instanceof Error ? err.message : String(err)}`);
2203
+ emitJson(continueResult());
2204
+ }
1524
2205
  });
1525
2206
  //#endregion
1526
2207
  //#region src/commands/config.ts
@@ -6325,6 +7006,8 @@ completion.on("command", ({ reply }) => {
6325
7006
  "claude-hook-file-context",
6326
7007
  "claude-hook-inject",
6327
7008
  "claude-hook-ingest",
7009
+ "codex-hook-inject",
7010
+ "codex-hook-ingest",
6328
7011
  "config",
6329
7012
  "coordinator",
6330
7013
  "db",
@@ -6403,6 +7086,8 @@ program.addCommand(mcpCommand);
6403
7086
  program.addCommand(claudeHookInjectCommand);
6404
7087
  program.addCommand(claudeHookIngestCommand);
6405
7088
  program.addCommand(claudeHookFileContextCommand);
7089
+ program.addCommand(codexHookInjectCommand);
7090
+ program.addCommand(codexHookIngestCommand);
6406
7091
  program.addCommand(dbCommand);
6407
7092
  program.addCommand(exportMemoriesCommand);
6408
7093
  program.addCommand(importMemoriesCommand);