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 +398 -53
- 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,23 +101,24 @@ var BOOLEAN_TOGGLE_VALUES = new Set([
|
|
|
101
101
|
"on",
|
|
102
102
|
"no"
|
|
103
103
|
]);
|
|
104
|
-
function expandHome$
|
|
105
|
-
|
|
106
|
-
if (value
|
|
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$
|
|
113
|
-
return expandHome$
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
629
|
+
logHookEvent("codemem claude-hook-ingest failed to spool payload");
|
|
371
630
|
return false;
|
|
372
631
|
}
|
|
373
|
-
|
|
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
|
-
|
|
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
|
-
|
|
684
|
+
logHookEvent(`codemem claude-hook-ingest quarantined corrupt spool payload (${reason}): ${quarantineName}`);
|
|
426
685
|
} catch {
|
|
427
686
|
try {
|
|
428
687
|
unlinkSync(sourcePath);
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1468
|
+
let pack = EMPTY_PACK;
|
|
1469
|
+
let origin = "none";
|
|
1189
1470
|
try {
|
|
1190
|
-
|
|
1471
|
+
pack = await buildPack(query, project, resolveDb(resolveDbOpt(opts)), workingSetPaths);
|
|
1472
|
+
if (pack.packText) origin = "local";
|
|
1191
1473
|
} catch (err) {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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);
|