codemem 0.34.0 → 0.35.0
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 +799 -114
- 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, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, ensureDeviceIdentity, ensureSchemaBootstrapped, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, listMaintenanceJobs, listPerPeerScopeSyncState, 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";
|
|
2
|
+
import { DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, REF_BACKFILL_JOB, RawEventSweeper, RefBackfillRunner, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, ensureDeviceIdentity, ensureSchemaBootstrapped, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, listMaintenanceJobs, listPerPeerScopeSyncState, 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
6
|
import { homedir, networkInterfaces } from "node:os";
|
|
7
7
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { styleText } from "node:util";
|
|
9
|
-
import { randomInt } from "node:crypto";
|
|
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 { spawn, spawnSync } from "node:child_process";
|
|
@@ -101,7 +101,7 @@ var BOOLEAN_TOGGLE_VALUES = new Set([
|
|
|
101
101
|
"on",
|
|
102
102
|
"no"
|
|
103
103
|
]);
|
|
104
|
-
function expandHome$
|
|
104
|
+
function expandHome$4(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,8 +110,8 @@ function expandHome$3(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$4("~/.codemem/plugin.log");
|
|
114
|
+
return expandHome$4(raw.trim());
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
117
|
* Append a single timestamped line to the plugin log. Best-effort: any
|
|
@@ -146,24 +146,24 @@ var KIND_ICONS = {
|
|
|
146
146
|
change: "✅",
|
|
147
147
|
exploration: "🔬"
|
|
148
148
|
};
|
|
149
|
-
function emitJson$
|
|
149
|
+
function emitJson$2(value) {
|
|
150
150
|
console.log(JSON.stringify(value));
|
|
151
151
|
}
|
|
152
|
-
function emitError$
|
|
152
|
+
function emitError$2(value) {
|
|
153
153
|
process.stderr.write(`${JSON.stringify(value)}\n`);
|
|
154
154
|
}
|
|
155
|
-
function continueResult$
|
|
155
|
+
function continueResult$2() {
|
|
156
156
|
return { continue: true };
|
|
157
157
|
}
|
|
158
|
-
function envNotDisabled$
|
|
158
|
+
function envNotDisabled$2(value) {
|
|
159
159
|
const normalized = String(value ?? "").trim().toLowerCase();
|
|
160
160
|
return normalized !== "0" && normalized !== "false" && normalized !== "off";
|
|
161
161
|
}
|
|
162
|
-
function envTruthy$
|
|
162
|
+
function envTruthy$3(value) {
|
|
163
163
|
const normalized = String(value ?? "").trim().toLowerCase();
|
|
164
164
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
165
165
|
}
|
|
166
|
-
function expandHome$
|
|
166
|
+
function expandHome$3(value) {
|
|
167
167
|
if (value === "~") return homedir();
|
|
168
168
|
if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
|
|
169
169
|
return value;
|
|
@@ -288,30 +288,30 @@ function resolveProject$1(payload) {
|
|
|
288
288
|
return resolveHookProject(typeof payload.cwd === "string" ? payload.cwd : null, payload.project);
|
|
289
289
|
}
|
|
290
290
|
async function buildClaudeFileContext(payload, opts, deps = {}) {
|
|
291
|
-
if (envTruthy$
|
|
292
|
-
if (!envNotDisabled$
|
|
291
|
+
if (envTruthy$3(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$2();
|
|
292
|
+
if (!envNotDisabled$2(process.env.CODEMEM_FILE_CONTEXT || "1")) return continueResult$2();
|
|
293
293
|
const filePath = extractFilePath(payload);
|
|
294
|
-
if (!filePath) return continueResult$
|
|
294
|
+
if (!filePath) return continueResult$2();
|
|
295
295
|
const cwd = typeof payload.cwd === "string" && payload.cwd.trim() ? payload.cwd : process.cwd();
|
|
296
|
-
const expandedPath = expandHome$
|
|
296
|
+
const expandedPath = expandHome$3(filePath);
|
|
297
297
|
const absolutePath = isAbsolute(expandedPath) ? expandedPath : resolve(cwd, expandedPath);
|
|
298
298
|
const relativePath = relative(cwd, absolutePath).split(sep).join("/");
|
|
299
299
|
const escapesCwd = relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath);
|
|
300
300
|
if (!relativePath || escapesCwd) {
|
|
301
301
|
logHookEvent(`file_context.skip reason=outside_cwd path=${JSON.stringify(filePath)} cwd=${JSON.stringify(cwd)}`);
|
|
302
|
-
return continueResult$
|
|
302
|
+
return continueResult$2();
|
|
303
303
|
}
|
|
304
304
|
const minBytes = Number.parseInt(process.env.CODEMEM_FILE_CONTEXT_MIN_BYTES ?? `${FILE_GATE_MIN_BYTES}`, 10);
|
|
305
305
|
const minBytesEffective = Number.isFinite(minBytes) && minBytes >= 0 ? minBytes : FILE_GATE_MIN_BYTES;
|
|
306
306
|
const stat = (deps.statFile ?? statFile)(absolutePath);
|
|
307
307
|
if (!stat) {
|
|
308
308
|
logHookEvent(`file_context.skip reason=stat_failed path=${JSON.stringify(relativePath)}`);
|
|
309
|
-
return continueResult$
|
|
309
|
+
return continueResult$2();
|
|
310
310
|
}
|
|
311
311
|
const bypassSizeGate = SMALL_FILE_BYPASS_PATTERNS.some((p) => p.test(relativePath));
|
|
312
312
|
if (stat.sizeBytes < minBytesEffective && !bypassSizeGate) {
|
|
313
313
|
logHookEvent(`file_context.skip reason=below_size_gate path=${JSON.stringify(relativePath)} size=${stat.sizeBytes} gate=${minBytesEffective}`);
|
|
314
|
-
return continueResult$
|
|
314
|
+
return continueResult$2();
|
|
315
315
|
}
|
|
316
316
|
const project = resolveProject$1(payload);
|
|
317
317
|
const resolveDb = deps.resolveDb ?? resolveDbPath;
|
|
@@ -321,16 +321,16 @@ async function buildClaudeFileContext(payload, opts, deps = {}) {
|
|
|
321
321
|
rows = queryFn(resolveDb(resolveDbOpt(opts)), relativePath, project, FETCH_LIMIT);
|
|
322
322
|
} catch (err) {
|
|
323
323
|
logHookEvent(`codemem claude-hook-file-context query failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
324
|
-
return continueResult$
|
|
324
|
+
return continueResult$2();
|
|
325
325
|
}
|
|
326
326
|
if (rows.length === 0) {
|
|
327
327
|
logHookEvent(`file_context.skip reason=no_observations path=${JSON.stringify(relativePath)} project=${JSON.stringify(project ?? "")}`);
|
|
328
|
-
return continueResult$
|
|
328
|
+
return continueResult$2();
|
|
329
329
|
}
|
|
330
330
|
const top = scoreAndDedupe(rows, relativePath, DISPLAY_LIMIT);
|
|
331
331
|
if (top.length === 0) {
|
|
332
332
|
logHookEvent(`file_context.skip reason=no_top_after_dedupe path=${JSON.stringify(relativePath)} candidates=${rows.length}`);
|
|
333
|
-
return continueResult$
|
|
333
|
+
return continueResult$2();
|
|
334
334
|
}
|
|
335
335
|
let staleness = null;
|
|
336
336
|
if (stat.mtimeMs > 0) {
|
|
@@ -358,14 +358,14 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
|
|
|
358
358
|
for await (const chunk of process.stdin) raw += String(chunk);
|
|
359
359
|
const trimmed = raw.trim();
|
|
360
360
|
if (!trimmed) {
|
|
361
|
-
emitJson$
|
|
361
|
+
emitJson$2(continueResult$2());
|
|
362
362
|
return;
|
|
363
363
|
}
|
|
364
364
|
let payload;
|
|
365
365
|
try {
|
|
366
366
|
const parsed = JSON.parse(trimmed);
|
|
367
367
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
368
|
-
emitError$
|
|
368
|
+
emitError$2({
|
|
369
369
|
error: "parse_error",
|
|
370
370
|
message: "payload must be a JSON object"
|
|
371
371
|
});
|
|
@@ -374,14 +374,14 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
|
|
|
374
374
|
}
|
|
375
375
|
payload = parsed;
|
|
376
376
|
} catch {
|
|
377
|
-
emitError$
|
|
377
|
+
emitError$2({
|
|
378
378
|
error: "parse_error",
|
|
379
379
|
message: "invalid JSON"
|
|
380
380
|
});
|
|
381
381
|
process.exitCode = 1;
|
|
382
382
|
return;
|
|
383
383
|
}
|
|
384
|
-
emitJson$
|
|
384
|
+
emitJson$2(await buildClaudeFileContext(payload, opts));
|
|
385
385
|
});
|
|
386
386
|
//#endregion
|
|
387
387
|
//#region src/commands/claude-hook-ingest-spool.ts
|
|
@@ -391,28 +391,28 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
|
|
|
391
391
|
* payloads when both HTTP and direct ingestion paths fail, and a
|
|
392
392
|
* recovery routine that promotes stale temp files back into the queue.
|
|
393
393
|
*/
|
|
394
|
-
var DEFAULT_LOCK_TTL_S = 300;
|
|
395
|
-
var DEFAULT_LOCK_GRACE_S = 2;
|
|
396
|
-
var LOCK_ACQUIRE_ATTEMPTS = 100;
|
|
397
|
-
var LOCK_ACQUIRE_BACKOFF_MS = 50;
|
|
394
|
+
var DEFAULT_LOCK_TTL_S$1 = 300;
|
|
395
|
+
var DEFAULT_LOCK_GRACE_S$1 = 2;
|
|
396
|
+
var LOCK_ACQUIRE_ATTEMPTS$1 = 100;
|
|
397
|
+
var LOCK_ACQUIRE_BACKOFF_MS$1 = 50;
|
|
398
398
|
var LockBusyError = class extends Error {
|
|
399
399
|
constructor() {
|
|
400
400
|
super("claude-hook-ingest lock busy");
|
|
401
401
|
this.name = "LockBusyError";
|
|
402
402
|
}
|
|
403
403
|
};
|
|
404
|
-
function expandHome$
|
|
404
|
+
function expandHome$2(value) {
|
|
405
405
|
if (value === "~") return homedir();
|
|
406
406
|
if (value.startsWith("~/")) return join(homedir(), value.slice(2));
|
|
407
407
|
return value;
|
|
408
408
|
}
|
|
409
|
-
function envInt(name, fallback) {
|
|
409
|
+
function envInt$1(name, fallback) {
|
|
410
410
|
const raw = process.env[name];
|
|
411
411
|
if (raw === void 0) return fallback;
|
|
412
412
|
const parsed = Number.parseInt(raw, 10);
|
|
413
413
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
414
414
|
}
|
|
415
|
-
function envTruthy$
|
|
415
|
+
function envTruthy$2(name, fallback) {
|
|
416
416
|
const raw = process.env[name];
|
|
417
417
|
if (raw === void 0) return fallback;
|
|
418
418
|
const normalized = raw.trim().toLowerCase();
|
|
@@ -430,15 +430,15 @@ function envTruthy$1(name, fallback) {
|
|
|
430
430
|
].includes(normalized)) return false;
|
|
431
431
|
return fallback;
|
|
432
432
|
}
|
|
433
|
-
function lockConfig() {
|
|
433
|
+
function lockConfig$1() {
|
|
434
434
|
return {
|
|
435
|
-
lockDir: expandHome$
|
|
436
|
-
ttlSeconds: Math.max(1, envInt("CODEMEM_CLAUDE_HOOK_LOCK_TTL_S", DEFAULT_LOCK_TTL_S)),
|
|
437
|
-
graceSeconds: Math.max(1, envInt("CODEMEM_CLAUDE_HOOK_LOCK_GRACE_S", DEFAULT_LOCK_GRACE_S))
|
|
435
|
+
lockDir: expandHome$2(process.env.CODEMEM_CLAUDE_HOOK_LOCK_DIR?.trim() || "~/.codemem/claude-hook-ingest.lock"),
|
|
436
|
+
ttlSeconds: Math.max(1, envInt$1("CODEMEM_CLAUDE_HOOK_LOCK_TTL_S", DEFAULT_LOCK_TTL_S$1)),
|
|
437
|
+
graceSeconds: Math.max(1, envInt$1("CODEMEM_CLAUDE_HOOK_LOCK_GRACE_S", DEFAULT_LOCK_GRACE_S$1))
|
|
438
438
|
};
|
|
439
439
|
}
|
|
440
440
|
function spoolDir() {
|
|
441
|
-
return expandHome$
|
|
441
|
+
return expandHome$2(process.env.CODEMEM_CLAUDE_HOOK_SPOOL_DIR?.trim() || "~/.codemem/claude-hook-spool");
|
|
442
442
|
}
|
|
443
443
|
/**
|
|
444
444
|
* Cheap pre-check used by the unlocked HTTP-success path to decide
|
|
@@ -470,7 +470,7 @@ function readFileTrimmedOrEmpty(path) {
|
|
|
470
470
|
return "";
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
|
-
function readLockMetadata(lockDir) {
|
|
473
|
+
function readLockMetadata$1(lockDir) {
|
|
474
474
|
const pid = readFileTrimmedOrEmpty(join(lockDir, "pid"));
|
|
475
475
|
const owner = readFileTrimmedOrEmpty(join(lockDir, "owner"));
|
|
476
476
|
const tsRaw = readFileTrimmedOrEmpty(join(lockDir, "ts"));
|
|
@@ -481,7 +481,7 @@ function readLockMetadata(lockDir) {
|
|
|
481
481
|
owner
|
|
482
482
|
};
|
|
483
483
|
}
|
|
484
|
-
function isPidAlive(pidText) {
|
|
484
|
+
function isPidAlive$1(pidText) {
|
|
485
485
|
const pid = Number.parseInt(pidText, 10);
|
|
486
486
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
487
487
|
try {
|
|
@@ -491,11 +491,11 @@ function isPidAlive(pidText) {
|
|
|
491
491
|
return false;
|
|
492
492
|
}
|
|
493
493
|
}
|
|
494
|
-
function lockIsStale(cfg) {
|
|
495
|
-
const snapshot = readLockMetadata(cfg.lockDir);
|
|
494
|
+
function lockIsStale$1(cfg) {
|
|
495
|
+
const snapshot = readLockMetadata$1(cfg.lockDir);
|
|
496
496
|
const nowS = Math.floor(Date.now() / 1e3);
|
|
497
497
|
if (snapshot.pid) {
|
|
498
|
-
if (isPidAlive(snapshot.pid)) {
|
|
498
|
+
if (isPidAlive$1(snapshot.pid)) {
|
|
499
499
|
if (snapshot.ts === null) return {
|
|
500
500
|
stale: false,
|
|
501
501
|
snapshot
|
|
@@ -528,7 +528,7 @@ function lockIsStale(cfg) {
|
|
|
528
528
|
snapshot
|
|
529
529
|
};
|
|
530
530
|
}
|
|
531
|
-
function cleanupLockDir(lockDir) {
|
|
531
|
+
function cleanupLockDir$1(lockDir) {
|
|
532
532
|
for (const name of [
|
|
533
533
|
"pid",
|
|
534
534
|
"ts",
|
|
@@ -543,13 +543,13 @@ function cleanupLockDir(lockDir) {
|
|
|
543
543
|
function snapshotsEqual(a, b) {
|
|
544
544
|
return a.pid === b.pid && a.ts === b.ts && a.owner === b.owner;
|
|
545
545
|
}
|
|
546
|
-
function cleanupLockDirIfUnchanged(lockDir, snapshot) {
|
|
547
|
-
if (snapshotsEqual(readLockMetadata(lockDir), snapshot)) cleanupLockDir(lockDir);
|
|
546
|
+
function cleanupLockDirIfUnchanged$1(lockDir, snapshot) {
|
|
547
|
+
if (snapshotsEqual(readLockMetadata$1(lockDir), snapshot)) cleanupLockDir$1(lockDir);
|
|
548
548
|
}
|
|
549
|
-
function isErrnoException(err) {
|
|
549
|
+
function isErrnoException$1(err) {
|
|
550
550
|
return typeof err === "object" && err !== null && "code" in err;
|
|
551
551
|
}
|
|
552
|
-
async function sleep(ms) {
|
|
552
|
+
async function sleep$1(ms) {
|
|
553
553
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
554
554
|
}
|
|
555
555
|
/**
|
|
@@ -563,21 +563,21 @@ async function sleep(ms) {
|
|
|
563
563
|
* race between mkdir and writing pid/ts.
|
|
564
564
|
*/
|
|
565
565
|
async function withClaudeHookIngestLock(fn) {
|
|
566
|
-
const cfg = lockConfig();
|
|
566
|
+
const cfg = lockConfig$1();
|
|
567
567
|
mkdirSync(dirname(cfg.lockDir), { recursive: true });
|
|
568
568
|
const ownerToken = `${process.pid}-${Math.floor(Date.now() / 1e3)}-${randomInt(1e3, 1e4)}`;
|
|
569
569
|
let acquired = false;
|
|
570
|
-
for (let attempt = 0; attempt < LOCK_ACQUIRE_ATTEMPTS; attempt++) {
|
|
570
|
+
for (let attempt = 0; attempt < LOCK_ACQUIRE_ATTEMPTS$1; attempt++) {
|
|
571
571
|
try {
|
|
572
572
|
mkdirSync(cfg.lockDir);
|
|
573
573
|
} catch (err) {
|
|
574
|
-
if (isErrnoException(err) && err.code === "EEXIST") {
|
|
575
|
-
const { stale, snapshot } = lockIsStale(cfg);
|
|
576
|
-
if (stale) cleanupLockDirIfUnchanged(cfg.lockDir, snapshot);
|
|
577
|
-
await sleep(LOCK_ACQUIRE_BACKOFF_MS);
|
|
574
|
+
if (isErrnoException$1(err) && err.code === "EEXIST") {
|
|
575
|
+
const { stale, snapshot } = lockIsStale$1(cfg);
|
|
576
|
+
if (stale) cleanupLockDirIfUnchanged$1(cfg.lockDir, snapshot);
|
|
577
|
+
await sleep$1(LOCK_ACQUIRE_BACKOFF_MS$1);
|
|
578
578
|
continue;
|
|
579
579
|
}
|
|
580
|
-
await sleep(LOCK_ACQUIRE_BACKOFF_MS);
|
|
580
|
+
await sleep$1(LOCK_ACQUIRE_BACKOFF_MS$1);
|
|
581
581
|
continue;
|
|
582
582
|
}
|
|
583
583
|
try {
|
|
@@ -587,15 +587,15 @@ async function withClaudeHookIngestLock(fn) {
|
|
|
587
587
|
acquired = true;
|
|
588
588
|
break;
|
|
589
589
|
} catch {
|
|
590
|
-
cleanupLockDir(cfg.lockDir);
|
|
591
|
-
await sleep(LOCK_ACQUIRE_BACKOFF_MS);
|
|
590
|
+
cleanupLockDir$1(cfg.lockDir);
|
|
591
|
+
await sleep$1(LOCK_ACQUIRE_BACKOFF_MS$1);
|
|
592
592
|
}
|
|
593
593
|
}
|
|
594
594
|
if (!acquired) throw new LockBusyError();
|
|
595
595
|
try {
|
|
596
596
|
return await fn();
|
|
597
597
|
} finally {
|
|
598
|
-
if (readFileTrimmedOrEmpty(join(cfg.lockDir, "owner")) === ownerToken) cleanupLockDir(cfg.lockDir);
|
|
598
|
+
if (readFileTrimmedOrEmpty(join(cfg.lockDir, "owner")) === ownerToken) cleanupLockDir$1(cfg.lockDir);
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
601
|
/**
|
|
@@ -676,7 +676,7 @@ function recoverStaleTmpSpool(ttlSeconds) {
|
|
|
676
676
|
* place with a `.bad-<reason>-` prefix so an operator can inspect or
|
|
677
677
|
* delete it manually.
|
|
678
678
|
*/
|
|
679
|
-
function quarantineSpoolEntry(dir, name, reason) {
|
|
679
|
+
function quarantineSpoolEntry$1(dir, name, reason) {
|
|
680
680
|
const sourcePath = join(dir, name);
|
|
681
681
|
const quarantineName = `.bad-${reason}-${Date.now()}-${randomInt(1e3, 1e4)}-${name}`;
|
|
682
682
|
try {
|
|
@@ -735,12 +735,12 @@ async function drainSpool(handler) {
|
|
|
735
735
|
try {
|
|
736
736
|
parsed = JSON.parse(raw);
|
|
737
737
|
} catch {
|
|
738
|
-
quarantineSpoolEntry(dir, name, "parse-error");
|
|
738
|
+
quarantineSpoolEntry$1(dir, name, "parse-error");
|
|
739
739
|
result.failed++;
|
|
740
740
|
continue;
|
|
741
741
|
}
|
|
742
742
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
743
|
-
quarantineSpoolEntry(dir, name, "wrong-shape");
|
|
743
|
+
quarantineSpoolEntry$1(dir, name, "wrong-shape");
|
|
744
744
|
continue;
|
|
745
745
|
}
|
|
746
746
|
let ok = false;
|
|
@@ -769,9 +769,9 @@ async function drainSpool(handler) {
|
|
|
769
769
|
function shouldForceBoundaryFlush(payload) {
|
|
770
770
|
const eventName = typeof payload.hook_event_name === "string" ? payload.hook_event_name.trim() : "";
|
|
771
771
|
if (eventName !== "Stop" && eventName !== "SessionEnd") return false;
|
|
772
|
-
if (eventName === "SessionEnd") return envTruthy$
|
|
773
|
-
if (!envTruthy$
|
|
774
|
-
return envTruthy$
|
|
772
|
+
if (eventName === "SessionEnd") return envTruthy$2("CODEMEM_CLAUDE_HOOK_FLUSH", true);
|
|
773
|
+
if (!envTruthy$2("CODEMEM_CLAUDE_HOOK_FLUSH", false)) return false;
|
|
774
|
+
return envTruthy$2("CODEMEM_CLAUDE_HOOK_FLUSH_ON_STOP", false);
|
|
775
775
|
}
|
|
776
776
|
/**
|
|
777
777
|
* Returns the configured lock TTL so callers (`claude-hook-ingest`)
|
|
@@ -779,7 +779,7 @@ function shouldForceBoundaryFlush(payload) {
|
|
|
779
779
|
* the env.
|
|
780
780
|
*/
|
|
781
781
|
function lockTtlSeconds() {
|
|
782
|
-
return lockConfig().ttlSeconds;
|
|
782
|
+
return lockConfig$1().ttlSeconds;
|
|
783
783
|
}
|
|
784
784
|
//#endregion
|
|
785
785
|
//#region src/commands/claude-hook-session-state.ts
|
|
@@ -812,14 +812,14 @@ function defaultSessionState() {
|
|
|
812
812
|
updated_at: ""
|
|
813
813
|
};
|
|
814
814
|
}
|
|
815
|
-
function expandHome(value) {
|
|
815
|
+
function expandHome$1(value) {
|
|
816
816
|
if (value === "~") return homedir();
|
|
817
817
|
if (value.startsWith("~/")) return join(homedir(), value.slice(2));
|
|
818
818
|
return value;
|
|
819
819
|
}
|
|
820
820
|
function contextDir() {
|
|
821
821
|
const override = process.env.CODEMEM_CLAUDE_HOOK_CONTEXT_DIR;
|
|
822
|
-
return expandHome(override?.trim() ? override : "~/.codemem/claude-hook-context");
|
|
822
|
+
return expandHome$1(override?.trim() ? override : "~/.codemem/claude-hook-context");
|
|
823
823
|
}
|
|
824
824
|
function sessionFileStem(sessionId) {
|
|
825
825
|
const trimmed = sessionId.trim();
|
|
@@ -1014,7 +1014,7 @@ function workingSetPathsFromState(state) {
|
|
|
1014
1014
|
* echo '{"hook_event_name":"Stop","session_id":"...","last_assistant_message":"..."}' \
|
|
1015
1015
|
* | codemem claude-hook-ingest
|
|
1016
1016
|
*/
|
|
1017
|
-
function emitStructuredError$
|
|
1017
|
+
function emitStructuredError$2(errorCode, message) {
|
|
1018
1018
|
console.log(JSON.stringify({
|
|
1019
1019
|
error: errorCode,
|
|
1020
1020
|
message
|
|
@@ -1037,7 +1037,7 @@ function emitStructuredError$1(errorCode, message) {
|
|
|
1037
1037
|
* transient, we'll need a reason field in the response and updated
|
|
1038
1038
|
* client handling — not an unconditional fail-over.
|
|
1039
1039
|
*/
|
|
1040
|
-
async function tryHttpIngest(payload, host, port) {
|
|
1040
|
+
async function tryHttpIngest$1(payload, host, port) {
|
|
1041
1041
|
const url = `http://${host}:${port}/api/claude-hooks`;
|
|
1042
1042
|
const controller = new AbortController();
|
|
1043
1043
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
@@ -1191,7 +1191,7 @@ async function flushBoundaryRawEvents(payload, dbPath) {
|
|
|
1191
1191
|
* disk spool durability.
|
|
1192
1192
|
*/
|
|
1193
1193
|
async function ingestClaudeHookPayload(payload, opts, deps = {}) {
|
|
1194
|
-
const httpIngest = deps.httpIngest ?? tryHttpIngest;
|
|
1194
|
+
const httpIngest = deps.httpIngest ?? tryHttpIngest$1;
|
|
1195
1195
|
const directIngest = deps.directIngest ?? directEnqueue;
|
|
1196
1196
|
const resolveDb = deps.resolveDb ?? resolveDbPath;
|
|
1197
1197
|
const boundaryFlush = deps.boundaryFlush ?? flushBoundaryRawEvents;
|
|
@@ -1305,84 +1305,84 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
|
|
|
1305
1305
|
var claudeHookCmd = new Command("claude-hook-ingest").configureHelp(helpStyle).description("Ingest Claude hook payload: HTTP first, direct DB fallback");
|
|
1306
1306
|
addDbOption(claudeHookCmd);
|
|
1307
1307
|
addViewerHostOptions(claudeHookCmd);
|
|
1308
|
-
function envTruthyValue(value) {
|
|
1308
|
+
function envTruthyValue$1(value) {
|
|
1309
1309
|
const normalized = String(value ?? "").trim().toLowerCase();
|
|
1310
1310
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
1311
1311
|
}
|
|
1312
1312
|
var claudeHookIngestCommand = claudeHookCmd.action(async (opts) => {
|
|
1313
|
-
if (envTruthyValue(process.env.CODEMEM_PLUGIN_IGNORE)) return;
|
|
1313
|
+
if (envTruthyValue$1(process.env.CODEMEM_PLUGIN_IGNORE)) return;
|
|
1314
1314
|
let raw;
|
|
1315
1315
|
try {
|
|
1316
1316
|
raw = readFileSync(0, "utf8").trim();
|
|
1317
1317
|
} catch {
|
|
1318
|
-
emitStructuredError$
|
|
1318
|
+
emitStructuredError$2("read_error", "failed to read stdin");
|
|
1319
1319
|
return;
|
|
1320
1320
|
}
|
|
1321
1321
|
if (!raw) {
|
|
1322
|
-
emitStructuredError$
|
|
1322
|
+
emitStructuredError$2("read_error", "empty stdin");
|
|
1323
1323
|
return;
|
|
1324
1324
|
}
|
|
1325
1325
|
let payload;
|
|
1326
1326
|
try {
|
|
1327
1327
|
const parsed = JSON.parse(raw);
|
|
1328
1328
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1329
|
-
emitStructuredError$
|
|
1329
|
+
emitStructuredError$2("parse_error", "payload must be a JSON object");
|
|
1330
1330
|
return;
|
|
1331
1331
|
}
|
|
1332
1332
|
payload = parsed;
|
|
1333
1333
|
} catch {
|
|
1334
|
-
emitStructuredError$
|
|
1334
|
+
emitStructuredError$2("parse_error", "invalid JSON");
|
|
1335
1335
|
return;
|
|
1336
1336
|
}
|
|
1337
1337
|
try {
|
|
1338
1338
|
const result = await ingestClaudeHookPayload(payload, opts);
|
|
1339
1339
|
console.log(JSON.stringify(result));
|
|
1340
1340
|
} catch (err) {
|
|
1341
|
-
emitStructuredError$
|
|
1341
|
+
emitStructuredError$2("ingest_error", err instanceof Error ? err.message : String(err));
|
|
1342
1342
|
}
|
|
1343
1343
|
});
|
|
1344
1344
|
//#endregion
|
|
1345
1345
|
//#region src/commands/claude-hook-inject.ts
|
|
1346
|
-
var HOOK_EVENT_NAME = "UserPromptSubmit";
|
|
1347
|
-
var EMPTY_PACK = {
|
|
1346
|
+
var HOOK_EVENT_NAME$1 = "UserPromptSubmit";
|
|
1347
|
+
var EMPTY_PACK$1 = {
|
|
1348
1348
|
packText: "",
|
|
1349
1349
|
items: 0,
|
|
1350
1350
|
packTokens: 0
|
|
1351
1351
|
};
|
|
1352
|
-
var DEFAULT_VIEWER_HOST = "127.0.0.1";
|
|
1353
|
-
var DEFAULT_VIEWER_PORT = 38888;
|
|
1354
|
-
var DEFAULT_MAX_CHARS = 16e3;
|
|
1355
|
-
var DEFAULT_HTTP_MAX_TIME_S = 2;
|
|
1356
|
-
function emitJson(value) {
|
|
1352
|
+
var DEFAULT_VIEWER_HOST$1 = "127.0.0.1";
|
|
1353
|
+
var DEFAULT_VIEWER_PORT$1 = 38888;
|
|
1354
|
+
var DEFAULT_MAX_CHARS$1 = 16e3;
|
|
1355
|
+
var DEFAULT_HTTP_MAX_TIME_S$1 = 2;
|
|
1356
|
+
function emitJson$1(value) {
|
|
1357
1357
|
console.log(JSON.stringify(value));
|
|
1358
1358
|
}
|
|
1359
|
-
function emitError(value) {
|
|
1359
|
+
function emitError$1(value) {
|
|
1360
1360
|
process.stderr.write(`${JSON.stringify(value)}\n`);
|
|
1361
1361
|
}
|
|
1362
|
-
function envNotDisabled(value) {
|
|
1362
|
+
function envNotDisabled$1(value) {
|
|
1363
1363
|
const normalized = String(value ?? "").trim().toLowerCase();
|
|
1364
1364
|
return normalized !== "0" && normalized !== "false" && normalized !== "off";
|
|
1365
1365
|
}
|
|
1366
|
-
function envTruthy(value) {
|
|
1366
|
+
function envTruthy$1(value) {
|
|
1367
1367
|
const normalized = String(value ?? "").trim().toLowerCase();
|
|
1368
1368
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
1369
1369
|
}
|
|
1370
|
-
function parsePositiveInt$
|
|
1370
|
+
function parsePositiveInt$2(value, fallback) {
|
|
1371
1371
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
1372
1372
|
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
1373
1373
|
return parsed;
|
|
1374
1374
|
}
|
|
1375
|
-
function continueResult(additionalContext) {
|
|
1375
|
+
function continueResult$1(additionalContext) {
|
|
1376
1376
|
if (!additionalContext) return { continue: true };
|
|
1377
1377
|
return {
|
|
1378
1378
|
continue: true,
|
|
1379
1379
|
hookSpecificOutput: {
|
|
1380
|
-
hookEventName: HOOK_EVENT_NAME,
|
|
1380
|
+
hookEventName: HOOK_EVENT_NAME$1,
|
|
1381
1381
|
additionalContext
|
|
1382
1382
|
}
|
|
1383
1383
|
};
|
|
1384
1384
|
}
|
|
1385
|
-
function truncateAdditionalContext(text, maxChars) {
|
|
1385
|
+
function truncateAdditionalContext$1(text, maxChars) {
|
|
1386
1386
|
const normalized = text.trim();
|
|
1387
1387
|
if (!normalized) return "";
|
|
1388
1388
|
if (!Number.isFinite(maxChars) || maxChars <= 0 || normalized.length <= maxChars) return normalized;
|
|
@@ -1391,14 +1391,14 @@ function truncateAdditionalContext(text, maxChars) {
|
|
|
1391
1391
|
function extractInjectContext(payload) {
|
|
1392
1392
|
return normalizePromptText(payload.prompt) || null;
|
|
1393
1393
|
}
|
|
1394
|
-
function resolveInjectProject(payload) {
|
|
1394
|
+
function resolveInjectProject$1(payload) {
|
|
1395
1395
|
return resolveHookProject(typeof payload.cwd === "string" ? payload.cwd : null, payload.project);
|
|
1396
1396
|
}
|
|
1397
|
-
async function buildLocalPack(context, project, dbPath, workingSetPaths = []) {
|
|
1397
|
+
async function buildLocalPack$1(context, project, dbPath, workingSetPaths = []) {
|
|
1398
1398
|
const store = new MemoryStore(dbPath);
|
|
1399
1399
|
try {
|
|
1400
|
-
const limit = parsePositiveInt$
|
|
1401
|
-
const budget = parsePositiveInt$
|
|
1400
|
+
const limit = parsePositiveInt$2(process.env.CODEMEM_INJECT_LIMIT, 8);
|
|
1401
|
+
const budget = parsePositiveInt$2(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
|
|
1402
1402
|
const filters = {};
|
|
1403
1403
|
if (project) filters.project = project;
|
|
1404
1404
|
if (workingSetPaths.length > 0) filters.working_set_paths = workingSetPaths;
|
|
@@ -1412,18 +1412,18 @@ async function buildLocalPack(context, project, dbPath, workingSetPaths = []) {
|
|
|
1412
1412
|
store.close();
|
|
1413
1413
|
}
|
|
1414
1414
|
}
|
|
1415
|
-
async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S * 1e3) {
|
|
1415
|
+
async function tryHttpPack$1(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S$1 * 1e3) {
|
|
1416
1416
|
const empty = {
|
|
1417
1417
|
packText: "",
|
|
1418
1418
|
items: 0,
|
|
1419
1419
|
packTokens: 0
|
|
1420
1420
|
};
|
|
1421
|
-
const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
|
|
1422
|
-
const port = parsePositiveInt$
|
|
1421
|
+
const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST$1;
|
|
1422
|
+
const port = parsePositiveInt$2(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT$1);
|
|
1423
1423
|
const url = new URL(`http://${host}:${port}/api/pack`);
|
|
1424
1424
|
url.searchParams.set("context", context);
|
|
1425
|
-
url.searchParams.set("limit", String(parsePositiveInt$
|
|
1426
|
-
url.searchParams.set("token_budget", String(parsePositiveInt$
|
|
1425
|
+
url.searchParams.set("limit", String(parsePositiveInt$2(process.env.CODEMEM_INJECT_LIMIT, 8)));
|
|
1426
|
+
url.searchParams.set("token_budget", String(parsePositiveInt$2(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800)));
|
|
1427
1427
|
if (project) url.searchParams.set("project", project);
|
|
1428
1428
|
const controller = new AbortController();
|
|
1429
1429
|
const timeout = setTimeout(() => controller.abort(), maxTimeMs);
|
|
@@ -1443,8 +1443,8 @@ async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S
|
|
|
1443
1443
|
}
|
|
1444
1444
|
}
|
|
1445
1445
|
async function buildClaudeHookInjection(payload, opts, deps = {}) {
|
|
1446
|
-
if (envTruthy(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult();
|
|
1447
|
-
if (!envNotDisabled(process.env.CODEMEM_INJECT_CONTEXT || "1")) return continueResult();
|
|
1446
|
+
if (envTruthy$1(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$1();
|
|
1447
|
+
if (!envNotDisabled$1(process.env.CODEMEM_INJECT_CONTEXT || "1")) return continueResult$1();
|
|
1448
1448
|
let state = null;
|
|
1449
1449
|
try {
|
|
1450
1450
|
state = trackHookSessionState(payload);
|
|
@@ -1452,20 +1452,20 @@ async function buildClaudeHookInjection(payload, opts, deps = {}) {
|
|
|
1452
1452
|
state = null;
|
|
1453
1453
|
}
|
|
1454
1454
|
const promptText = extractInjectContext(payload);
|
|
1455
|
-
if (!promptText) return continueResult();
|
|
1456
|
-
const buildPack = deps.buildLocalPack ?? buildLocalPack;
|
|
1457
|
-
const httpPack = deps.httpPack ?? tryHttpPack;
|
|
1455
|
+
if (!promptText) return continueResult$1();
|
|
1456
|
+
const buildPack = deps.buildLocalPack ?? buildLocalPack$1;
|
|
1457
|
+
const httpPack = deps.httpPack ?? tryHttpPack$1;
|
|
1458
1458
|
const resolveDb = deps.resolveDb ?? resolveDbPath;
|
|
1459
|
-
const project = resolveInjectProject(payload);
|
|
1459
|
+
const project = resolveInjectProject$1(payload);
|
|
1460
1460
|
const query = buildInjectQuery({
|
|
1461
1461
|
prompt: promptText,
|
|
1462
1462
|
project,
|
|
1463
1463
|
state
|
|
1464
1464
|
});
|
|
1465
1465
|
const workingSetPaths = workingSetPathsFromState(state);
|
|
1466
|
-
const maxChars = parsePositiveInt$
|
|
1467
|
-
const httpMaxTimeMs = parsePositiveInt$
|
|
1468
|
-
let pack = EMPTY_PACK;
|
|
1466
|
+
const maxChars = parsePositiveInt$2(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS$1);
|
|
1467
|
+
const httpMaxTimeMs = parsePositiveInt$2(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S$1) * 1e3;
|
|
1468
|
+
let pack = EMPTY_PACK$1;
|
|
1469
1469
|
let origin = "none";
|
|
1470
1470
|
try {
|
|
1471
1471
|
pack = await buildPack(query, project, resolveDb(resolveDbOpt(opts)), workingSetPaths);
|
|
@@ -1473,7 +1473,7 @@ async function buildClaudeHookInjection(payload, opts, deps = {}) {
|
|
|
1473
1473
|
} catch (err) {
|
|
1474
1474
|
logHookEvent(`codemem claude-hook-inject local pack failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1475
1475
|
}
|
|
1476
|
-
if (!pack.packText && envNotDisabled(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) {
|
|
1476
|
+
if (!pack.packText && envNotDisabled$1(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) {
|
|
1477
1477
|
pack = await httpPack(query, project, httpMaxTimeMs);
|
|
1478
1478
|
if (pack.packText) origin = "http";
|
|
1479
1479
|
}
|
|
@@ -1488,7 +1488,7 @@ async function buildClaudeHookInjection(payload, opts, deps = {}) {
|
|
|
1488
1488
|
];
|
|
1489
1489
|
if (project) fields.push(`project=${JSON.stringify(project)}`);
|
|
1490
1490
|
logHookEvent(fields.join(" "));
|
|
1491
|
-
return continueResult(truncateAdditionalContext(pack.packText, maxChars));
|
|
1491
|
+
return continueResult$1(truncateAdditionalContext$1(pack.packText, maxChars));
|
|
1492
1492
|
}
|
|
1493
1493
|
var claudeHookInjectCmd = new Command("claude-hook-inject").configureHelp(helpStyle).description("Return Claude hook additionalContext from local pack generation");
|
|
1494
1494
|
addDbOption(claudeHookInjectCmd);
|
|
@@ -1497,14 +1497,14 @@ var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
|
|
|
1497
1497
|
for await (const chunk of process.stdin) raw += String(chunk);
|
|
1498
1498
|
const trimmed = raw.trim();
|
|
1499
1499
|
if (!trimmed) {
|
|
1500
|
-
emitJson(continueResult());
|
|
1500
|
+
emitJson$1(continueResult$1());
|
|
1501
1501
|
return;
|
|
1502
1502
|
}
|
|
1503
1503
|
let payload;
|
|
1504
1504
|
try {
|
|
1505
1505
|
const parsed = JSON.parse(trimmed);
|
|
1506
1506
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1507
|
-
emitError({
|
|
1507
|
+
emitError$1({
|
|
1508
1508
|
error: "parse_error",
|
|
1509
1509
|
message: "payload must be a JSON object"
|
|
1510
1510
|
});
|
|
@@ -1513,14 +1513,695 @@ var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
|
|
|
1513
1513
|
}
|
|
1514
1514
|
payload = parsed;
|
|
1515
1515
|
} catch {
|
|
1516
|
-
emitError({
|
|
1516
|
+
emitError$1({
|
|
1517
1517
|
error: "parse_error",
|
|
1518
1518
|
message: "invalid JSON"
|
|
1519
1519
|
});
|
|
1520
1520
|
process.exitCode = 1;
|
|
1521
1521
|
return;
|
|
1522
1522
|
}
|
|
1523
|
-
emitJson(await buildClaudeHookInjection(payload, opts));
|
|
1523
|
+
emitJson$1(await buildClaudeHookInjection(payload, opts));
|
|
1524
|
+
});
|
|
1525
|
+
//#endregion
|
|
1526
|
+
//#region src/commands/codex-hook-ingest-spool.ts
|
|
1527
|
+
var DEFAULT_LOCK_TTL_S = 120;
|
|
1528
|
+
var DEFAULT_LOCK_GRACE_S = 2;
|
|
1529
|
+
var LOCK_ACQUIRE_ATTEMPTS = 20;
|
|
1530
|
+
var LOCK_ACQUIRE_BACKOFF_MS = 50;
|
|
1531
|
+
var CodexHookLockBusyError = class extends Error {
|
|
1532
|
+
constructor() {
|
|
1533
|
+
super("codex-hook-ingest lock busy");
|
|
1534
|
+
this.name = "CodexHookLockBusyError";
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
function expandHome(value) {
|
|
1538
|
+
if (value === "~") return homedir();
|
|
1539
|
+
if (value.startsWith("~/")) return join(homedir(), value.slice(2));
|
|
1540
|
+
return value;
|
|
1541
|
+
}
|
|
1542
|
+
function envInt(name, fallback) {
|
|
1543
|
+
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
|
1544
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
1545
|
+
}
|
|
1546
|
+
function lockConfig() {
|
|
1547
|
+
return {
|
|
1548
|
+
lockDir: expandHome(process.env.CODEMEM_CODEX_HOOK_LOCK_DIR?.trim() || "~/.codemem/codex-hook-ingest.lock"),
|
|
1549
|
+
ttlSeconds: Math.max(1, envInt("CODEMEM_CODEX_HOOK_LOCK_TTL_S", DEFAULT_LOCK_TTL_S)),
|
|
1550
|
+
graceSeconds: Math.max(1, envInt("CODEMEM_CODEX_HOOK_LOCK_GRACE_S", DEFAULT_LOCK_GRACE_S))
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
function codexHookSpoolDir() {
|
|
1554
|
+
return expandHome(process.env.CODEMEM_CODEX_HOOK_SPOOL_DIR?.trim() || "~/.codemem/codex-hook-spool");
|
|
1555
|
+
}
|
|
1556
|
+
function codexHookLockTtlSeconds() {
|
|
1557
|
+
return lockConfig().ttlSeconds;
|
|
1558
|
+
}
|
|
1559
|
+
function hasCodexHookSpooledEntries() {
|
|
1560
|
+
let entries;
|
|
1561
|
+
try {
|
|
1562
|
+
entries = readdirSync(codexHookSpoolDir());
|
|
1563
|
+
} catch {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
return entries.some((name) => name.endsWith(".json") && !name.startsWith(".hook-tmp-") && !name.startsWith(".bad-"));
|
|
1567
|
+
}
|
|
1568
|
+
function readTrimmed(path) {
|
|
1569
|
+
try {
|
|
1570
|
+
return readFileSync(path, "utf8").trim();
|
|
1571
|
+
} catch {
|
|
1572
|
+
return "";
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function readLockMetadata(lockDir) {
|
|
1576
|
+
const rawTs = readTrimmed(join(lockDir, "ts"));
|
|
1577
|
+
const ts = rawTs === "" ? null : Number.parseInt(rawTs, 10);
|
|
1578
|
+
return {
|
|
1579
|
+
pid: readTrimmed(join(lockDir, "pid")),
|
|
1580
|
+
ts: ts === null || !Number.isFinite(ts) ? null : ts,
|
|
1581
|
+
owner: readTrimmed(join(lockDir, "owner"))
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
function isPidAlive(pidText) {
|
|
1585
|
+
const pid = Number.parseInt(pidText, 10);
|
|
1586
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
1587
|
+
try {
|
|
1588
|
+
process.kill(pid, 0);
|
|
1589
|
+
return true;
|
|
1590
|
+
} catch {
|
|
1591
|
+
return false;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function lockIsStale(cfg) {
|
|
1595
|
+
const snapshot = readLockMetadata(cfg.lockDir);
|
|
1596
|
+
const nowS = Math.floor(Date.now() / 1e3);
|
|
1597
|
+
if (snapshot.pid) {
|
|
1598
|
+
if (isPidAlive(snapshot.pid)) return {
|
|
1599
|
+
stale: snapshot.ts !== null && nowS - snapshot.ts > cfg.ttlSeconds,
|
|
1600
|
+
snapshot
|
|
1601
|
+
};
|
|
1602
|
+
return {
|
|
1603
|
+
stale: true,
|
|
1604
|
+
snapshot
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
if (snapshot.ts !== null) return {
|
|
1608
|
+
stale: nowS - snapshot.ts > cfg.graceSeconds,
|
|
1609
|
+
snapshot
|
|
1610
|
+
};
|
|
1611
|
+
try {
|
|
1612
|
+
return {
|
|
1613
|
+
stale: nowS - Math.floor(statSync(cfg.lockDir).mtimeMs / 1e3) > cfg.graceSeconds,
|
|
1614
|
+
snapshot
|
|
1615
|
+
};
|
|
1616
|
+
} catch {
|
|
1617
|
+
return {
|
|
1618
|
+
stale: true,
|
|
1619
|
+
snapshot
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
function cleanupLockDir(lockDir) {
|
|
1624
|
+
for (const name of [
|
|
1625
|
+
"pid",
|
|
1626
|
+
"ts",
|
|
1627
|
+
"owner"
|
|
1628
|
+
]) try {
|
|
1629
|
+
unlinkSync(join(lockDir, name));
|
|
1630
|
+
} catch {}
|
|
1631
|
+
try {
|
|
1632
|
+
rmdirSync(lockDir);
|
|
1633
|
+
} catch {}
|
|
1634
|
+
}
|
|
1635
|
+
function cleanupLockDirIfUnchanged(lockDir, snapshot) {
|
|
1636
|
+
const current = readLockMetadata(lockDir);
|
|
1637
|
+
if (current.pid === snapshot.pid && current.ts === snapshot.ts && current.owner === snapshot.owner) cleanupLockDir(lockDir);
|
|
1638
|
+
}
|
|
1639
|
+
function isErrnoException(err) {
|
|
1640
|
+
return typeof err === "object" && err !== null && "code" in err;
|
|
1641
|
+
}
|
|
1642
|
+
function sleep(ms) {
|
|
1643
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1644
|
+
}
|
|
1645
|
+
async function withCodexHookIngestLock(fn) {
|
|
1646
|
+
const cfg = lockConfig();
|
|
1647
|
+
mkdirSync(dirname(cfg.lockDir), { recursive: true });
|
|
1648
|
+
const ownerToken = `${process.pid}-${Math.floor(Date.now() / 1e3)}-${randomInt(1e3, 1e4)}`;
|
|
1649
|
+
let acquired = false;
|
|
1650
|
+
for (let attempt = 0; attempt < LOCK_ACQUIRE_ATTEMPTS; attempt++) {
|
|
1651
|
+
try {
|
|
1652
|
+
mkdirSync(cfg.lockDir);
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
if (isErrnoException(err) && err.code === "EEXIST") {
|
|
1655
|
+
const { stale, snapshot } = lockIsStale(cfg);
|
|
1656
|
+
if (stale) cleanupLockDirIfUnchanged(cfg.lockDir, snapshot);
|
|
1657
|
+
}
|
|
1658
|
+
await sleep(LOCK_ACQUIRE_BACKOFF_MS);
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
writeFileSync(join(cfg.lockDir, "ts"), String(Math.floor(Date.now() / 1e3)), "utf8");
|
|
1663
|
+
writeFileSync(join(cfg.lockDir, "pid"), String(process.pid), "utf8");
|
|
1664
|
+
writeFileSync(join(cfg.lockDir, "owner"), ownerToken, "utf8");
|
|
1665
|
+
acquired = true;
|
|
1666
|
+
break;
|
|
1667
|
+
} catch {
|
|
1668
|
+
cleanupLockDir(cfg.lockDir);
|
|
1669
|
+
await sleep(LOCK_ACQUIRE_BACKOFF_MS);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (!acquired) throw new CodexHookLockBusyError();
|
|
1673
|
+
try {
|
|
1674
|
+
return await fn();
|
|
1675
|
+
} finally {
|
|
1676
|
+
if (readTrimmed(join(cfg.lockDir, "owner")) === ownerToken) cleanupLockDir(cfg.lockDir);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
function spoolCodexHookPayload(payload) {
|
|
1680
|
+
const dir = codexHookSpoolDir();
|
|
1681
|
+
try {
|
|
1682
|
+
mkdirSync(dir, { recursive: true });
|
|
1683
|
+
} catch {
|
|
1684
|
+
logHookEvent("codemem codex-hook-ingest failed to create spool dir");
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
const tmpPath = join(dir, `.hook-tmp-${process.pid}-${Date.now()}-${randomInt(1e3, 1e4)}.json`);
|
|
1688
|
+
try {
|
|
1689
|
+
writeFileSync(tmpPath, JSON.stringify(payload), "utf8");
|
|
1690
|
+
} catch {
|
|
1691
|
+
logHookEvent("codemem codex-hook-ingest failed to allocate spool temp file");
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
const finalPath = join(dir, `hook-${Math.floor(Date.now() / 1e3)}-${process.pid}-${randomInt(1e3, 1e4)}.json`);
|
|
1695
|
+
try {
|
|
1696
|
+
renameSync(tmpPath, finalPath);
|
|
1697
|
+
} catch {
|
|
1698
|
+
try {
|
|
1699
|
+
unlinkSync(tmpPath);
|
|
1700
|
+
} catch {}
|
|
1701
|
+
logHookEvent("codemem codex-hook-ingest failed to spool payload");
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
logHookEvent(`codemem codex-hook-ingest spooled payload: ${finalPath}`);
|
|
1705
|
+
return true;
|
|
1706
|
+
}
|
|
1707
|
+
function recoverStaleCodexHookTmpSpool(ttlSeconds) {
|
|
1708
|
+
const dir = codexHookSpoolDir();
|
|
1709
|
+
try {
|
|
1710
|
+
mkdirSync(dir, { recursive: true });
|
|
1711
|
+
} catch {
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
let entries;
|
|
1715
|
+
try {
|
|
1716
|
+
entries = readdirSync(dir);
|
|
1717
|
+
} catch {
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
const nowS = Date.now() / 1e3;
|
|
1721
|
+
for (const name of entries) {
|
|
1722
|
+
if (!name.startsWith(".hook-tmp-") || !name.endsWith(".json")) continue;
|
|
1723
|
+
const tmpPath = join(dir, name);
|
|
1724
|
+
try {
|
|
1725
|
+
if (nowS - statSync(tmpPath).mtimeMs / 1e3 <= ttlSeconds) continue;
|
|
1726
|
+
renameSync(tmpPath, join(dir, `hook-recovered-${Math.floor(nowS)}-${process.pid}-${randomInt(1e3, 1e4)}.json`));
|
|
1727
|
+
} catch {}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function quarantineSpoolEntry(dir, name, reason) {
|
|
1731
|
+
try {
|
|
1732
|
+
renameSync(join(dir, name), join(dir, `.bad-${reason}-${Date.now()}-${randomInt(1e3, 1e4)}-${name}`));
|
|
1733
|
+
} catch {
|
|
1734
|
+
try {
|
|
1735
|
+
unlinkSync(join(dir, name));
|
|
1736
|
+
} catch {}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
async function drainCodexHookSpool(handler) {
|
|
1740
|
+
const dir = codexHookSpoolDir();
|
|
1741
|
+
try {
|
|
1742
|
+
mkdirSync(dir, { recursive: true });
|
|
1743
|
+
} catch {
|
|
1744
|
+
return {
|
|
1745
|
+
processed: 0,
|
|
1746
|
+
failed: 0
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
let entries;
|
|
1750
|
+
try {
|
|
1751
|
+
entries = readdirSync(dir);
|
|
1752
|
+
} catch {
|
|
1753
|
+
return {
|
|
1754
|
+
processed: 0,
|
|
1755
|
+
failed: 0
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
const result = {
|
|
1759
|
+
processed: 0,
|
|
1760
|
+
failed: 0
|
|
1761
|
+
};
|
|
1762
|
+
for (const name of entries.filter((entry) => entry.endsWith(".json") && !entry.startsWith(".hook-tmp-") && !entry.startsWith(".bad-")).sort()) {
|
|
1763
|
+
const path = join(dir, name);
|
|
1764
|
+
let parsed;
|
|
1765
|
+
try {
|
|
1766
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1767
|
+
} catch {
|
|
1768
|
+
quarantineSpoolEntry(dir, name, "parse-error");
|
|
1769
|
+
result.failed++;
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1773
|
+
quarantineSpoolEntry(dir, name, "wrong-shape");
|
|
1774
|
+
result.failed++;
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
let ok = false;
|
|
1778
|
+
try {
|
|
1779
|
+
ok = await handler(parsed);
|
|
1780
|
+
} catch {
|
|
1781
|
+
ok = false;
|
|
1782
|
+
}
|
|
1783
|
+
if (!ok) {
|
|
1784
|
+
result.failed++;
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
try {
|
|
1788
|
+
unlinkSync(path);
|
|
1789
|
+
result.processed++;
|
|
1790
|
+
} catch {}
|
|
1791
|
+
}
|
|
1792
|
+
return result;
|
|
1793
|
+
}
|
|
1794
|
+
//#endregion
|
|
1795
|
+
//#region src/commands/codex-hook-ingest.ts
|
|
1796
|
+
/**
|
|
1797
|
+
* codemem codex-hook-ingest — read a single Codex hook payload from stdin and
|
|
1798
|
+
* enqueue it for raw-event processing.
|
|
1799
|
+
*/
|
|
1800
|
+
var DEFAULT_HTTP_TIMEOUT_MS = 1e3;
|
|
1801
|
+
function httpTimeoutMs() {
|
|
1802
|
+
const parsed = Number.parseInt(process.env.CODEMEM_CODEX_HOOK_HTTP_TIMEOUT_MS ?? "", 10);
|
|
1803
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_HTTP_TIMEOUT_MS;
|
|
1804
|
+
}
|
|
1805
|
+
function emitStructuredError$1(errorCode, message) {
|
|
1806
|
+
console.log(JSON.stringify({
|
|
1807
|
+
error: errorCode,
|
|
1808
|
+
message
|
|
1809
|
+
}));
|
|
1810
|
+
process.exitCode = 1;
|
|
1811
|
+
}
|
|
1812
|
+
function envTruthyValue(value) {
|
|
1813
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
1814
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
1815
|
+
}
|
|
1816
|
+
function hasPayloadTimestamp(payload) {
|
|
1817
|
+
return typeof payload.timestamp === "string" && payload.timestamp.trim() !== "" || typeof payload.ts === "string" && payload.ts.trim() !== "";
|
|
1818
|
+
}
|
|
1819
|
+
function normalizePayloadForIngest(payload) {
|
|
1820
|
+
if (hasPayloadTimestamp(payload)) return payload;
|
|
1821
|
+
return {
|
|
1822
|
+
...payload,
|
|
1823
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1824
|
+
codemem_generated_event_nonce: randomUUID()
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
async function tryHttpIngest(payload, host, port) {
|
|
1828
|
+
const url = `http://${host}:${port}/api/codex-hooks`;
|
|
1829
|
+
const controller = new AbortController();
|
|
1830
|
+
const timeout = setTimeout(() => controller.abort(), httpTimeoutMs());
|
|
1831
|
+
try {
|
|
1832
|
+
const res = await fetch(url, {
|
|
1833
|
+
method: "POST",
|
|
1834
|
+
headers: { "Content-Type": "application/json" },
|
|
1835
|
+
body: JSON.stringify(payload),
|
|
1836
|
+
signal: controller.signal
|
|
1837
|
+
});
|
|
1838
|
+
if (!res.ok) return {
|
|
1839
|
+
ok: false,
|
|
1840
|
+
inserted: 0,
|
|
1841
|
+
skipped: 0
|
|
1842
|
+
};
|
|
1843
|
+
const body = await res.json();
|
|
1844
|
+
if (body == null || typeof body !== "object" || Array.isArray(body)) {
|
|
1845
|
+
logHookEvent("codemem codex-hook-ingest HTTP accepted with invalid response type");
|
|
1846
|
+
return {
|
|
1847
|
+
ok: false,
|
|
1848
|
+
inserted: 0,
|
|
1849
|
+
skipped: 0
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
const obj = body;
|
|
1853
|
+
if (typeof obj.inserted !== "number" || typeof obj.skipped !== "number") {
|
|
1854
|
+
logHookEvent("codemem codex-hook-ingest HTTP accepted with unexpected response body");
|
|
1855
|
+
return {
|
|
1856
|
+
ok: false,
|
|
1857
|
+
inserted: 0,
|
|
1858
|
+
skipped: 0
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
return {
|
|
1862
|
+
ok: true,
|
|
1863
|
+
inserted: obj.inserted,
|
|
1864
|
+
skipped: obj.skipped
|
|
1865
|
+
};
|
|
1866
|
+
} catch {
|
|
1867
|
+
return {
|
|
1868
|
+
ok: false,
|
|
1869
|
+
inserted: 0,
|
|
1870
|
+
skipped: 0
|
|
1871
|
+
};
|
|
1872
|
+
} finally {
|
|
1873
|
+
clearTimeout(timeout);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
function directEnqueueCodexHook(payload, dbPath) {
|
|
1877
|
+
const envelope = buildRawEventEnvelopeFromCodexHook(payload);
|
|
1878
|
+
if (!envelope) return {
|
|
1879
|
+
inserted: 0,
|
|
1880
|
+
skipped: 1
|
|
1881
|
+
};
|
|
1882
|
+
const db = connect(dbPath);
|
|
1883
|
+
try {
|
|
1884
|
+
try {
|
|
1885
|
+
loadSqliteVec(db);
|
|
1886
|
+
} catch {}
|
|
1887
|
+
ensureSchemaBootstrapped(db);
|
|
1888
|
+
const strippedPayload = stripPrivateObj(envelope.payload);
|
|
1889
|
+
if (db.prepare("SELECT 1 FROM raw_events WHERE source = ? AND stream_id = ? AND event_id = ? LIMIT 1").get(envelope.source, envelope.session_stream_id, envelope.event_id)) return {
|
|
1890
|
+
inserted: 0,
|
|
1891
|
+
skipped: 0
|
|
1892
|
+
};
|
|
1893
|
+
db.prepare(`INSERT INTO raw_events(
|
|
1894
|
+
source, stream_id, opencode_session_id, event_id, event_seq,
|
|
1895
|
+
event_type, ts_wall_ms, payload_json, created_at
|
|
1896
|
+
) VALUES (?, ?, ?, ?, (
|
|
1897
|
+
SELECT COALESCE(MAX(event_seq), -1) + 1
|
|
1898
|
+
FROM raw_events WHERE source = ? AND stream_id = ?
|
|
1899
|
+
), ?, ?, ?, datetime('now'))`).run(envelope.source, envelope.session_stream_id, envelope.opencode_session_id, envelope.event_id, envelope.source, envelope.session_stream_id, envelope.event_type, envelope.ts_wall_ms, JSON.stringify(strippedPayload));
|
|
1900
|
+
const maxSeqRow = db.prepare("SELECT COALESCE(MAX(event_seq), 0) AS max_seq FROM raw_events WHERE source = ? AND stream_id = ?").get(envelope.source, envelope.session_stream_id);
|
|
1901
|
+
db.prepare(`INSERT INTO raw_event_sessions(
|
|
1902
|
+
source, stream_id, opencode_session_id, cwd, project, started_at,
|
|
1903
|
+
last_seen_ts_wall_ms, last_received_event_seq, last_flushed_event_seq, updated_at
|
|
1904
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, -1, datetime('now'))
|
|
1905
|
+
ON CONFLICT(source, stream_id) DO UPDATE SET
|
|
1906
|
+
cwd = COALESCE(excluded.cwd, cwd),
|
|
1907
|
+
project = COALESCE(excluded.project, project),
|
|
1908
|
+
started_at = COALESCE(excluded.started_at, started_at),
|
|
1909
|
+
last_seen_ts_wall_ms = MAX(COALESCE(excluded.last_seen_ts_wall_ms, 0), COALESCE(last_seen_ts_wall_ms, 0)),
|
|
1910
|
+
last_received_event_seq = MAX(excluded.last_received_event_seq, last_received_event_seq),
|
|
1911
|
+
updated_at = datetime('now')`).run(envelope.source, envelope.session_stream_id, envelope.opencode_session_id, envelope.cwd, envelope.project, envelope.started_at, envelope.ts_wall_ms, maxSeqRow.max_seq);
|
|
1912
|
+
return {
|
|
1913
|
+
inserted: 1,
|
|
1914
|
+
skipped: 0
|
|
1915
|
+
};
|
|
1916
|
+
} finally {
|
|
1917
|
+
db.close();
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
async function ingestCodexHookPayload(payload, opts, deps = {}) {
|
|
1921
|
+
const httpIngest = deps.httpIngest ?? tryHttpIngest;
|
|
1922
|
+
const directIngest = deps.directIngest ?? directEnqueueCodexHook;
|
|
1923
|
+
const resolveDb = deps.resolveDb ?? resolveDbPath;
|
|
1924
|
+
const port = typeof opts.port === "number" ? opts.port : Number.parseInt(opts.port, 10);
|
|
1925
|
+
const ingestPayload = normalizePayloadForIngest(payload);
|
|
1926
|
+
let cachedDbPath = null;
|
|
1927
|
+
const getDbPath = () => {
|
|
1928
|
+
if (cachedDbPath === null) cachedDbPath = resolveDb(resolveDbOpt(opts));
|
|
1929
|
+
return cachedDbPath;
|
|
1930
|
+
};
|
|
1931
|
+
const tryDirectFallback = (queuedPayload) => {
|
|
1932
|
+
try {
|
|
1933
|
+
directIngest(queuedPayload, getDbPath());
|
|
1934
|
+
return true;
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
logHookEvent(`codemem codex-hook-ingest direct fallback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
const drainBacklogIfPresent = async () => {
|
|
1941
|
+
if (!hasCodexHookSpooledEntries()) return;
|
|
1942
|
+
try {
|
|
1943
|
+
await withCodexHookIngestLock(async () => {
|
|
1944
|
+
recoverStaleCodexHookTmpSpool(codexHookLockTtlSeconds());
|
|
1945
|
+
await drainCodexHookSpool(async (queuedPayload) => {
|
|
1946
|
+
return (await httpIngest(queuedPayload, opts.host, port)).ok || tryDirectFallback(queuedPayload);
|
|
1947
|
+
});
|
|
1948
|
+
});
|
|
1949
|
+
} catch (err) {
|
|
1950
|
+
if (err instanceof CodexHookLockBusyError) return;
|
|
1951
|
+
logHookEvent(`codemem codex-hook-ingest backlog drain failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
const httpResult = await httpIngest(ingestPayload, opts.host, port);
|
|
1955
|
+
if (httpResult.ok) {
|
|
1956
|
+
await drainBacklogIfPresent();
|
|
1957
|
+
return {
|
|
1958
|
+
inserted: httpResult.inserted,
|
|
1959
|
+
skipped: httpResult.skipped,
|
|
1960
|
+
via: "http"
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
try {
|
|
1964
|
+
return await withCodexHookIngestLock(async () => {
|
|
1965
|
+
recoverStaleCodexHookTmpSpool(codexHookLockTtlSeconds());
|
|
1966
|
+
let currentResult;
|
|
1967
|
+
try {
|
|
1968
|
+
currentResult = {
|
|
1969
|
+
...directIngest(ingestPayload, getDbPath()),
|
|
1970
|
+
via: "direct"
|
|
1971
|
+
};
|
|
1972
|
+
} catch (err) {
|
|
1973
|
+
logHookEvent(`codemem codex-hook-ingest direct fallback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1974
|
+
if (!spoolCodexHookPayload(ingestPayload)) throw new Error("codex-hook-ingest: fallback and spool both failed");
|
|
1975
|
+
currentResult = {
|
|
1976
|
+
inserted: 0,
|
|
1977
|
+
skipped: 0,
|
|
1978
|
+
via: "spool"
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
await drainCodexHookSpool((queuedPayload) => tryDirectFallback(queuedPayload));
|
|
1982
|
+
return currentResult;
|
|
1983
|
+
});
|
|
1984
|
+
} catch (err) {
|
|
1985
|
+
if (!(err instanceof CodexHookLockBusyError)) throw err;
|
|
1986
|
+
logHookEvent("codemem codex-hook-ingest lock busy; trying unlocked fallback");
|
|
1987
|
+
try {
|
|
1988
|
+
return {
|
|
1989
|
+
...directIngest(ingestPayload, getDbPath()),
|
|
1990
|
+
via: "direct"
|
|
1991
|
+
};
|
|
1992
|
+
} catch (directErr) {
|
|
1993
|
+
logHookEvent(`codemem codex-hook-ingest unlocked direct fallback failed: ${directErr instanceof Error ? directErr.message : String(directErr)}`);
|
|
1994
|
+
}
|
|
1995
|
+
if (spoolCodexHookPayload(ingestPayload)) return {
|
|
1996
|
+
inserted: 0,
|
|
1997
|
+
skipped: 0,
|
|
1998
|
+
via: "spool_lock_busy"
|
|
1999
|
+
};
|
|
2000
|
+
throw err;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
var codexHookCmd = new Command("codex-hook-ingest").configureHelp(helpStyle).description("Ingest Codex hook payload: HTTP first, direct DB fallback");
|
|
2004
|
+
addDbOption(codexHookCmd);
|
|
2005
|
+
addViewerHostOptions(codexHookCmd);
|
|
2006
|
+
var codexHookIngestCommand = codexHookCmd.action(async (opts) => {
|
|
2007
|
+
if (envTruthyValue(process.env.CODEMEM_PLUGIN_IGNORE)) return;
|
|
2008
|
+
let raw;
|
|
2009
|
+
try {
|
|
2010
|
+
raw = readFileSync(0, "utf8").trim();
|
|
2011
|
+
} catch {
|
|
2012
|
+
emitStructuredError$1("read_error", "failed to read stdin");
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
if (!raw) {
|
|
2016
|
+
emitStructuredError$1("read_error", "empty stdin");
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
let payload;
|
|
2020
|
+
try {
|
|
2021
|
+
const parsed = JSON.parse(raw);
|
|
2022
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2023
|
+
emitStructuredError$1("parse_error", "payload must be a JSON object");
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
payload = parsed;
|
|
2027
|
+
} catch {
|
|
2028
|
+
emitStructuredError$1("parse_error", "invalid JSON");
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
try {
|
|
2032
|
+
const result = await ingestCodexHookPayload(payload, opts);
|
|
2033
|
+
console.log(JSON.stringify(result));
|
|
2034
|
+
} catch (err) {
|
|
2035
|
+
emitStructuredError$1("ingest_error", err instanceof Error ? err.message : String(err));
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
//#endregion
|
|
2039
|
+
//#region src/commands/codex-hook-inject.ts
|
|
2040
|
+
var HOOK_EVENT_NAME = "UserPromptSubmit";
|
|
2041
|
+
var EMPTY_PACK = {
|
|
2042
|
+
packText: "",
|
|
2043
|
+
items: 0,
|
|
2044
|
+
packTokens: 0
|
|
2045
|
+
};
|
|
2046
|
+
var DEFAULT_VIEWER_HOST = "127.0.0.1";
|
|
2047
|
+
var DEFAULT_VIEWER_PORT = 38888;
|
|
2048
|
+
var DEFAULT_MAX_CHARS = 16e3;
|
|
2049
|
+
var DEFAULT_HTTP_MAX_TIME_S = 2;
|
|
2050
|
+
function emitJson(value) {
|
|
2051
|
+
console.log(JSON.stringify(value));
|
|
2052
|
+
}
|
|
2053
|
+
function emitError(value) {
|
|
2054
|
+
process.stderr.write(`${JSON.stringify(value)}\n`);
|
|
2055
|
+
}
|
|
2056
|
+
function envNotDisabled(value) {
|
|
2057
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
2058
|
+
return normalized !== "0" && normalized !== "false" && normalized !== "off";
|
|
2059
|
+
}
|
|
2060
|
+
function envTruthy(value) {
|
|
2061
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
2062
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
2063
|
+
}
|
|
2064
|
+
function parsePositiveInt$1(value, fallback) {
|
|
2065
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
2066
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
2067
|
+
}
|
|
2068
|
+
function continueResult(additionalContext) {
|
|
2069
|
+
if (!additionalContext) return { continue: true };
|
|
2070
|
+
return {
|
|
2071
|
+
continue: true,
|
|
2072
|
+
hookSpecificOutput: {
|
|
2073
|
+
hookEventName: HOOK_EVENT_NAME,
|
|
2074
|
+
additionalContext
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
function truncateAdditionalContext(text, maxChars) {
|
|
2079
|
+
const normalized = text.trim();
|
|
2080
|
+
if (!normalized) return "";
|
|
2081
|
+
if (!Number.isFinite(maxChars) || maxChars <= 0 || normalized.length <= maxChars) return normalized;
|
|
2082
|
+
return `${normalized.slice(0, maxChars).trimEnd()}\n\n[pack truncated]`;
|
|
2083
|
+
}
|
|
2084
|
+
function resolveInjectProject(payload) {
|
|
2085
|
+
return resolveHookProject(typeof payload.cwd === "string" ? payload.cwd : null, payload.project);
|
|
2086
|
+
}
|
|
2087
|
+
function buildCodexInjectQuery(prompt, project) {
|
|
2088
|
+
return [prompt, project ?? ""].filter((part) => part.trim().length > 0).join(" ").slice(0, 500) || "recent work";
|
|
2089
|
+
}
|
|
2090
|
+
async function buildLocalPack(context, project, dbPath) {
|
|
2091
|
+
const store = new MemoryStore(dbPath);
|
|
2092
|
+
try {
|
|
2093
|
+
const limit = parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8);
|
|
2094
|
+
const budget = parsePositiveInt$1(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
|
|
2095
|
+
const filters = {};
|
|
2096
|
+
if (project) filters.project = project;
|
|
2097
|
+
const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
|
|
2098
|
+
return {
|
|
2099
|
+
packText: String(pack.pack_text ?? "").trim(),
|
|
2100
|
+
items: Array.isArray(pack.items) ? pack.items.length : 0,
|
|
2101
|
+
packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics?.pack_tokens) : 0
|
|
2102
|
+
};
|
|
2103
|
+
} finally {
|
|
2104
|
+
store.close();
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S * 1e3) {
|
|
2108
|
+
const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
|
|
2109
|
+
const port = parsePositiveInt$1(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
|
|
2110
|
+
const url = new URL(`http://${host}:${port}/api/pack`);
|
|
2111
|
+
url.searchParams.set("context", context);
|
|
2112
|
+
url.searchParams.set("limit", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8)));
|
|
2113
|
+
url.searchParams.set("token_budget", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800)));
|
|
2114
|
+
if (project) url.searchParams.set("project", project);
|
|
2115
|
+
const controller = new AbortController();
|
|
2116
|
+
const timeout = setTimeout(() => controller.abort(), maxTimeMs);
|
|
2117
|
+
try {
|
|
2118
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
2119
|
+
if (!res.ok) return EMPTY_PACK;
|
|
2120
|
+
const body = await res.json();
|
|
2121
|
+
return {
|
|
2122
|
+
packText: String(body.pack_text ?? "").trim(),
|
|
2123
|
+
items: Array.isArray(body.items) ? body.items.length : 0,
|
|
2124
|
+
packTokens: Number.isFinite(Number(body.metrics?.pack_tokens)) ? Number(body.metrics?.pack_tokens) : 0
|
|
2125
|
+
};
|
|
2126
|
+
} catch {
|
|
2127
|
+
return EMPTY_PACK;
|
|
2128
|
+
} finally {
|
|
2129
|
+
clearTimeout(timeout);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
async function buildCodexHookInjection(payload, opts, deps = {}) {
|
|
2133
|
+
if (envTruthy(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult();
|
|
2134
|
+
if (!envNotDisabled(process.env.CODEMEM_INJECT_CONTEXT || "1")) return continueResult();
|
|
2135
|
+
const promptText = normalizePromptText(payload.prompt);
|
|
2136
|
+
if (!promptText) return continueResult();
|
|
2137
|
+
const buildPack = deps.buildLocalPack ?? buildLocalPack;
|
|
2138
|
+
const httpPack = deps.httpPack ?? tryHttpPack;
|
|
2139
|
+
const resolveDb = deps.resolveDb ?? resolveDbPath;
|
|
2140
|
+
const project = resolveInjectProject(payload);
|
|
2141
|
+
const query = buildCodexInjectQuery(promptText, project);
|
|
2142
|
+
const maxChars = parsePositiveInt$1(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS);
|
|
2143
|
+
const httpMaxTimeMs = parsePositiveInt$1(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S) * 1e3;
|
|
2144
|
+
let pack = EMPTY_PACK;
|
|
2145
|
+
let origin = "none";
|
|
2146
|
+
try {
|
|
2147
|
+
pack = await buildPack(query, project, resolveDb(resolveDbOpt(opts)));
|
|
2148
|
+
if (pack.packText) origin = "local";
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
logHookEvent(`codemem codex-hook-inject local pack failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2151
|
+
}
|
|
2152
|
+
if (!pack.packText && envNotDisabled(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) {
|
|
2153
|
+
pack = await httpPack(query, project, httpMaxTimeMs);
|
|
2154
|
+
if (pack.packText) origin = "http";
|
|
2155
|
+
}
|
|
2156
|
+
const fields = [
|
|
2157
|
+
"inject.pack.ok",
|
|
2158
|
+
"source=codex",
|
|
2159
|
+
`origin=${origin}`,
|
|
2160
|
+
`items=${pack.items}`,
|
|
2161
|
+
`pack_tokens=${pack.packTokens}`,
|
|
2162
|
+
`query_len=${query.length}`,
|
|
2163
|
+
`empty=${pack.packText ? "false" : "true"}`
|
|
2164
|
+
];
|
|
2165
|
+
if (project) fields.push(`project=${JSON.stringify(project)}`);
|
|
2166
|
+
logHookEvent(fields.join(" "));
|
|
2167
|
+
return continueResult(truncateAdditionalContext(pack.packText, maxChars));
|
|
2168
|
+
}
|
|
2169
|
+
var codexHookInjectCmd = new Command("codex-hook-inject").configureHelp(helpStyle).description("Return Codex hook additionalContext from local pack generation");
|
|
2170
|
+
addDbOption(codexHookInjectCmd);
|
|
2171
|
+
var codexHookInjectCommand = codexHookInjectCmd.action(async (opts) => {
|
|
2172
|
+
let raw = "";
|
|
2173
|
+
for await (const chunk of process.stdin) raw += String(chunk);
|
|
2174
|
+
const trimmed = raw.trim();
|
|
2175
|
+
if (!trimmed) {
|
|
2176
|
+
emitJson(continueResult());
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
let payload;
|
|
2180
|
+
try {
|
|
2181
|
+
const parsed = JSON.parse(trimmed);
|
|
2182
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2183
|
+
emitError({
|
|
2184
|
+
error: "parse_error",
|
|
2185
|
+
message: "payload must be a JSON object"
|
|
2186
|
+
});
|
|
2187
|
+
process.exitCode = 1;
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
payload = parsed;
|
|
2191
|
+
} catch {
|
|
2192
|
+
emitError({
|
|
2193
|
+
error: "parse_error",
|
|
2194
|
+
message: "invalid JSON"
|
|
2195
|
+
});
|
|
2196
|
+
process.exitCode = 1;
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
try {
|
|
2200
|
+
emitJson(await buildCodexHookInjection(payload, opts));
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
logHookEvent(`codemem codex-hook-inject failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2203
|
+
emitJson(continueResult());
|
|
2204
|
+
}
|
|
1524
2205
|
});
|
|
1525
2206
|
//#endregion
|
|
1526
2207
|
//#region src/commands/config.ts
|
|
@@ -6325,6 +7006,8 @@ completion.on("command", ({ reply }) => {
|
|
|
6325
7006
|
"claude-hook-file-context",
|
|
6326
7007
|
"claude-hook-inject",
|
|
6327
7008
|
"claude-hook-ingest",
|
|
7009
|
+
"codex-hook-inject",
|
|
7010
|
+
"codex-hook-ingest",
|
|
6328
7011
|
"config",
|
|
6329
7012
|
"coordinator",
|
|
6330
7013
|
"db",
|
|
@@ -6403,6 +7086,8 @@ program.addCommand(mcpCommand);
|
|
|
6403
7086
|
program.addCommand(claudeHookInjectCommand);
|
|
6404
7087
|
program.addCommand(claudeHookIngestCommand);
|
|
6405
7088
|
program.addCommand(claudeHookFileContextCommand);
|
|
7089
|
+
program.addCommand(codexHookInjectCommand);
|
|
7090
|
+
program.addCommand(codexHookIngestCommand);
|
|
6406
7091
|
program.addCommand(dbCommand);
|
|
6407
7092
|
program.addCommand(exportMemoriesCommand);
|
|
6408
7093
|
program.addCommand(importMemoriesCommand);
|