codemem 0.47.0-beta.2 → 0.47.0-beta.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 CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { ACCOUNT_CONNECTION_MIGRATION_STATEMENTS, ACCOUNT_UNLINK_MIGRATION_STATEMENTS, AUTH_LINK_AUTHORITY_CUTOVER_SQL, ContextOnlyReplayError, CoordinatorAccountLinkError, DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, DeviceIdentityError, EXTRACTION_BENCHMARK_QUALITY_WEIGHTS, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, ObserverOutputError, ObserverOutputTransportError, OwnerProfileEnrollmentError, PROMPT_TRANSPORT_PROTOCOL_RANGE, REF_BACKFILL_JOB, RawEventIngestValidationError, RawEventSweeper, RefBackfillRunner, RemoteCoordinatorRequestError, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, TRUSTED_HOOK_MAPPER_OPTIONS, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, applyDistillRule, arePromptTransportProtocolRangesCompatible, atomicReplaceConfigFile, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildTieredObserverSelection, buildViewerIdentityTarget, classifyPromptTransportFailure, clonePromptPackAttempt, collectOperationalStatus, compareMemoryRoleReports, connect, connectReadOnly, coordinatorAuthControllerReviewAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantIdentityGroupAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorQualifyOwnerAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewInviteAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeDeviceAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, describePiObserverStatus, detectInstallKind, draftDistillRule, enrollOwnerProfile, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, expandUserPath, exportMemories, extractApplyPatchPaths, extractionReplayObserverIdentity, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getAttributionDiagnostics, getCodememConfigPath, getExtractionBenchmarkProfile, getExtractionModelPricing, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryArtifactReport, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getUpdateStatus, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, importPiSessions, ingestRawEvents, initDatabase, isAutomaticRecallMeasurement, isCoordinatorControllerAttestationReference, isEmbeddingDisabled, isExplicitUpdateInstallEligible, isReleaseVersionForChannel, judgeDistillReport, linkCoordinatorAccount, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadRuntimeSigningPublicKey, loadSqliteVec, mdnsEnabled, mutateCodememConfigFile, normalizeCoordinatorAccountLinkOrigin, normalizePromptTransportProtocolRange, observerBaseUrlForProviderOverride, planReplicationOpsAgePrune, probeCodememViewerLiveness, probeCodememViewerLiveness as probeCodememViewerLiveness$1, probeRequiredNativeRuntime, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readAccountLinkDevice, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorAccountLinkStatus, readCoordinatorOwnerReviewLocalEvidence, readCoordinatorSyncConfig, readImportPayload, recordAutomaticRecall, recordPromptPackArtifacts, recordPromptPackTerminal, recordRetrievalSurface, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveComparablePath, resolveDbPath, resolveHookProject, resolveKeyPaths, resolvePiAgentDir, resolvePiObserverConfig, resolveProject, resolveProjectRoot, resolveRetrievalSession, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, searchPiSessions, setPeerProjectFilter, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, upgradeCoordinatorAccountSchemaFile, upgradeCoordinatorAuthSchemaFile, vacuumDatabase, viewerUrl, writeCodememConfigFile } from "@codemem/core";
2
+ import { ACCOUNT_CONNECTION_MIGRATION_STATEMENTS, ACCOUNT_UNLINK_MIGRATION_STATEMENTS, AUTH_LINK_AUTHORITY_CUTOVER_SQL, ContextOnlyReplayError, CoordinatorAccountLinkError, DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, DeviceIdentityError, EXTRACTION_BENCHMARK_QUALITY_WEIGHTS, FILE_CONTEXT_TRANSPORT_MAX_BYTES, FILE_CONTEXT_TRANSPORT_PROFILE, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, ObserverOutputError, ObserverOutputTransportError, OwnerProfileEnrollmentError, PROMPT_TRANSPORT_PROTOCOL_RANGE, REF_BACKFILL_JOB, RawEventIngestValidationError, RawEventSweeper, RefBackfillRunner, RemoteCoordinatorRequestError, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, TRUSTED_HOOK_MAPPER_OPTIONS, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, applyDistillRule, arePromptTransportProtocolRangesCompatible, atomicReplaceConfigFile, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildTieredObserverSelection, buildViewerIdentityTarget, classifyPromptTransportFailure, clonePromptPackAttempt, collectOperationalStatus, compareMemoryRoleReports, connect, connectReadOnly, coordinatorAuthControllerReviewAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantIdentityGroupAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorQualifyOwnerAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewInviteAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeDeviceAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, createFileContextRunner, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, describePiObserverStatus, detectInstallKind, dispatchFileContextTransport, draftDistillRule, enrollOwnerProfile, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, expandUserPath, exportMemories, extractApplyPatchPaths, extractionReplayObserverIdentity, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getAttributionDiagnostics, getCodememConfigPath, getExtractionBenchmarkProfile, getExtractionModelPricing, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryArtifactReport, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getUpdateStatus, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, importPiSessions, ingestRawEvents, initDatabase, isAutomaticRecallMeasurement, isCoordinatorControllerAttestationReference, isEmbeddingDisabled, isExplicitUpdateInstallEligible, isReleaseVersionForChannel, judgeDistillReport, linkCoordinatorAccount, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadRuntimeSigningPublicKey, loadSqliteVec, mdnsEnabled, mutateCodememConfigFile, normalizeCoordinatorAccountLinkOrigin, normalizePromptTransportProtocolRange, observerBaseUrlForProviderOverride, parseFileContextTransportRequest, planReplicationOpsAgePrune, probeCodememViewerLiveness, probeCodememViewerLiveness as probeCodememViewerLiveness$1, probeRequiredNativeRuntime, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readAccountLinkDevice, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorAccountLinkStatus, readCoordinatorOwnerReviewLocalEvidence, readCoordinatorSyncConfig, readImportPayload, recordAutomaticRecall, recordPromptPackArtifacts, recordPromptPackTerminal, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveComparablePath, resolveDbPath, resolveHookProject, resolveKeyPaths, resolvePiAgentDir, resolvePiObserverConfig, resolveProject, resolveProjectRoot, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, searchPiSessions, setPeerProjectFilter, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, upgradeCoordinatorAccountSchemaFile, upgradeCoordinatorAuthSchemaFile, vacuumDatabase, viewerUrl, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
- import { randomInt, randomUUID } from "node:crypto";
6
5
  import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
6
  import { homedir, networkInterfaces } from "node:os";
8
7
  import { dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
9
- import { styleText } from "node:util";
8
+ import { isDeepStrictEqual, styleText } from "node:util";
9
+ import { randomInt, randomUUID } from "node:crypto";
10
10
  import * as p from "@clack/prompts";
11
11
  import { serve } from "@hono/node-server";
12
12
  import { execFileSync, spawn, spawnSync } from "node:child_process";
@@ -79,6 +79,54 @@ function emitJsonError(errorCode, message, exitCode = 1) {
79
79
  process.exitCode = exitCode;
80
80
  }
81
81
  //#endregion
82
+ //#region src/commands/claude-hook-file-context-transport.ts
83
+ /** Internal packaged-hook transport. Normal CLI invocation keeps its original guards/output. */
84
+ function runClaudeFileContextTransport(payload, opts) {
85
+ const parsed = parseFileContextTransportRequest(payload);
86
+ if (!parsed.success) throw new Error("invalid file-context transport request");
87
+ let request = parsed.data;
88
+ if ([
89
+ "1",
90
+ "true",
91
+ "yes",
92
+ "on"
93
+ ].includes(String(process.env.CODEMEM_PLUGIN_IGNORE ?? "").trim().toLowerCase())) return {
94
+ ok: true,
95
+ attempt_id: request.attempt_id,
96
+ request_id: request.request_id,
97
+ result: { continue: true },
98
+ delivery_pending: false
99
+ };
100
+ const disabled = (value) => [
101
+ "0",
102
+ "false",
103
+ "off"
104
+ ].includes(String(value ?? "").trim().toLowerCase());
105
+ if (disabled(process.env.CODEMEM_RETRIEVAL_LEDGER)) request = {
106
+ ...request,
107
+ ledger_enabled: false
108
+ };
109
+ if (request.action === "lookup" && disabled(process.env.CODEMEM_FILE_CONTEXT)) {
110
+ const { relative_path: _path, project: _project, file_mtime_ms: _mtime, ...base } = request;
111
+ request = {
112
+ ...base,
113
+ action: "skip",
114
+ reason: "file_context_disabled"
115
+ };
116
+ }
117
+ const dbPath = resolveDbPath(resolveDbOpt(opts));
118
+ if (resolve(dbPath) !== resolve(request.db_path) || !isDeepStrictEqual(buildViewerIdentityTarget(), request.identity_target)) throw new Error("file-context target mismatch");
119
+ const store = new MemoryStore(dbPath);
120
+ try {
121
+ if (!store.hasCurrentIdentity()) throw new Error("file-context identity changed");
122
+ return dispatchFileContextTransport(store, request);
123
+ } finally {
124
+ try {
125
+ store.close();
126
+ } catch {}
127
+ }
128
+ }
129
+ //#endregion
82
130
  //#region src/commands/claude-hook-plugin-log.ts
83
131
  /**
84
132
  * Append-only plugin event log used by `claude-hook-inject` and
@@ -130,24 +178,12 @@ function logHookEvent(message) {
130
178
  //#endregion
131
179
  //#region src/commands/claude-hook-file-context.ts
132
180
  var FILE_GATE_MIN_BYTES = 1500;
133
- var FETCH_LIMIT = 40;
134
- var DISPLAY_LIMIT = 15;
135
- var MTIME_FRESH_TOLERANCE_MS = 3e5;
136
181
  var SMALL_FILE_BYPASS_PATTERNS = [
137
182
  /\.(json|jsonc|toml|ya?ml)$/i,
138
183
  /\.env(\.|$)/i,
139
184
  /(^|\/)dockerfile(\.|$)/i,
140
185
  /\.config\.(js|ts|mjs|cjs|json)$/i
141
186
  ];
142
- var KIND_ICONS = {
143
- decision: "⚖️",
144
- bugfix: "🔴",
145
- feature: "🟢",
146
- refactor: "🔄",
147
- discovery: "🔵",
148
- change: "✅",
149
- exploration: "🔬"
150
- };
151
187
  function emitJson$2(value) {
152
188
  console.log(JSON.stringify(value));
153
189
  }
@@ -187,105 +223,6 @@ function statFile(absPath) {
187
223
  return null;
188
224
  }
189
225
  }
190
- function parseJsonArray(value) {
191
- if (!value) return [];
192
- try {
193
- const parsed = JSON.parse(value);
194
- if (!Array.isArray(parsed)) return [];
195
- return parsed.filter((item) => typeof item === "string");
196
- } catch {
197
- return [];
198
- }
199
- }
200
- function normalizePathForCompare(path) {
201
- return path.replace(/\\/g, "/");
202
- }
203
- function scoreRow(row, normalizedTarget, idx) {
204
- const filesModified = parseJsonArray(row.files_modified);
205
- const inModified = filesModified.some((f) => normalizePathForCompare(f) === normalizedTarget);
206
- let score = 0;
207
- if (inModified) score += 2;
208
- if (filesModified.length <= 1) score += 2;
209
- else if (filesModified.length <= 3) score += 1;
210
- return {
211
- row,
212
- score,
213
- idx
214
- };
215
- }
216
- function scoreAndDedupe(rows, targetPath, limit) {
217
- const normalizedTarget = normalizePathForCompare(targetPath);
218
- const scored = rows.map((row, idx) => scoreRow(row, normalizedTarget, idx));
219
- const bestPerSession = /* @__PURE__ */ new Map();
220
- for (const item of scored) {
221
- const existing = bestPerSession.get(item.row.session_id);
222
- if (!existing || item.score > existing.score || item.score === existing.score && item.idx < existing.idx) bestPerSession.set(item.row.session_id, item);
223
- }
224
- const deduped = Array.from(bestPerSession.values());
225
- deduped.sort((a, b) => b.score - a.score || a.idx - b.idx);
226
- return deduped.slice(0, limit).map((s) => s.row);
227
- }
228
- function compactTime(timeStr) {
229
- return timeStr.toLowerCase().replace(" am", "a").replace(" pm", "p");
230
- }
231
- function formatTime(epochMs) {
232
- return new Date(epochMs).toLocaleString("en-US", {
233
- hour: "numeric",
234
- minute: "2-digit",
235
- hour12: true
236
- });
237
- }
238
- function formatDate(epochMs) {
239
- return new Date(epochMs).toLocaleString("en-US", {
240
- month: "short",
241
- day: "numeric",
242
- year: "numeric"
243
- });
244
- }
245
- function formatTimeline(rows, filePath, staleness) {
246
- const safePath = filePath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
247
- const enriched = rows.map((row) => ({
248
- row,
249
- epochMs: Date.parse(row.created_at)
250
- })).filter((item) => Number.isFinite(item.epochMs) && item.epochMs > 0);
251
- const byDay = /* @__PURE__ */ new Map();
252
- for (const item of enriched) {
253
- const day = formatDate(item.epochMs);
254
- const bucket = byDay.get(day);
255
- if (bucket) bucket.push(item);
256
- else byDay.set(day, [item]);
257
- }
258
- const sortedDays = Array.from(byDay.entries()).sort((a, b) => {
259
- return Math.min(...a[1].map((i) => i.epochMs)) - Math.min(...b[1].map((i) => i.epochMs));
260
- });
261
- const ids = rows.map((r) => r.id);
262
- 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(", ")}]).`];
263
- if (staleness) {
264
- const driftMinutes = Math.max(1, Math.round((staleness.fileMtimeMs - staleness.newestObservationMs) / 6e4));
265
- 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.`);
266
- }
267
- for (const [day, dayItems] of sortedDays) {
268
- const chronological = [...dayItems].sort((a, b) => a.epochMs - b.epochMs);
269
- lines.push(`### ${day}`);
270
- for (const { row, epochMs } of chronological) {
271
- const title = (row.title || "Untitled").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
272
- const icon = KIND_ICONS[row.kind] ?? "❔";
273
- const time = compactTime(formatTime(epochMs));
274
- lines.push(`${row.id} ${time} ${icon} (${row.kind}) ${title}`);
275
- }
276
- }
277
- return lines.join("\n");
278
- }
279
- function queryByFile(dbPath, relativePath, project, limit) {
280
- const store = new MemoryStore(dbPath);
281
- try {
282
- const opts = { limit };
283
- if (project) opts.project = project;
284
- return store.findByFile(relativePath, opts);
285
- } finally {
286
- store.close();
287
- }
288
- }
289
226
  function resolveProject$1(payload) {
290
227
  const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
291
228
  return resolveHookProject(cwd, payload.project);
@@ -294,73 +231,45 @@ function sourceSessionId(payload) {
294
231
  const value = payload.session_id;
295
232
  return typeof value === "string" && value.trim() ? value.trim() : null;
296
233
  }
297
- async function buildClaudeFileContext(payload, opts, deps = {}) {
298
- if (envTruthy$4(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$2();
299
- const filePath = extractFilePath(payload);
300
- if (!filePath) return continueResult$2();
301
- const startedAt = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
302
- const attemptId = (deps.createAttemptId ?? randomUUID)();
303
- const resolveDb = deps.resolveDb ?? resolveDbPath;
234
+ function fileContextStore(opts, deps) {
304
235
  let resolvedDbPath = null;
305
236
  let activeStore = null;
306
- const getStore = () => {
307
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
308
- activeStore ??= (deps.createStore ?? ((dbPath) => new MemoryStore(dbPath)))(resolvedDbPath);
309
- return activeStore;
237
+ const dbPath = () => {
238
+ resolvedDbPath ??= (deps.resolveDb ?? resolveDbPath)(resolveDbOpt(opts));
239
+ return resolvedDbPath;
310
240
  };
311
- const finish = (result) => {
312
- try {
313
- activeStore?.close();
314
- } catch {}
315
- activeStore = null;
316
- return result;
317
- };
318
- const record = (attemptInput) => {
319
- if (!envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1")) return;
320
- try {
321
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
322
- const completedAt = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
323
- const ledgerInput = {
324
- ...attemptInput,
325
- attemptId,
326
- surface: "file_context",
327
- trigger: "automatic",
328
- startedAt: startedAt.toISOString(),
329
- completedAt: completedAt.toISOString(),
330
- latencyMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
331
- recorderVersion: "claude-file-context-v1",
332
- source: "claude",
333
- streamId: sourceSessionId(payload),
334
- sourceSessionId: sourceSessionId(payload),
335
- mode: "claude_pre_tool_use_read"
336
- };
337
- if (deps.recordAttempt) deps.recordAttempt(resolvedDbPath, ledgerInput);
338
- else {
339
- const store = getStore();
340
- recordRetrievalSurface(store.db, {
341
- ...ledgerInput,
342
- sessionId: resolveRetrievalSession(store.db, "claude", ledgerInput.sourceSessionId)
343
- });
344
- }
345
- } catch {}
346
- };
347
- const updateDelivery = (status) => {
348
- if (!envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1")) return;
349
- try {
350
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
351
- if (deps.updateDelivery) deps.updateDelivery(resolvedDbPath, attemptId, status);
352
- else tryUpdateRetrievalDelivery(getStore().db, attemptId, status);
353
- } catch {}
354
- };
355
- if (!envNotDisabled$3(process.env.CODEMEM_FILE_CONTEXT || "1")) {
356
- record({
357
- retrievalStatus: "skipped",
358
- deliveryStatus: "not_attempted",
359
- failureCode: "file_context_disabled",
360
- failureStage: "configuration"
361
- });
362
- return finish(continueResult$2());
363
- }
241
+ return {
242
+ dbPath,
243
+ getStore: () => {
244
+ activeStore ??= (deps.createStore ?? ((path) => new MemoryStore(path)))(dbPath());
245
+ return activeStore;
246
+ },
247
+ finish: (result) => {
248
+ try {
249
+ activeStore?.close();
250
+ } catch {}
251
+ activeStore = null;
252
+ return result;
253
+ }
254
+ };
255
+ }
256
+ function fileContextRunner(payload, store, deps) {
257
+ const findByFile = deps.queryByFile;
258
+ const recordAttempt = deps.recordAttempt;
259
+ const updateDelivery = deps.updateDelivery;
260
+ return createFileContextRunner({
261
+ getStore: store.getStore,
262
+ sourceSessionId: sourceSessionId(payload),
263
+ ledgerEnabled: () => envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1"),
264
+ now: deps.now,
265
+ createAttemptId: deps.createAttemptId,
266
+ log: logHookEvent,
267
+ ...findByFile ? { findByFile: (path, options) => findByFile(store.dbPath(), path, options.project ?? null, options.limit) } : {},
268
+ ...recordAttempt ? { recordAttempt: (input) => recordAttempt(store.dbPath(), input) } : {},
269
+ ...updateDelivery ? { updateDelivery: (attemptId, status) => updateDelivery(store.dbPath(), attemptId, status) } : {}
270
+ });
271
+ }
272
+ function fileContextPath(filePath, payload) {
364
273
  const cwd = typeof payload.cwd === "string" && payload.cwd.trim() ? payload.cwd : process.cwd();
365
274
  const expandedPath = expandHome$2(filePath);
366
275
  const absolutePath = isAbsolute(expandedPath) ? expandedPath : resolve(cwd, expandedPath);
@@ -368,133 +277,78 @@ async function buildClaudeFileContext(payload, opts, deps = {}) {
368
277
  const escapesCwd = relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath);
369
278
  if (!relativePath || escapesCwd) {
370
279
  logHookEvent(`file_context.skip reason=outside_cwd path=${JSON.stringify(filePath)} cwd=${JSON.stringify(cwd)}`);
371
- record({
372
- retrievalStatus: "skipped",
373
- deliveryStatus: "not_attempted",
374
- failureCode: "outside_cwd",
375
- failureStage: "path_validation"
376
- });
377
- return finish(continueResult$2());
280
+ return null;
378
281
  }
282
+ return {
283
+ absolutePath,
284
+ relativePath
285
+ };
286
+ }
287
+ async function buildClaudeFileContext(payload, opts, deps = {}) {
288
+ if (envTruthy$4(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$2();
289
+ const filePath = extractFilePath(payload);
290
+ if (!filePath) return continueResult$2();
291
+ const store = fileContextStore(opts, deps);
292
+ const runner = fileContextRunner(payload, store, deps);
293
+ if (!envNotDisabled$3(process.env.CODEMEM_FILE_CONTEXT || "1")) return store.finish(runner.skip({ reason: "file_context_disabled" }));
294
+ const path = fileContextPath(filePath, payload);
295
+ if (!path) return store.finish(runner.skip({ reason: "outside_cwd" }));
296
+ const { absolutePath, relativePath } = path;
379
297
  const minBytes = Number.parseInt(process.env.CODEMEM_FILE_CONTEXT_MIN_BYTES ?? `${FILE_GATE_MIN_BYTES}`, 10);
380
298
  const minBytesEffective = Number.isFinite(minBytes) && minBytes >= 0 ? minBytes : FILE_GATE_MIN_BYTES;
381
299
  const stat = (deps.statFile ?? statFile)(absolutePath);
382
300
  if (!stat) {
383
301
  logHookEvent(`file_context.skip reason=stat_failed path=${JSON.stringify(relativePath)}`);
384
- record({
385
- retrievalStatus: "skipped",
386
- deliveryStatus: "not_attempted",
387
- failureCode: "stat_failed",
388
- failureStage: "file_access",
302
+ return store.finish(runner.skip({
303
+ reason: "stat_failed",
389
304
  repositoryPaths: [relativePath]
390
- });
391
- return finish(continueResult$2());
305
+ }));
392
306
  }
393
307
  const bypassSizeGate = SMALL_FILE_BYPASS_PATTERNS.some((p) => p.test(relativePath));
394
308
  if (stat.sizeBytes < minBytesEffective && !bypassSizeGate) {
395
309
  logHookEvent(`file_context.skip reason=below_size_gate path=${JSON.stringify(relativePath)} size=${stat.sizeBytes} gate=${minBytesEffective}`);
396
- record({
397
- retrievalStatus: "skipped",
398
- deliveryStatus: "not_attempted",
399
- failureCode: "below_size_gate",
400
- failureStage: "size_gate",
401
- repositoryPaths: [relativePath]
402
- });
403
- return finish(continueResult$2());
404
- }
405
- const project = resolveProject$1(payload);
406
- const queryFn = deps.queryByFile ?? queryByFile;
407
- let rows = [];
408
- try {
409
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
410
- rows = deps.queryByFile ? queryFn(resolvedDbPath, relativePath, project, FETCH_LIMIT) : getStore().findByFile(relativePath, {
411
- limit: FETCH_LIMIT,
412
- ...project ? { project } : {}
413
- });
414
- } catch (err) {
415
- logHookEvent(`codemem claude-hook-file-context query failed: ${err instanceof Error ? err.message : String(err)}`);
416
- record({
417
- retrievalStatus: "failed",
418
- deliveryStatus: "not_attempted",
419
- failureCode: "query_failed",
420
- failureStage: "retrieval",
421
- project,
422
- filters: project ? { project } : void 0,
310
+ return store.finish(runner.skip({
311
+ reason: "below_size_gate",
423
312
  repositoryPaths: [relativePath]
424
- });
425
- return finish(continueResult$2());
426
- }
427
- if (rows.length === 0) {
428
- logHookEvent(`file_context.skip reason=no_observations path=${JSON.stringify(relativePath)} project=${JSON.stringify(project ?? "")}`);
429
- record({
430
- retrievalStatus: "no_results",
431
- deliveryStatus: "not_attempted",
432
- project,
433
- filters: project ? { project } : void 0,
434
- repositoryPaths: [relativePath]
435
- });
436
- return finish(continueResult$2());
437
- }
438
- const top = scoreAndDedupe(rows, relativePath, DISPLAY_LIMIT);
439
- if (top.length === 0) {
440
- logHookEvent(`file_context.skip reason=no_top_after_dedupe path=${JSON.stringify(relativePath)} candidates=${rows.length}`);
441
- record({
442
- retrievalStatus: "succeeded",
443
- deliveryStatus: "not_attempted",
444
- candidateIds: rows.map((row) => row.id),
445
- candidateCount: rows.length,
446
- selectedIds: [],
447
- failureCode: "no_top_after_dedupe",
448
- failureStage: "selection",
449
- project,
450
- filters: project ? { project } : void 0,
451
- repositoryPaths: [relativePath]
452
- });
453
- return finish(continueResult$2());
454
- }
455
- let staleness = null;
456
- if (stat.mtimeMs > 0) {
457
- const newestObservationMs = top.reduce((max, row) => {
458
- const epoch = Date.parse(row.created_at);
459
- return Number.isFinite(epoch) && epoch > max ? epoch : max;
460
- }, 0);
461
- if (newestObservationMs > 0 && stat.mtimeMs > newestObservationMs + MTIME_FRESH_TOLERANCE_MS) staleness = {
462
- fileMtimeMs: stat.mtimeMs,
463
- newestObservationMs
464
- };
465
- }
466
- record({
467
- retrievalStatus: "succeeded",
468
- deliveryStatus: "not_attempted",
469
- candidateIds: rows.map((row) => row.id),
470
- candidateCount: rows.length,
471
- selectedIds: top.map((row) => row.id),
472
- project,
473
- filters: project ? { project } : void 0,
474
- repositoryPaths: [relativePath]
475
- });
476
- let timeline;
477
- try {
478
- timeline = formatTimeline(top, relativePath, staleness);
479
- } catch {
480
- updateDelivery("failed");
481
- return finish(continueResult$2());
313
+ }));
482
314
  }
483
- logHookEvent(`file_context.ok path=${JSON.stringify(relativePath)} candidates=${rows.length} surfaced=${top.length} project=${JSON.stringify(project ?? "")} stale=${staleness ? "true" : "false"}`);
484
- updateDelivery("handed_off");
485
- return finish({ hookSpecificOutput: {
486
- hookEventName: "PreToolUse",
487
- permissionDecision: "allow",
488
- additionalContext: timeline
489
- } });
315
+ return store.finish(runner.run({
316
+ relativePath,
317
+ project: resolveProject$1(payload),
318
+ fileMtimeMs: stat.mtimeMs
319
+ }));
490
320
  }
491
321
  var claudeHookFileContextCmd = new Command("claude-hook-file-context").configureHelp(helpStyle).description("Return Claude PreToolUse:Read additionalContext from per-file observation timeline");
492
322
  addDbOption(claudeHookFileContextCmd);
323
+ claudeHookFileContextCmd.addOption(new Option("--transport-profile").hideHelp());
324
+ claudeHookFileContextCmd.addOption(new Option("--transport").hideHelp());
493
325
  var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts) => {
326
+ if (opts.transportProfile) {
327
+ process.stdout.write(`${JSON.stringify(FILE_CONTEXT_TRANSPORT_PROFILE)}\n`);
328
+ return;
329
+ }
494
330
  let raw = "";
495
- for await (const chunk of process.stdin) raw += String(chunk);
331
+ for await (const chunk of process.stdin) {
332
+ raw += String(chunk);
333
+ if (opts.transport && Buffer.byteLength(raw) > FILE_CONTEXT_TRANSPORT_MAX_BYTES) {
334
+ emitError$2({
335
+ error: "parse_error",
336
+ message: "transport payload too large"
337
+ });
338
+ process.exitCode = 1;
339
+ return;
340
+ }
341
+ }
496
342
  const trimmed = raw.trim();
497
343
  if (!trimmed) {
344
+ if (opts.transport) {
345
+ process.stdout.write(`${JSON.stringify({
346
+ error: "parse_error",
347
+ message: "transport request required"
348
+ })}\n`);
349
+ process.exitCode = 1;
350
+ return;
351
+ }
498
352
  emitJson$2(continueResult$2());
499
353
  return;
500
354
  }
@@ -518,8 +372,23 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
518
372
  process.exitCode = 1;
519
373
  return;
520
374
  }
521
- emitJson$2(await buildClaudeFileContext(payload, opts));
375
+ await emitFileContext(payload, opts);
522
376
  });
377
+ async function emitFileContext(payload, opts) {
378
+ try {
379
+ if (opts.transport) {
380
+ process.stdout.write(`${JSON.stringify(runClaudeFileContextTransport(payload, opts))}\n`);
381
+ return;
382
+ }
383
+ emitJson$2(await buildClaudeFileContext(payload, opts));
384
+ } catch {
385
+ emitError$2({
386
+ error: "file_context_failed",
387
+ message: "file context unavailable"
388
+ });
389
+ process.exitCode = 1;
390
+ }
391
+ }
523
392
  //#endregion
524
393
  //#region src/commands/hook-ingest-live-spool.ts
525
394
  function spoolLiveHookPayload(dir, payload, logPrefix) {