codemem 0.29.0 → 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,23 +101,24 @@ var BOOLEAN_TOGGLE_VALUES = new Set([
101
101
  "on",
102
102
  "no"
103
103
  ]);
104
- function expandHome$2(value) {
105
- if (value === "~") return homedir();
106
- if (value.startsWith("~/")) return join(homedir(), value.slice(2));
104
+ function expandHome$3(value) {
105
+ const home = process.env.HOME?.trim() || homedir();
106
+ if (value === "~") return home;
107
+ if (value.startsWith("~/")) return join(home, value.slice(2));
107
108
  return value;
108
109
  }
109
110
  function pluginLogPath() {
110
111
  const raw = process.env.CODEMEM_PLUGIN_LOG_PATH ?? process.env.CODEMEM_PLUGIN_LOG ?? "";
111
112
  const normalized = raw.trim().toLowerCase();
112
- if (BOOLEAN_TOGGLE_VALUES.has(normalized)) return expandHome$2("~/.codemem/plugin.log");
113
- return expandHome$2(raw.trim());
113
+ if (BOOLEAN_TOGGLE_VALUES.has(normalized)) return expandHome$3("~/.codemem/plugin.log");
114
+ return expandHome$3(raw.trim());
114
115
  }
115
116
  /**
116
117
  * Append a single timestamped line to the plugin log. Best-effort: any
117
118
  * filesystem error is swallowed so a logging failure can never bubble up
118
119
  * into a Claude hook crash.
119
120
  */
120
- function logHookFailure(message) {
121
+ function logHookEvent(message) {
121
122
  const path = pluginLogPath();
122
123
  try {
123
124
  mkdirSync(dirname(path), { recursive: true });
@@ -125,6 +126,264 @@ function logHookFailure(message) {
125
126
  } catch {}
126
127
  }
127
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
128
387
  //#region src/commands/claude-hook-ingest-spool.ts
129
388
  /**
130
389
  * Durability layer for `claude-hook-ingest`: file-based mutex to
@@ -349,7 +608,7 @@ function spoolPayload(payload) {
349
608
  try {
350
609
  mkdirSync(dir, { recursive: true });
351
610
  } catch {
352
- logHookFailure("codemem claude-hook-ingest failed to create spool dir");
611
+ logHookEvent("codemem claude-hook-ingest failed to create spool dir");
353
612
  return false;
354
613
  }
355
614
  const payloadText = JSON.stringify(payload);
@@ -357,7 +616,7 @@ function spoolPayload(payload) {
357
616
  try {
358
617
  writeFileSync(tmpPath, payloadText, { encoding: "utf8" });
359
618
  } catch {
360
- logHookFailure("codemem claude-hook-ingest failed to allocate spool temp file");
619
+ logHookEvent("codemem claude-hook-ingest failed to allocate spool temp file");
361
620
  return false;
362
621
  }
363
622
  const finalPath = join(dir, `hook-${Math.floor(Date.now() / 1e3)}-${process.pid}-${randomInt(1e3, 1e4)}.json`);
@@ -367,10 +626,10 @@ function spoolPayload(payload) {
367
626
  try {
368
627
  unlinkSync(tmpPath);
369
628
  } catch {}
370
- logHookFailure("codemem claude-hook-ingest failed to spool payload");
629
+ logHookEvent("codemem claude-hook-ingest failed to spool payload");
371
630
  return false;
372
631
  }
373
- logHookFailure(`codemem claude-hook-ingest spooled payload: ${finalPath}`);
632
+ logHookEvent(`codemem claude-hook-ingest spooled payload: ${finalPath}`);
374
633
  return true;
375
634
  }
376
635
  /**
@@ -407,7 +666,7 @@ function recoverStaleTmpSpool(ttlSeconds) {
407
666
  const recoveredPath = join(dir, `hook-recovered-${Math.floor(nowS)}-${process.pid}-${randomInt(1e3, 1e4)}.json`);
408
667
  try {
409
668
  renameSync(tmpPath, recoveredPath);
410
- logHookFailure(`codemem claude-hook-ingest recovered stale temp spool payload: ${recoveredPath}`);
669
+ logHookEvent(`codemem claude-hook-ingest recovered stale temp spool payload: ${recoveredPath}`);
411
670
  } catch {}
412
671
  }
413
672
  }
@@ -422,11 +681,11 @@ function quarantineSpoolEntry(dir, name, reason) {
422
681
  const quarantineName = `.bad-${reason}-${Date.now()}-${randomInt(1e3, 1e4)}-${name}`;
423
682
  try {
424
683
  renameSync(sourcePath, join(dir, quarantineName));
425
- logHookFailure(`codemem claude-hook-ingest quarantined corrupt spool payload (${reason}): ${quarantineName}`);
684
+ logHookEvent(`codemem claude-hook-ingest quarantined corrupt spool payload (${reason}): ${quarantineName}`);
426
685
  } catch {
427
686
  try {
428
687
  unlinkSync(sourcePath);
429
- logHookFailure(`codemem claude-hook-ingest dropped corrupt spool payload (${reason}): ${name}`);
688
+ logHookEvent(`codemem claude-hook-ingest dropped corrupt spool payload (${reason}): ${name}`);
430
689
  } catch {}
431
690
  }
432
691
  }
@@ -468,7 +727,7 @@ async function drainSpool(handler) {
468
727
  try {
469
728
  raw = readFileSync(path, "utf8");
470
729
  } catch {
471
- logHookFailure(`codemem claude-hook-ingest failed to read spooled payload: ${path}`);
730
+ logHookEvent(`codemem claude-hook-ingest failed to read spooled payload: ${path}`);
472
731
  result.failed++;
473
732
  continue;
474
733
  }
@@ -495,7 +754,7 @@ async function drainSpool(handler) {
495
754
  result.processed++;
496
755
  } catch {}
497
756
  else {
498
- logHookFailure(`codemem claude-hook-ingest failed processing spooled payload: ${path}`);
757
+ logHookEvent(`codemem claude-hook-ingest failed processing spooled payload: ${path}`);
499
758
  result.failed++;
500
759
  }
501
760
  }
@@ -798,7 +1057,7 @@ async function tryHttpIngest(payload, host, port) {
798
1057
  try {
799
1058
  body = await res.json();
800
1059
  } catch {
801
- logHookFailure("codemem claude-hook-ingest HTTP accepted with invalid response body");
1060
+ logHookEvent("codemem claude-hook-ingest HTTP accepted with invalid response body");
802
1061
  return {
803
1062
  ok: false,
804
1063
  inserted: 0,
@@ -806,7 +1065,7 @@ async function tryHttpIngest(payload, host, port) {
806
1065
  };
807
1066
  }
808
1067
  if (body == null || typeof body !== "object" || Array.isArray(body)) {
809
- logHookFailure("codemem claude-hook-ingest HTTP accepted with invalid response type");
1068
+ logHookEvent("codemem claude-hook-ingest HTTP accepted with invalid response type");
810
1069
  return {
811
1070
  ok: false,
812
1071
  inserted: 0,
@@ -815,7 +1074,7 @@ async function tryHttpIngest(payload, host, port) {
815
1074
  }
816
1075
  const obj = body;
817
1076
  if (typeof obj.inserted !== "number" || typeof obj.skipped !== "number") {
818
- logHookFailure("codemem claude-hook-ingest HTTP accepted with unexpected response body");
1077
+ logHookEvent("codemem claude-hook-ingest HTTP accepted with unexpected response body");
819
1078
  return {
820
1079
  ok: false,
821
1080
  inserted: 0,
@@ -901,14 +1160,14 @@ async function flushBoundaryRawEvents(payload, dbPath) {
901
1160
  try {
902
1161
  observer = new ObserverClient();
903
1162
  } catch (err) {
904
- 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)}`);
905
1164
  return;
906
1165
  }
907
1166
  let store;
908
1167
  try {
909
1168
  store = new MemoryStore(dbPath);
910
1169
  } catch (err) {
911
- 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)}`);
912
1171
  return;
913
1172
  }
914
1173
  try {
@@ -921,7 +1180,7 @@ async function flushBoundaryRawEvents(payload, dbPath) {
921
1180
  maxEvents: null
922
1181
  });
923
1182
  } catch (err) {
924
- 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)}`);
925
1184
  } finally {
926
1185
  store.close();
927
1186
  }
@@ -952,7 +1211,7 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
952
1211
  result: directIngest(queued, getDbPath())
953
1212
  };
954
1213
  } catch (err) {
955
- 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)}`);
956
1215
  return { ok: false };
957
1216
  }
958
1217
  };
@@ -961,12 +1220,12 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
961
1220
  try {
962
1221
  directIngest(payload, getDbPath());
963
1222
  } catch (err) {
964
- 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)}`);
965
1224
  }
966
1225
  try {
967
1226
  await boundaryFlush(payload, getDbPath());
968
1227
  } catch (err) {
969
- 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)}`);
970
1229
  }
971
1230
  };
972
1231
  const drainBacklogIfPresent = async () => {
@@ -981,7 +1240,7 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
981
1240
  });
982
1241
  } catch (err) {
983
1242
  if (err instanceof LockBusyError) return;
984
- 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)}`);
985
1244
  }
986
1245
  };
987
1246
  const httpResult = await httpIngest(payload, opts.host, port);
@@ -1023,12 +1282,12 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
1023
1282
  skipped: 0,
1024
1283
  via: "spool"
1025
1284
  };
1026
- logHookFailure("codemem claude-hook-ingest failed: fallback and spool failed");
1285
+ logHookEvent("codemem claude-hook-ingest failed: fallback and spool failed");
1027
1286
  throw new Error("claude-hook-ingest: fallback and spool both failed");
1028
1287
  });
1029
1288
  } catch (err) {
1030
1289
  if (!(err instanceof LockBusyError)) throw err;
1031
- logHookFailure("codemem claude-hook-ingest lock busy; trying unlocked fallback");
1290
+ logHookEvent("codemem claude-hook-ingest lock busy; trying unlocked fallback");
1032
1291
  const direct = tryDirectFallback(payload);
1033
1292
  if (direct.ok) return {
1034
1293
  ...direct.result,
@@ -1039,7 +1298,7 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
1039
1298
  skipped: 0,
1040
1299
  via: "spool_lock_busy"
1041
1300
  };
1042
- logHookFailure("codemem claude-hook-ingest failed: unlocked fallback and spool failed");
1301
+ logHookEvent("codemem claude-hook-ingest failed: unlocked fallback and spool failed");
1043
1302
  throw err;
1044
1303
  }
1045
1304
  }
@@ -1085,6 +1344,11 @@ var claudeHookIngestCommand = claudeHookCmd.action(async (opts) => {
1085
1344
  //#endregion
1086
1345
  //#region src/commands/claude-hook-inject.ts
1087
1346
  var HOOK_EVENT_NAME = "UserPromptSubmit";
1347
+ var EMPTY_PACK = {
1348
+ packText: "",
1349
+ items: 0,
1350
+ packTokens: 0
1351
+ };
1088
1352
  var DEFAULT_VIEWER_HOST = "127.0.0.1";
1089
1353
  var DEFAULT_VIEWER_PORT = 38888;
1090
1354
  var DEFAULT_MAX_CHARS = 16e3;
@@ -1092,6 +1356,9 @@ var DEFAULT_HTTP_MAX_TIME_S = 2;
1092
1356
  function emitJson(value) {
1093
1357
  console.log(JSON.stringify(value));
1094
1358
  }
1359
+ function emitError(value) {
1360
+ process.stderr.write(`${JSON.stringify(value)}\n`);
1361
+ }
1095
1362
  function envNotDisabled(value) {
1096
1363
  const normalized = String(value ?? "").trim().toLowerCase();
1097
1364
  return normalized !== "0" && normalized !== "false" && normalized !== "off";
@@ -1136,12 +1403,21 @@ async function buildLocalPack(context, project, dbPath, workingSetPaths = []) {
1136
1403
  if (project) filters.project = project;
1137
1404
  if (workingSetPaths.length > 0) filters.working_set_paths = workingSetPaths;
1138
1405
  const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
1139
- 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
+ };
1140
1411
  } finally {
1141
1412
  store.close();
1142
1413
  }
1143
1414
  }
1144
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
+ };
1145
1421
  const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
1146
1422
  const port = parsePositiveInt$1(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
1147
1423
  const url = new URL(`http://${host}:${port}/api/pack`);
@@ -1153,11 +1429,15 @@ async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S
1153
1429
  const timeout = setTimeout(() => controller.abort(), maxTimeMs);
1154
1430
  try {
1155
1431
  const res = await fetch(url, { signal: controller.signal });
1156
- if (!res.ok) return "";
1432
+ if (!res.ok) return empty;
1157
1433
  const body = await res.json();
1158
- 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
+ };
1159
1439
  } catch {
1160
- return "";
1440
+ return empty;
1161
1441
  } finally {
1162
1442
  clearTimeout(timeout);
1163
1443
  }
@@ -1185,15 +1465,30 @@ async function buildClaudeHookInjection(payload, opts, deps = {}) {
1185
1465
  const workingSetPaths = workingSetPathsFromState(state);
1186
1466
  const maxChars = parsePositiveInt$1(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS);
1187
1467
  const httpMaxTimeMs = parsePositiveInt$1(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S) * 1e3;
1188
- let additionalContext = "";
1468
+ let pack = EMPTY_PACK;
1469
+ let origin = "none";
1189
1470
  try {
1190
- 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";
1191
1473
  } catch (err) {
1192
- additionalContext = "";
1193
- logHookFailure(`codemem claude-hook-inject local pack failed: ${err instanceof Error ? err.message : String(err)}`);
1194
- }
1195
- if (!additionalContext && envNotDisabled(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) additionalContext = await httpPack(query, project, httpMaxTimeMs);
1196
- 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));
1197
1492
  }
1198
1493
  var claudeHookInjectCmd = new Command("claude-hook-inject").configureHelp(helpStyle).description("Return Claude hook additionalContext from local pack generation");
1199
1494
  addDbOption(claudeHookInjectCmd);
@@ -1209,7 +1504,7 @@ var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
1209
1504
  try {
1210
1505
  const parsed = JSON.parse(trimmed);
1211
1506
  if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
1212
- emitJson({
1507
+ emitError({
1213
1508
  error: "parse_error",
1214
1509
  message: "payload must be a JSON object"
1215
1510
  });
@@ -1218,7 +1513,7 @@ var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
1218
1513
  }
1219
1514
  payload = parsed;
1220
1515
  } catch {
1221
- emitJson({
1516
+ emitError({
1222
1517
  error: "parse_error",
1223
1518
  message: "invalid JSON"
1224
1519
  });
@@ -2168,7 +2463,8 @@ backfillTagsCmd.action((opts) => {
2168
2463
  since: opts.since ?? null,
2169
2464
  project,
2170
2465
  activeOnly: !opts.inactive,
2171
- dryRun: opts.dryRun === true
2466
+ dryRun: opts.dryRun === true,
2467
+ scanner: store.scanner
2172
2468
  });
2173
2469
  if (opts.json) {
2174
2470
  console.log(JSON.stringify(result, null, 2));
@@ -2302,7 +2598,8 @@ backfillNarrativeCmd.action((opts) => {
2302
2598
  const limit = parseOptionalPositiveInt$1(opts.limit);
2303
2599
  const result = backfillNarrativeFromBody(store.db, {
2304
2600
  limit,
2305
- dryRun: opts.dryRun === true
2601
+ dryRun: opts.dryRun === true,
2602
+ scanner: store.scanner
2306
2603
  });
2307
2604
  if (opts.json) {
2308
2605
  console.log(JSON.stringify(result, null, 2));
@@ -2332,7 +2629,8 @@ aiBackfillStructuredCmd.action(async (opts) => {
2332
2629
  limit,
2333
2630
  kinds,
2334
2631
  overwrite: opts.overwrite === true,
2335
- dryRun: opts.dryRun === true
2632
+ dryRun: opts.dryRun === true,
2633
+ scanner: store.scanner
2336
2634
  });
2337
2635
  if (opts.json) {
2338
2636
  console.log(JSON.stringify(result, null, 2));
@@ -2350,6 +2648,47 @@ aiBackfillStructuredCmd.action(async (opts) => {
2350
2648
  }
2351
2649
  });
2352
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);
2353
2692
  //#endregion
2354
2693
  //#region src/commands/embed.ts
2355
2694
  function parseOptionalPositiveInt(value) {
@@ -4733,7 +5072,10 @@ onceCmd.action(async (opts) => {
4733
5072
  let hadFailure = false;
4734
5073
  const results = [];
4735
5074
  for (const row of rows) {
4736
- 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
+ });
4737
5079
  if (!result.ok) hadFailure = true;
4738
5080
  results.push({
4739
5081
  peer_device_id: row.peer_device_id,
@@ -5102,7 +5444,8 @@ enableCmd.action((opts) => {
5102
5444
  const effectivePort = opts.syncPort ?? opts.port;
5103
5445
  const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
5104
5446
  try {
5105
- 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 });
5106
5449
  const config = readCliConfig(opts.config);
5107
5450
  config.sync_enabled = true;
5108
5451
  if (effectiveHost) config.sync_host = effectiveHost;
@@ -5358,7 +5701,7 @@ bootstrapCmd.action(async (peerArg, opts) => {
5358
5701
  bootstrapGrantId: opts.bootstrapGrant,
5359
5702
  pageSize
5360
5703
  });
5361
- const result = applyBootstrapSnapshot(store.db, peerDeviceId, items, resetInfo);
5704
+ const result = applyBootstrapSnapshot(store.db, peerDeviceId, items, resetInfo, store.scanner);
5362
5705
  if (opts.json) console.log(JSON.stringify({
5363
5706
  ok: result.ok,
5364
5707
  applied: result.applied,
@@ -5415,6 +5758,7 @@ var versionCommand = new Command("version").configureHelp(helpStyle).description
5415
5758
  var completion = omelette("codemem <command>");
5416
5759
  completion.on("command", ({ reply }) => {
5417
5760
  reply([
5761
+ "claude-hook-file-context",
5418
5762
  "claude-hook-inject",
5419
5763
  "claude-hook-ingest",
5420
5764
  "config",
@@ -5494,6 +5838,7 @@ program.addCommand(coordinatorCommand);
5494
5838
  program.addCommand(mcpCommand);
5495
5839
  program.addCommand(claudeHookInjectCommand);
5496
5840
  program.addCommand(claudeHookIngestCommand);
5841
+ program.addCommand(claudeHookFileContextCommand);
5497
5842
  program.addCommand(dbCommand);
5498
5843
  program.addCommand(exportMemoriesCommand);
5499
5844
  program.addCommand(importMemoriesCommand);