codemem 0.29.1 → 0.29.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +395 -51
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
|
83
|
-
* `claude-hook-ingest` to record
|
|
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$
|
|
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$
|
|
114
|
-
return expandHome$
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
629
|
+
logHookEvent("codemem claude-hook-ingest failed to spool payload");
|
|
372
630
|
return false;
|
|
373
631
|
}
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
684
|
+
logHookEvent(`codemem claude-hook-ingest quarantined corrupt spool payload (${reason}): ${quarantineName}`);
|
|
427
685
|
} catch {
|
|
428
686
|
try {
|
|
429
687
|
unlinkSync(sourcePath);
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1468
|
+
let pack = EMPTY_PACK;
|
|
1469
|
+
let origin = "none";
|
|
1190
1470
|
try {
|
|
1191
|
-
|
|
1471
|
+
pack = await buildPack(query, project, resolveDb(resolveDbOpt(opts)), workingSetPaths);
|
|
1472
|
+
if (pack.packText) origin = "local";
|
|
1192
1473
|
} catch (err) {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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);
|