codemem 0.29.1 → 0.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, ensureDeviceIdentity, ensureSchemaBootstrapped, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, listMaintenanceJobs, loadObserverConfig, loadPublicKey, loadSqliteVec, mdnsEnabled, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, 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, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, ensureDeviceIdentity, ensureSchemaBootstrapped, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, listMaintenanceJobs, 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
+ import { homedir, networkInterfaces } from "node:os";
7
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
8
  import { styleText } from "node:util";
7
9
  import { randomInt } from "node:crypto";
8
- import { homedir, networkInterfaces } from "node:os";
9
- import { dirname, join } from "node:path";
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";
@@ -79,9 +79,9 @@ function emitJsonError(errorCode, message, exitCode = 1) {
79
79
  //#endregion
80
80
  //#region src/commands/claude-hook-plugin-log.ts
81
81
  /**
82
- * Append-only plugin failure log used by both `claude-hook-inject` and
83
- * `claude-hook-ingest` to record errors that don't justify crashing the
84
- * hook command itself.
82
+ * Append-only plugin event log used by `claude-hook-inject` and
83
+ * `claude-hook-ingest` to record successes (e.g. `inject.pack.ok ...`) and
84
+ * errors that don't justify crashing the hook command itself.
85
85
  *
86
86
  * Behavior:
87
87
  * - Default log path is `~/.codemem/plugin.log`.
@@ -101,7 +101,7 @@ var BOOLEAN_TOGGLE_VALUES = new Set([
101
101
  "on",
102
102
  "no"
103
103
  ]);
104
- function expandHome$2(value) {
104
+ function expandHome$3(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,15 +110,15 @@ function expandHome$2(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$2("~/.codemem/plugin.log");
114
- return expandHome$2(raw.trim());
113
+ if (BOOLEAN_TOGGLE_VALUES.has(normalized)) return expandHome$3("~/.codemem/plugin.log");
114
+ return expandHome$3(raw.trim());
115
115
  }
116
116
  /**
117
117
  * Append a single timestamped line to the plugin log. Best-effort: any
118
118
  * filesystem error is swallowed so a logging failure can never bubble up
119
119
  * into a Claude hook crash.
120
120
  */
121
- function logHookFailure(message) {
121
+ function logHookEvent(message) {
122
122
  const path = pluginLogPath();
123
123
  try {
124
124
  mkdirSync(dirname(path), { recursive: true });
@@ -126,6 +126,264 @@ function logHookFailure(message) {
126
126
  } catch {}
127
127
  }
128
128
  //#endregion
129
+ //#region src/commands/claude-hook-file-context.ts
130
+ var FILE_GATE_MIN_BYTES = 1500;
131
+ var FETCH_LIMIT = 40;
132
+ var DISPLAY_LIMIT = 15;
133
+ var MTIME_FRESH_TOLERANCE_MS = 300 * 1e3;
134
+ var SMALL_FILE_BYPASS_PATTERNS = [
135
+ /\.(json|jsonc|toml|ya?ml)$/i,
136
+ /\.env(\.|$)/i,
137
+ /(^|\/)dockerfile(\.|$)/i,
138
+ /\.config\.(js|ts|mjs|cjs|json)$/i
139
+ ];
140
+ var KIND_ICONS = {
141
+ decision: "⚖️",
142
+ bugfix: "🔴",
143
+ feature: "🟢",
144
+ refactor: "🔄",
145
+ discovery: "🔵",
146
+ change: "✅",
147
+ exploration: "🔬"
148
+ };
149
+ function emitJson$1(value) {
150
+ console.log(JSON.stringify(value));
151
+ }
152
+ function emitError$1(value) {
153
+ process.stderr.write(`${JSON.stringify(value)}\n`);
154
+ }
155
+ function continueResult$1() {
156
+ return { continue: true };
157
+ }
158
+ function envNotDisabled$1(value) {
159
+ const normalized = String(value ?? "").trim().toLowerCase();
160
+ return normalized !== "0" && normalized !== "false" && normalized !== "off";
161
+ }
162
+ function envTruthy$2(value) {
163
+ const normalized = String(value ?? "").trim().toLowerCase();
164
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
165
+ }
166
+ function expandHome$2(value) {
167
+ if (value === "~") return homedir();
168
+ if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
169
+ return value;
170
+ }
171
+ function extractFilePath(payload) {
172
+ const toolInput = payload.tool_input;
173
+ if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) return null;
174
+ const filePath = toolInput.file_path;
175
+ return typeof filePath === "string" && filePath.trim() ? filePath.trim() : null;
176
+ }
177
+ function statFile(absPath) {
178
+ try {
179
+ const stat = statSync(absPath);
180
+ return {
181
+ sizeBytes: stat.size,
182
+ mtimeMs: stat.mtimeMs
183
+ };
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+ function parseJsonArray(value) {
189
+ if (!value) return [];
190
+ try {
191
+ const parsed = JSON.parse(value);
192
+ if (!Array.isArray(parsed)) return [];
193
+ return parsed.filter((item) => typeof item === "string");
194
+ } catch {
195
+ return [];
196
+ }
197
+ }
198
+ function normalizePathForCompare(path) {
199
+ return path.replace(/\\/g, "/");
200
+ }
201
+ function scoreRow(row, normalizedTarget, idx) {
202
+ const filesModified = parseJsonArray(row.files_modified);
203
+ const inModified = filesModified.some((f) => normalizePathForCompare(f) === normalizedTarget);
204
+ let score = 0;
205
+ if (inModified) score += 2;
206
+ if (filesModified.length <= 1) score += 2;
207
+ else if (filesModified.length <= 3) score += 1;
208
+ return {
209
+ row,
210
+ score,
211
+ idx
212
+ };
213
+ }
214
+ function scoreAndDedupe(rows, targetPath, limit) {
215
+ const normalizedTarget = normalizePathForCompare(targetPath);
216
+ const scored = rows.map((row, idx) => scoreRow(row, normalizedTarget, idx));
217
+ const bestPerSession = /* @__PURE__ */ new Map();
218
+ for (const item of scored) {
219
+ const existing = bestPerSession.get(item.row.session_id);
220
+ if (!existing || item.score > existing.score || item.score === existing.score && item.idx < existing.idx) bestPerSession.set(item.row.session_id, item);
221
+ }
222
+ const deduped = Array.from(bestPerSession.values());
223
+ deduped.sort((a, b) => b.score - a.score || a.idx - b.idx);
224
+ return deduped.slice(0, limit).map((s) => s.row);
225
+ }
226
+ function compactTime(timeStr) {
227
+ return timeStr.toLowerCase().replace(" am", "a").replace(" pm", "p");
228
+ }
229
+ function formatTime(epochMs) {
230
+ return new Date(epochMs).toLocaleString("en-US", {
231
+ hour: "numeric",
232
+ minute: "2-digit",
233
+ hour12: true
234
+ });
235
+ }
236
+ function formatDate(epochMs) {
237
+ return new Date(epochMs).toLocaleString("en-US", {
238
+ month: "short",
239
+ day: "numeric",
240
+ year: "numeric"
241
+ });
242
+ }
243
+ function formatTimeline(rows, filePath, staleness) {
244
+ const safePath = filePath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
245
+ const enriched = rows.map((row) => ({
246
+ row,
247
+ epochMs: Date.parse(row.created_at)
248
+ })).filter((item) => Number.isFinite(item.epochMs) && item.epochMs > 0);
249
+ const byDay = /* @__PURE__ */ new Map();
250
+ for (const item of enriched) {
251
+ const day = formatDate(item.epochMs);
252
+ const bucket = byDay.get(day);
253
+ if (bucket) bucket.push(item);
254
+ else byDay.set(day, [item]);
255
+ }
256
+ const sortedDays = Array.from(byDay.entries()).sort((a, b) => {
257
+ return Math.min(...a[1].map((i) => i.epochMs)) - Math.min(...b[1].map((i) => i.epochMs));
258
+ });
259
+ const ids = rows.map((r) => r.id);
260
+ const lines = [`This file (${safePath}) has prior codemem observations. The Read result below is unchanged.`, `- Fetch full bodies on demand: memory.get_observations([${ids.join(", ")}]).`];
261
+ if (staleness) {
262
+ const driftMinutes = Math.max(1, Math.round((staleness.fileMtimeMs - staleness.newestObservationMs) / 6e4));
263
+ lines.unshift(`Heads up: this file was modified ~${driftMinutes} min after the most recent observation below. Past entries may be partially stale — verify against the Read result before relying on them.`);
264
+ }
265
+ for (const [day, dayItems] of sortedDays) {
266
+ const chronological = [...dayItems].sort((a, b) => a.epochMs - b.epochMs);
267
+ lines.push(`### ${day}`);
268
+ for (const { row, epochMs } of chronological) {
269
+ const title = (row.title || "Untitled").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
270
+ const icon = KIND_ICONS[row.kind] ?? "❔";
271
+ const time = compactTime(formatTime(epochMs));
272
+ lines.push(`${row.id} ${time} ${icon} (${row.kind}) ${title}`);
273
+ }
274
+ }
275
+ return lines.join("\n");
276
+ }
277
+ function queryByFile(dbPath, relativePath, project, limit) {
278
+ const store = new MemoryStore(dbPath);
279
+ try {
280
+ const opts = { limit };
281
+ if (project) opts.project = project;
282
+ return store.findByFile(relativePath, opts);
283
+ } finally {
284
+ store.close();
285
+ }
286
+ }
287
+ function resolveProject$1(payload) {
288
+ return resolveHookProject(typeof payload.cwd === "string" ? payload.cwd : null, payload.project);
289
+ }
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();
293
+ const filePath = extractFilePath(payload);
294
+ if (!filePath) return continueResult$1();
295
+ const cwd = typeof payload.cwd === "string" && payload.cwd.trim() ? payload.cwd : process.cwd();
296
+ const expandedPath = expandHome$2(filePath);
297
+ const absolutePath = isAbsolute(expandedPath) ? expandedPath : resolve(cwd, expandedPath);
298
+ const relativePath = relative(cwd, absolutePath).split(sep).join("/");
299
+ const escapesCwd = relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath);
300
+ if (!relativePath || escapesCwd) {
301
+ logHookEvent(`file_context.skip reason=outside_cwd path=${JSON.stringify(filePath)} cwd=${JSON.stringify(cwd)}`);
302
+ return continueResult$1();
303
+ }
304
+ const minBytes = Number.parseInt(process.env.CODEMEM_FILE_CONTEXT_MIN_BYTES ?? `${FILE_GATE_MIN_BYTES}`, 10);
305
+ const minBytesEffective = Number.isFinite(minBytes) && minBytes >= 0 ? minBytes : FILE_GATE_MIN_BYTES;
306
+ const stat = (deps.statFile ?? statFile)(absolutePath);
307
+ if (!stat) {
308
+ logHookEvent(`file_context.skip reason=stat_failed path=${JSON.stringify(relativePath)}`);
309
+ return continueResult$1();
310
+ }
311
+ const bypassSizeGate = SMALL_FILE_BYPASS_PATTERNS.some((p) => p.test(relativePath));
312
+ if (stat.sizeBytes < minBytesEffective && !bypassSizeGate) {
313
+ logHookEvent(`file_context.skip reason=below_size_gate path=${JSON.stringify(relativePath)} size=${stat.sizeBytes} gate=${minBytesEffective}`);
314
+ return continueResult$1();
315
+ }
316
+ const project = resolveProject$1(payload);
317
+ const resolveDb = deps.resolveDb ?? resolveDbPath;
318
+ const queryFn = deps.queryByFile ?? queryByFile;
319
+ let rows = [];
320
+ try {
321
+ rows = queryFn(resolveDb(resolveDbOpt(opts)), relativePath, project, FETCH_LIMIT);
322
+ } catch (err) {
323
+ logHookEvent(`codemem claude-hook-file-context query failed: ${err instanceof Error ? err.message : String(err)}`);
324
+ return continueResult$1();
325
+ }
326
+ if (rows.length === 0) {
327
+ logHookEvent(`file_context.skip reason=no_observations path=${JSON.stringify(relativePath)} project=${JSON.stringify(project ?? "")}`);
328
+ return continueResult$1();
329
+ }
330
+ const top = scoreAndDedupe(rows, relativePath, DISPLAY_LIMIT);
331
+ if (top.length === 0) {
332
+ logHookEvent(`file_context.skip reason=no_top_after_dedupe path=${JSON.stringify(relativePath)} candidates=${rows.length}`);
333
+ return continueResult$1();
334
+ }
335
+ let staleness = null;
336
+ if (stat.mtimeMs > 0) {
337
+ const newestObservationMs = top.reduce((max, row) => {
338
+ const epoch = Date.parse(row.created_at);
339
+ return Number.isFinite(epoch) && epoch > max ? epoch : max;
340
+ }, 0);
341
+ if (newestObservationMs > 0 && stat.mtimeMs > newestObservationMs + MTIME_FRESH_TOLERANCE_MS) staleness = {
342
+ fileMtimeMs: stat.mtimeMs,
343
+ newestObservationMs
344
+ };
345
+ }
346
+ const timeline = formatTimeline(top, relativePath, staleness);
347
+ logHookEvent(`file_context.ok path=${JSON.stringify(relativePath)} candidates=${rows.length} surfaced=${top.length} project=${JSON.stringify(project ?? "")} stale=${staleness ? "true" : "false"}`);
348
+ return { hookSpecificOutput: {
349
+ hookEventName: "PreToolUse",
350
+ permissionDecision: "allow",
351
+ additionalContext: timeline
352
+ } };
353
+ }
354
+ var claudeHookFileContextCmd = new Command("claude-hook-file-context").configureHelp(helpStyle).description("Return Claude PreToolUse:Read additionalContext from per-file observation timeline");
355
+ addDbOption(claudeHookFileContextCmd);
356
+ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts) => {
357
+ let raw = "";
358
+ for await (const chunk of process.stdin) raw += String(chunk);
359
+ const trimmed = raw.trim();
360
+ if (!trimmed) {
361
+ emitJson$1(continueResult$1());
362
+ return;
363
+ }
364
+ let payload;
365
+ try {
366
+ const parsed = JSON.parse(trimmed);
367
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
368
+ emitError$1({
369
+ error: "parse_error",
370
+ message: "payload must be a JSON object"
371
+ });
372
+ process.exitCode = 1;
373
+ return;
374
+ }
375
+ payload = parsed;
376
+ } catch {
377
+ emitError$1({
378
+ error: "parse_error",
379
+ message: "invalid JSON"
380
+ });
381
+ process.exitCode = 1;
382
+ return;
383
+ }
384
+ emitJson$1(await buildClaudeFileContext(payload, opts));
385
+ });
386
+ //#endregion
129
387
  //#region src/commands/claude-hook-ingest-spool.ts
130
388
  /**
131
389
  * Durability layer for `claude-hook-ingest`: file-based mutex to
@@ -350,7 +608,7 @@ function spoolPayload(payload) {
350
608
  try {
351
609
  mkdirSync(dir, { recursive: true });
352
610
  } catch {
353
- logHookFailure("codemem claude-hook-ingest failed to create spool dir");
611
+ logHookEvent("codemem claude-hook-ingest failed to create spool dir");
354
612
  return false;
355
613
  }
356
614
  const payloadText = JSON.stringify(payload);
@@ -358,7 +616,7 @@ function spoolPayload(payload) {
358
616
  try {
359
617
  writeFileSync(tmpPath, payloadText, { encoding: "utf8" });
360
618
  } catch {
361
- logHookFailure("codemem claude-hook-ingest failed to allocate spool temp file");
619
+ logHookEvent("codemem claude-hook-ingest failed to allocate spool temp file");
362
620
  return false;
363
621
  }
364
622
  const finalPath = join(dir, `hook-${Math.floor(Date.now() / 1e3)}-${process.pid}-${randomInt(1e3, 1e4)}.json`);
@@ -368,10 +626,10 @@ function spoolPayload(payload) {
368
626
  try {
369
627
  unlinkSync(tmpPath);
370
628
  } catch {}
371
- logHookFailure("codemem claude-hook-ingest failed to spool payload");
629
+ logHookEvent("codemem claude-hook-ingest failed to spool payload");
372
630
  return false;
373
631
  }
374
- logHookFailure(`codemem claude-hook-ingest spooled payload: ${finalPath}`);
632
+ logHookEvent(`codemem claude-hook-ingest spooled payload: ${finalPath}`);
375
633
  return true;
376
634
  }
377
635
  /**
@@ -408,7 +666,7 @@ function recoverStaleTmpSpool(ttlSeconds) {
408
666
  const recoveredPath = join(dir, `hook-recovered-${Math.floor(nowS)}-${process.pid}-${randomInt(1e3, 1e4)}.json`);
409
667
  try {
410
668
  renameSync(tmpPath, recoveredPath);
411
- logHookFailure(`codemem claude-hook-ingest recovered stale temp spool payload: ${recoveredPath}`);
669
+ logHookEvent(`codemem claude-hook-ingest recovered stale temp spool payload: ${recoveredPath}`);
412
670
  } catch {}
413
671
  }
414
672
  }
@@ -423,11 +681,11 @@ function quarantineSpoolEntry(dir, name, reason) {
423
681
  const quarantineName = `.bad-${reason}-${Date.now()}-${randomInt(1e3, 1e4)}-${name}`;
424
682
  try {
425
683
  renameSync(sourcePath, join(dir, quarantineName));
426
- logHookFailure(`codemem claude-hook-ingest quarantined corrupt spool payload (${reason}): ${quarantineName}`);
684
+ logHookEvent(`codemem claude-hook-ingest quarantined corrupt spool payload (${reason}): ${quarantineName}`);
427
685
  } catch {
428
686
  try {
429
687
  unlinkSync(sourcePath);
430
- logHookFailure(`codemem claude-hook-ingest dropped corrupt spool payload (${reason}): ${name}`);
688
+ logHookEvent(`codemem claude-hook-ingest dropped corrupt spool payload (${reason}): ${name}`);
431
689
  } catch {}
432
690
  }
433
691
  }
@@ -469,7 +727,7 @@ async function drainSpool(handler) {
469
727
  try {
470
728
  raw = readFileSync(path, "utf8");
471
729
  } catch {
472
- logHookFailure(`codemem claude-hook-ingest failed to read spooled payload: ${path}`);
730
+ logHookEvent(`codemem claude-hook-ingest failed to read spooled payload: ${path}`);
473
731
  result.failed++;
474
732
  continue;
475
733
  }
@@ -496,7 +754,7 @@ async function drainSpool(handler) {
496
754
  result.processed++;
497
755
  } catch {}
498
756
  else {
499
- logHookFailure(`codemem claude-hook-ingest failed processing spooled payload: ${path}`);
757
+ logHookEvent(`codemem claude-hook-ingest failed processing spooled payload: ${path}`);
500
758
  result.failed++;
501
759
  }
502
760
  }
@@ -799,7 +1057,7 @@ async function tryHttpIngest(payload, host, port) {
799
1057
  try {
800
1058
  body = await res.json();
801
1059
  } catch {
802
- logHookFailure("codemem claude-hook-ingest HTTP accepted with invalid response body");
1060
+ logHookEvent("codemem claude-hook-ingest HTTP accepted with invalid response body");
803
1061
  return {
804
1062
  ok: false,
805
1063
  inserted: 0,
@@ -807,7 +1065,7 @@ async function tryHttpIngest(payload, host, port) {
807
1065
  };
808
1066
  }
809
1067
  if (body == null || typeof body !== "object" || Array.isArray(body)) {
810
- logHookFailure("codemem claude-hook-ingest HTTP accepted with invalid response type");
1068
+ logHookEvent("codemem claude-hook-ingest HTTP accepted with invalid response type");
811
1069
  return {
812
1070
  ok: false,
813
1071
  inserted: 0,
@@ -816,7 +1074,7 @@ async function tryHttpIngest(payload, host, port) {
816
1074
  }
817
1075
  const obj = body;
818
1076
  if (typeof obj.inserted !== "number" || typeof obj.skipped !== "number") {
819
- logHookFailure("codemem claude-hook-ingest HTTP accepted with unexpected response body");
1077
+ logHookEvent("codemem claude-hook-ingest HTTP accepted with unexpected response body");
820
1078
  return {
821
1079
  ok: false,
822
1080
  inserted: 0,
@@ -902,14 +1160,14 @@ async function flushBoundaryRawEvents(payload, dbPath) {
902
1160
  try {
903
1161
  observer = new ObserverClient();
904
1162
  } catch (err) {
905
- logHookFailure(`codemem claude-hook-ingest boundary flush observer init failed: ${err instanceof Error ? err.message : String(err)}`);
1163
+ logHookEvent(`codemem claude-hook-ingest boundary flush observer init failed: ${err instanceof Error ? err.message : String(err)}`);
906
1164
  return;
907
1165
  }
908
1166
  let store;
909
1167
  try {
910
1168
  store = new MemoryStore(dbPath);
911
1169
  } catch (err) {
912
- logHookFailure(`codemem claude-hook-ingest boundary flush store init failed: ${err instanceof Error ? err.message : String(err)}`);
1170
+ logHookEvent(`codemem claude-hook-ingest boundary flush store init failed: ${err instanceof Error ? err.message : String(err)}`);
913
1171
  return;
914
1172
  }
915
1173
  try {
@@ -922,7 +1180,7 @@ async function flushBoundaryRawEvents(payload, dbPath) {
922
1180
  maxEvents: null
923
1181
  });
924
1182
  } catch (err) {
925
- logHookFailure(`codemem claude-hook-ingest boundary flush raw events failed: ${err instanceof Error ? err.message : String(err)}`);
1183
+ logHookEvent(`codemem claude-hook-ingest boundary flush raw events failed: ${err instanceof Error ? err.message : String(err)}`);
926
1184
  } finally {
927
1185
  store.close();
928
1186
  }
@@ -953,7 +1211,7 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
953
1211
  result: directIngest(queued, getDbPath())
954
1212
  };
955
1213
  } catch (err) {
956
- logHookFailure(`codemem claude-hook-ingest direct fallback failed: ${err instanceof Error ? err.message : String(err)}`);
1214
+ logHookEvent(`codemem claude-hook-ingest direct fallback failed: ${err instanceof Error ? err.message : String(err)}`);
957
1215
  return { ok: false };
958
1216
  }
959
1217
  };
@@ -962,12 +1220,12 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
962
1220
  try {
963
1221
  directIngest(payload, getDbPath());
964
1222
  } catch (err) {
965
- logHookFailure(`codemem claude-hook-ingest boundary flush direct write failed: ${err instanceof Error ? err.message : String(err)}`);
1223
+ logHookEvent(`codemem claude-hook-ingest boundary flush direct write failed: ${err instanceof Error ? err.message : String(err)}`);
966
1224
  }
967
1225
  try {
968
1226
  await boundaryFlush(payload, getDbPath());
969
1227
  } catch (err) {
970
- logHookFailure(`codemem claude-hook-ingest boundary flush failed: ${err instanceof Error ? err.message : String(err)}`);
1228
+ logHookEvent(`codemem claude-hook-ingest boundary flush failed: ${err instanceof Error ? err.message : String(err)}`);
971
1229
  }
972
1230
  };
973
1231
  const drainBacklogIfPresent = async () => {
@@ -982,7 +1240,7 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
982
1240
  });
983
1241
  } catch (err) {
984
1242
  if (err instanceof LockBusyError) return;
985
- logHookFailure(`codemem claude-hook-ingest backlog drain failed: ${err instanceof Error ? err.message : String(err)}`);
1243
+ logHookEvent(`codemem claude-hook-ingest backlog drain failed: ${err instanceof Error ? err.message : String(err)}`);
986
1244
  }
987
1245
  };
988
1246
  const httpResult = await httpIngest(payload, opts.host, port);
@@ -1024,12 +1282,12 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
1024
1282
  skipped: 0,
1025
1283
  via: "spool"
1026
1284
  };
1027
- logHookFailure("codemem claude-hook-ingest failed: fallback and spool failed");
1285
+ logHookEvent("codemem claude-hook-ingest failed: fallback and spool failed");
1028
1286
  throw new Error("claude-hook-ingest: fallback and spool both failed");
1029
1287
  });
1030
1288
  } catch (err) {
1031
1289
  if (!(err instanceof LockBusyError)) throw err;
1032
- logHookFailure("codemem claude-hook-ingest lock busy; trying unlocked fallback");
1290
+ logHookEvent("codemem claude-hook-ingest lock busy; trying unlocked fallback");
1033
1291
  const direct = tryDirectFallback(payload);
1034
1292
  if (direct.ok) return {
1035
1293
  ...direct.result,
@@ -1040,7 +1298,7 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
1040
1298
  skipped: 0,
1041
1299
  via: "spool_lock_busy"
1042
1300
  };
1043
- logHookFailure("codemem claude-hook-ingest failed: unlocked fallback and spool failed");
1301
+ logHookEvent("codemem claude-hook-ingest failed: unlocked fallback and spool failed");
1044
1302
  throw err;
1045
1303
  }
1046
1304
  }
@@ -1086,6 +1344,11 @@ var claudeHookIngestCommand = claudeHookCmd.action(async (opts) => {
1086
1344
  //#endregion
1087
1345
  //#region src/commands/claude-hook-inject.ts
1088
1346
  var HOOK_EVENT_NAME = "UserPromptSubmit";
1347
+ var EMPTY_PACK = {
1348
+ packText: "",
1349
+ items: 0,
1350
+ packTokens: 0
1351
+ };
1089
1352
  var DEFAULT_VIEWER_HOST = "127.0.0.1";
1090
1353
  var DEFAULT_VIEWER_PORT = 38888;
1091
1354
  var DEFAULT_MAX_CHARS = 16e3;
@@ -1093,6 +1356,9 @@ var DEFAULT_HTTP_MAX_TIME_S = 2;
1093
1356
  function emitJson(value) {
1094
1357
  console.log(JSON.stringify(value));
1095
1358
  }
1359
+ function emitError(value) {
1360
+ process.stderr.write(`${JSON.stringify(value)}\n`);
1361
+ }
1096
1362
  function envNotDisabled(value) {
1097
1363
  const normalized = String(value ?? "").trim().toLowerCase();
1098
1364
  return normalized !== "0" && normalized !== "false" && normalized !== "off";
@@ -1137,12 +1403,21 @@ async function buildLocalPack(context, project, dbPath, workingSetPaths = []) {
1137
1403
  if (project) filters.project = project;
1138
1404
  if (workingSetPaths.length > 0) filters.working_set_paths = workingSetPaths;
1139
1405
  const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
1140
- return String(pack.pack_text ?? "").trim();
1406
+ return {
1407
+ packText: String(pack.pack_text ?? "").trim(),
1408
+ items: Array.isArray(pack.items) ? pack.items.length : 0,
1409
+ packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics.pack_tokens) : 0
1410
+ };
1141
1411
  } finally {
1142
1412
  store.close();
1143
1413
  }
1144
1414
  }
1145
1415
  async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S * 1e3) {
1416
+ const empty = {
1417
+ packText: "",
1418
+ items: 0,
1419
+ packTokens: 0
1420
+ };
1146
1421
  const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
1147
1422
  const port = parsePositiveInt$1(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
1148
1423
  const url = new URL(`http://${host}:${port}/api/pack`);
@@ -1154,11 +1429,15 @@ async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S
1154
1429
  const timeout = setTimeout(() => controller.abort(), maxTimeMs);
1155
1430
  try {
1156
1431
  const res = await fetch(url, { signal: controller.signal });
1157
- if (!res.ok) return "";
1432
+ if (!res.ok) return empty;
1158
1433
  const body = await res.json();
1159
- return String(body.pack_text ?? "").trim();
1434
+ return {
1435
+ packText: String(body.pack_text ?? "").trim(),
1436
+ items: Array.isArray(body.items) ? body.items.length : 0,
1437
+ packTokens: Number.isFinite(Number(body.metrics?.pack_tokens)) ? Number(body.metrics?.pack_tokens) : 0
1438
+ };
1160
1439
  } catch {
1161
- return "";
1440
+ return empty;
1162
1441
  } finally {
1163
1442
  clearTimeout(timeout);
1164
1443
  }
@@ -1186,15 +1465,30 @@ async function buildClaudeHookInjection(payload, opts, deps = {}) {
1186
1465
  const workingSetPaths = workingSetPathsFromState(state);
1187
1466
  const maxChars = parsePositiveInt$1(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS);
1188
1467
  const httpMaxTimeMs = parsePositiveInt$1(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S) * 1e3;
1189
- let additionalContext = "";
1468
+ let pack = EMPTY_PACK;
1469
+ let origin = "none";
1190
1470
  try {
1191
- additionalContext = await buildPack(query, project, resolveDb(resolveDbOpt(opts)), workingSetPaths);
1471
+ pack = await buildPack(query, project, resolveDb(resolveDbOpt(opts)), workingSetPaths);
1472
+ if (pack.packText) origin = "local";
1192
1473
  } catch (err) {
1193
- additionalContext = "";
1194
- logHookFailure(`codemem claude-hook-inject local pack failed: ${err instanceof Error ? err.message : String(err)}`);
1195
- }
1196
- if (!additionalContext && envNotDisabled(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) additionalContext = await httpPack(query, project, httpMaxTimeMs);
1197
- return continueResult(truncateAdditionalContext(additionalContext, maxChars));
1474
+ logHookEvent(`codemem claude-hook-inject local pack failed: ${err instanceof Error ? err.message : String(err)}`);
1475
+ }
1476
+ if (!pack.packText && envNotDisabled(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) {
1477
+ pack = await httpPack(query, project, httpMaxTimeMs);
1478
+ if (pack.packText) origin = "http";
1479
+ }
1480
+ const fields = [
1481
+ "inject.pack.ok",
1482
+ "source=claude",
1483
+ `origin=${origin}`,
1484
+ `items=${pack.items}`,
1485
+ `pack_tokens=${pack.packTokens}`,
1486
+ `query_len=${query.length}`,
1487
+ `empty=${pack.packText ? "false" : "true"}`
1488
+ ];
1489
+ if (project) fields.push(`project=${JSON.stringify(project)}`);
1490
+ logHookEvent(fields.join(" "));
1491
+ return continueResult(truncateAdditionalContext(pack.packText, maxChars));
1198
1492
  }
1199
1493
  var claudeHookInjectCmd = new Command("claude-hook-inject").configureHelp(helpStyle).description("Return Claude hook additionalContext from local pack generation");
1200
1494
  addDbOption(claudeHookInjectCmd);
@@ -1210,7 +1504,7 @@ var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
1210
1504
  try {
1211
1505
  const parsed = JSON.parse(trimmed);
1212
1506
  if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
1213
- emitJson({
1507
+ emitError({
1214
1508
  error: "parse_error",
1215
1509
  message: "payload must be a JSON object"
1216
1510
  });
@@ -1219,7 +1513,7 @@ var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
1219
1513
  }
1220
1514
  payload = parsed;
1221
1515
  } catch {
1222
- emitJson({
1516
+ emitError({
1223
1517
  error: "parse_error",
1224
1518
  message: "invalid JSON"
1225
1519
  });
@@ -2169,7 +2463,8 @@ backfillTagsCmd.action((opts) => {
2169
2463
  since: opts.since ?? null,
2170
2464
  project,
2171
2465
  activeOnly: !opts.inactive,
2172
- dryRun: opts.dryRun === true
2466
+ dryRun: opts.dryRun === true,
2467
+ scanner: store.scanner
2173
2468
  });
2174
2469
  if (opts.json) {
2175
2470
  console.log(JSON.stringify(result, null, 2));
@@ -2303,7 +2598,8 @@ backfillNarrativeCmd.action((opts) => {
2303
2598
  const limit = parseOptionalPositiveInt$1(opts.limit);
2304
2599
  const result = backfillNarrativeFromBody(store.db, {
2305
2600
  limit,
2306
- dryRun: opts.dryRun === true
2601
+ dryRun: opts.dryRun === true,
2602
+ scanner: store.scanner
2307
2603
  });
2308
2604
  if (opts.json) {
2309
2605
  console.log(JSON.stringify(result, null, 2));
@@ -2333,7 +2629,8 @@ aiBackfillStructuredCmd.action(async (opts) => {
2333
2629
  limit,
2334
2630
  kinds,
2335
2631
  overwrite: opts.overwrite === true,
2336
- dryRun: opts.dryRun === true
2632
+ dryRun: opts.dryRun === true,
2633
+ scanner: store.scanner
2337
2634
  });
2338
2635
  if (opts.json) {
2339
2636
  console.log(JSON.stringify(result, null, 2));
@@ -2351,6 +2648,47 @@ aiBackfillStructuredCmd.action(async (opts) => {
2351
2648
  }
2352
2649
  });
2353
2650
  dbCommand.addCommand(aiBackfillStructuredCmd);
2651
+ var scanSecretsCmd = new Command("scan-secrets").configureHelp(helpStyle).description("Sweep existing memories and redact any secrets found in stored content").option("--limit <n>", "max memories to scan in this run").option("--dry-run", "report detections without rewriting any rows");
2652
+ addDbOption(scanSecretsCmd);
2653
+ addJsonOption(scanSecretsCmd);
2654
+ scanSecretsCmd.action((opts) => {
2655
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
2656
+ try {
2657
+ const limit = parseOptionalPositiveInt$1(opts.limit);
2658
+ const result = scanSecretsRetroactive(store.db, {
2659
+ limit,
2660
+ dryRun: opts.dryRun === true,
2661
+ scanner: store.scanner
2662
+ });
2663
+ if (opts.json) {
2664
+ console.log(JSON.stringify(result, null, 2));
2665
+ return;
2666
+ }
2667
+ const action = opts.dryRun ? "Would redact" : "Redacted";
2668
+ p.intro("codemem db scan-secrets");
2669
+ p.log.success(`${action} ${result.updated} of ${result.checked} memories`);
2670
+ if (result.skippedOversized > 0) p.log.warn(`Skipped ${result.skippedOversized} oversized rows (above default 1 MiB cap)`);
2671
+ if (result.detections.length > 0) {
2672
+ const summary = result.detections.map((d) => `${d.kind}=${d.count}`).join(", ");
2673
+ p.log.info(`Detections: ${summary}`);
2674
+ }
2675
+ if (result.samples.length > 0) {
2676
+ const lines = result.samples.map((s) => {
2677
+ const kinds = s.detections.map((d) => `${d.kind}=${d.count}`).join(", ");
2678
+ const title = (s.redactedTitle ?? "").slice(0, 80);
2679
+ return ` #${s.id} [${kinds}] ${title}`;
2680
+ });
2681
+ p.log.info(`Affected memories:\n${lines.join("\n")}`);
2682
+ }
2683
+ p.outro("Re-run with no changes to confirm idempotency");
2684
+ } catch (error) {
2685
+ p.log.error(error instanceof Error ? error.message : String(error));
2686
+ process.exitCode = 1;
2687
+ } finally {
2688
+ store.close();
2689
+ }
2690
+ });
2691
+ dbCommand.addCommand(scanSecretsCmd);
2354
2692
  //#endregion
2355
2693
  //#region src/commands/embed.ts
2356
2694
  function parseOptionalPositiveInt(value) {
@@ -4734,7 +5072,10 @@ onceCmd.action(async (opts) => {
4734
5072
  let hadFailure = false;
4735
5073
  const results = [];
4736
5074
  for (const row of rows) {
4737
- const result = await runSyncPass(store.db, row.peer_device_id, { keysDir });
5075
+ const result = await runSyncPass(store.db, row.peer_device_id, {
5076
+ keysDir,
5077
+ scanner: store.scanner
5078
+ });
4738
5079
  if (!result.ok) hadFailure = true;
4739
5080
  results.push({
4740
5081
  peer_device_id: row.peer_device_id,
@@ -5103,7 +5444,8 @@ enableCmd.action((opts) => {
5103
5444
  const effectivePort = opts.syncPort ?? opts.port;
5104
5445
  const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
5105
5446
  try {
5106
- const [deviceId, fingerprint] = ensureDeviceIdentity(store.db);
5447
+ const keysDir = process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
5448
+ const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir });
5107
5449
  const config = readCliConfig(opts.config);
5108
5450
  config.sync_enabled = true;
5109
5451
  if (effectiveHost) config.sync_host = effectiveHost;
@@ -5359,7 +5701,7 @@ bootstrapCmd.action(async (peerArg, opts) => {
5359
5701
  bootstrapGrantId: opts.bootstrapGrant,
5360
5702
  pageSize
5361
5703
  });
5362
- const result = applyBootstrapSnapshot(store.db, peerDeviceId, items, resetInfo);
5704
+ const result = applyBootstrapSnapshot(store.db, peerDeviceId, items, resetInfo, store.scanner);
5363
5705
  if (opts.json) console.log(JSON.stringify({
5364
5706
  ok: result.ok,
5365
5707
  applied: result.applied,
@@ -5416,6 +5758,7 @@ var versionCommand = new Command("version").configureHelp(helpStyle).description
5416
5758
  var completion = omelette("codemem <command>");
5417
5759
  completion.on("command", ({ reply }) => {
5418
5760
  reply([
5761
+ "claude-hook-file-context",
5419
5762
  "claude-hook-inject",
5420
5763
  "claude-hook-ingest",
5421
5764
  "config",
@@ -5495,6 +5838,7 @@ program.addCommand(coordinatorCommand);
5495
5838
  program.addCommand(mcpCommand);
5496
5839
  program.addCommand(claudeHookInjectCommand);
5497
5840
  program.addCommand(claudeHookIngestCommand);
5841
+ program.addCommand(claudeHookFileContextCommand);
5498
5842
  program.addCommand(dbCommand);
5499
5843
  program.addCommand(exportMemoriesCommand);
5500
5844
  program.addCommand(importMemoriesCommand);