codemem 0.43.1 → 0.44.0-alpha.1
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/README.md +18 -1
- package/dist/index.js +703 -703
- package/dist/index.js.map +1 -1
- package/package.json +14 -13
package/dist/index.js
CHANGED
|
@@ -91,7 +91,7 @@ function emitJsonError(errorCode, message, exitCode = 1) {
|
|
|
91
91
|
* and empty) are treated as toggles, not paths, so the default is used.
|
|
92
92
|
* - All I/O is best-effort: failures are swallowed.
|
|
93
93
|
*/
|
|
94
|
-
var BOOLEAN_TOGGLE_VALUES = new Set([
|
|
94
|
+
var BOOLEAN_TOGGLE_VALUES = /* @__PURE__ */ new Set([
|
|
95
95
|
"",
|
|
96
96
|
"0",
|
|
97
97
|
"false",
|
|
@@ -102,7 +102,7 @@ var BOOLEAN_TOGGLE_VALUES = new Set([
|
|
|
102
102
|
"on",
|
|
103
103
|
"no"
|
|
104
104
|
]);
|
|
105
|
-
function expandHome$
|
|
105
|
+
function expandHome$3(value) {
|
|
106
106
|
const home = process.env.HOME?.trim() || homedir();
|
|
107
107
|
if (value === "~") return home;
|
|
108
108
|
if (value.startsWith("~/")) return join(home, value.slice(2));
|
|
@@ -111,8 +111,8 @@ function expandHome$4(value) {
|
|
|
111
111
|
function pluginLogPath() {
|
|
112
112
|
const raw = process.env.CODEMEM_PLUGIN_LOG_PATH ?? process.env.CODEMEM_PLUGIN_LOG ?? "";
|
|
113
113
|
const normalized = raw.trim().toLowerCase();
|
|
114
|
-
if (BOOLEAN_TOGGLE_VALUES.has(normalized)) return expandHome$
|
|
115
|
-
return expandHome$
|
|
114
|
+
if (BOOLEAN_TOGGLE_VALUES.has(normalized)) return expandHome$3("~/.codemem/plugin.log");
|
|
115
|
+
return expandHome$3(raw.trim());
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
118
|
* Append a single timestamped line to the plugin log. Best-effort: any
|
|
@@ -131,7 +131,7 @@ function logHookEvent(message) {
|
|
|
131
131
|
var FILE_GATE_MIN_BYTES = 1500;
|
|
132
132
|
var FETCH_LIMIT = 40;
|
|
133
133
|
var DISPLAY_LIMIT = 15;
|
|
134
|
-
var MTIME_FRESH_TOLERANCE_MS =
|
|
134
|
+
var MTIME_FRESH_TOLERANCE_MS = 3e5;
|
|
135
135
|
var SMALL_FILE_BYPASS_PATTERNS = [
|
|
136
136
|
/\.(json|jsonc|toml|ya?ml)$/i,
|
|
137
137
|
/\.env(\.|$)/i,
|
|
@@ -164,7 +164,7 @@ function envTruthy$3(value) {
|
|
|
164
164
|
const normalized = String(value ?? "").trim().toLowerCase();
|
|
165
165
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
166
166
|
}
|
|
167
|
-
function expandHome$
|
|
167
|
+
function expandHome$2(value) {
|
|
168
168
|
if (value === "~") return homedir();
|
|
169
169
|
if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
|
|
170
170
|
return value;
|
|
@@ -286,7 +286,8 @@ function queryByFile(dbPath, relativePath, project, limit) {
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
function resolveProject$1(payload) {
|
|
289
|
-
|
|
289
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
|
|
290
|
+
return resolveHookProject(cwd, payload.project);
|
|
290
291
|
}
|
|
291
292
|
function sourceSessionId(payload) {
|
|
292
293
|
const value = payload.session_id;
|
|
@@ -360,7 +361,7 @@ async function buildClaudeFileContext(payload, opts, deps = {}) {
|
|
|
360
361
|
return finish(continueResult$2());
|
|
361
362
|
}
|
|
362
363
|
const cwd = typeof payload.cwd === "string" && payload.cwd.trim() ? payload.cwd : process.cwd();
|
|
363
|
-
const expandedPath = expandHome$
|
|
364
|
+
const expandedPath = expandHome$2(filePath);
|
|
364
365
|
const absolutePath = isAbsolute(expandedPath) ? expandedPath : resolve(cwd, expandedPath);
|
|
365
366
|
const relativePath = relative(cwd, absolutePath).split(sep).join("/");
|
|
366
367
|
const escapesCwd = relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath);
|
|
@@ -519,34 +520,22 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
|
|
|
519
520
|
emitJson$2(await buildClaudeFileContext(payload, opts));
|
|
520
521
|
});
|
|
521
522
|
//#endregion
|
|
522
|
-
//#region src/commands/
|
|
523
|
+
//#region src/commands/hook-ingest-spool.ts
|
|
523
524
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* payloads when both HTTP and direct ingestion
|
|
527
|
-
* recovery routine that promotes stale temp files back into the
|
|
525
|
+
* Shared durability layer for the hook ingest commands (claude, codex):
|
|
526
|
+
* a file-based mutex that serializes concurrent invocations, an on-disk
|
|
527
|
+
* spool that captures payloads when both HTTP and direct ingestion fail,
|
|
528
|
+
* and a recovery routine that promotes stale temp files back into the
|
|
529
|
+
* queue. Each client instantiates its own copy via `createHookIngestSpool`
|
|
530
|
+
* with its own lock/spool dirs, TTL, retry budget, log prefix, and
|
|
531
|
+
* client-specific `LockBusyError` name/message. Per-client flush
|
|
532
|
+
* predicates stay in the client files.
|
|
533
|
+
*/
|
|
534
|
+
var LOCK_ACQUIRE_BACKOFF_MS = 50;
|
|
535
|
+
/**
|
|
536
|
+
* Boolean-shaped env toggle used by per-client flush predicates
|
|
537
|
+
* (claude's `shouldForceBoundaryFlush`).
|
|
528
538
|
*/
|
|
529
|
-
var DEFAULT_LOCK_TTL_S$1 = 300;
|
|
530
|
-
var DEFAULT_LOCK_GRACE_S$1 = 2;
|
|
531
|
-
var LOCK_ACQUIRE_ATTEMPTS$1 = 100;
|
|
532
|
-
var LOCK_ACQUIRE_BACKOFF_MS$1 = 50;
|
|
533
|
-
var LockBusyError = class extends Error {
|
|
534
|
-
constructor() {
|
|
535
|
-
super("claude-hook-ingest lock busy");
|
|
536
|
-
this.name = "LockBusyError";
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
function expandHome$2(value) {
|
|
540
|
-
if (value === "~") return homedir();
|
|
541
|
-
if (value.startsWith("~/")) return join(homedir(), value.slice(2));
|
|
542
|
-
return value;
|
|
543
|
-
}
|
|
544
|
-
function envInt$1(name, fallback) {
|
|
545
|
-
const raw = process.env[name];
|
|
546
|
-
if (raw === void 0) return fallback;
|
|
547
|
-
const parsed = Number.parseInt(raw, 10);
|
|
548
|
-
return Number.isFinite(parsed) ? parsed : fallback;
|
|
549
|
-
}
|
|
550
539
|
function envTruthy$2(name, fallback) {
|
|
551
540
|
const raw = process.env[name];
|
|
552
541
|
if (raw === void 0) return fallback;
|
|
@@ -565,336 +554,404 @@ function envTruthy$2(name, fallback) {
|
|
|
565
554
|
].includes(normalized)) return false;
|
|
566
555
|
return fallback;
|
|
567
556
|
}
|
|
568
|
-
function
|
|
569
|
-
return
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
graceSeconds: Math.max(1, envInt$1("CODEMEM_CLAUDE_HOOK_LOCK_GRACE_S", DEFAULT_LOCK_GRACE_S$1))
|
|
573
|
-
};
|
|
557
|
+
function expandHome$1(value) {
|
|
558
|
+
if (value === "~") return homedir();
|
|
559
|
+
if (value.startsWith("~/")) return join(homedir(), value.slice(2));
|
|
560
|
+
return value;
|
|
574
561
|
}
|
|
575
|
-
function
|
|
576
|
-
|
|
562
|
+
function envInt(name, fallback) {
|
|
563
|
+
const raw = process.env[name];
|
|
564
|
+
if (raw === void 0) return fallback;
|
|
565
|
+
const parsed = Number.parseInt(raw, 10);
|
|
566
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
577
567
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
568
|
+
function createHookIngestSpool(cfg) {
|
|
569
|
+
class LockBusyError extends Error {
|
|
570
|
+
constructor() {
|
|
571
|
+
super(cfg.lockBusyErrorMessage);
|
|
572
|
+
this.name = cfg.lockBusyErrorName;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function lockConfig() {
|
|
576
|
+
return {
|
|
577
|
+
lockDir: expandHome$1(process.env[cfg.lockDirEnv]?.trim() || cfg.lockDirDefault),
|
|
578
|
+
ttlSeconds: Math.max(1, envInt(cfg.lockTtlEnv, cfg.lockTtlDefault)),
|
|
579
|
+
graceSeconds: Math.max(1, envInt(cfg.lockGraceEnv, cfg.lockGraceDefault))
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function spoolDir() {
|
|
583
|
+
return expandHome$1(process.env[cfg.spoolDirEnv]?.trim() || cfg.spoolDirDefault);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Returns the configured lock TTL so callers (hook-ingest commands)
|
|
587
|
+
* can pass the same value to `recoverStaleTmpSpool` without re-reading
|
|
588
|
+
* the env.
|
|
589
|
+
*/
|
|
590
|
+
function lockTtlSeconds() {
|
|
591
|
+
return lockConfig().ttlSeconds;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Cheap pre-check used by the unlocked HTTP-success path to decide
|
|
595
|
+
* whether it needs to acquire the ingest lock and drain queued
|
|
596
|
+
* payloads. Returns true when the spool directory contains at least
|
|
597
|
+
* one active entry (a `*.json` file that is neither an in-flight
|
|
598
|
+
* `.hook-tmp-*` nor a quarantined `.bad-*` file). Any I/O failure
|
|
599
|
+
* is treated as "no entries" so callers stay on the fast path.
|
|
600
|
+
*/
|
|
601
|
+
function hasSpooledEntries() {
|
|
602
|
+
let entries;
|
|
603
|
+
try {
|
|
604
|
+
entries = readdirSync(spoolDir());
|
|
605
|
+
} catch {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
for (const name of entries) {
|
|
609
|
+
if (!name.endsWith(".json")) continue;
|
|
610
|
+
if (name.startsWith(".hook-tmp-") || name.startsWith(".bad-")) continue;
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
592
613
|
return false;
|
|
593
614
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
615
|
+
function readFileTrimmedOrEmpty(path) {
|
|
616
|
+
try {
|
|
617
|
+
return readFileSync(path, "utf8").trim();
|
|
618
|
+
} catch {
|
|
619
|
+
return "";
|
|
620
|
+
}
|
|
598
621
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
622
|
+
function readLockMetadata(lockDir) {
|
|
623
|
+
const pid = readFileTrimmedOrEmpty(join(lockDir, "pid"));
|
|
624
|
+
const owner = readFileTrimmedOrEmpty(join(lockDir, "owner"));
|
|
625
|
+
const tsRaw = readFileTrimmedOrEmpty(join(lockDir, "ts"));
|
|
626
|
+
const ts = tsRaw === "" ? null : Number.parseInt(tsRaw, 10);
|
|
627
|
+
return {
|
|
628
|
+
pid,
|
|
629
|
+
ts: ts === null || !Number.isFinite(ts) ? null : ts,
|
|
630
|
+
owner
|
|
631
|
+
};
|
|
606
632
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
owner
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
function isPidAlive$1(pidText) {
|
|
620
|
-
const pid = Number.parseInt(pidText, 10);
|
|
621
|
-
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
622
|
-
try {
|
|
623
|
-
process.kill(pid, 0);
|
|
624
|
-
return true;
|
|
625
|
-
} catch {
|
|
626
|
-
return false;
|
|
633
|
+
function isPidAlive(pidText) {
|
|
634
|
+
const pid = Number.parseInt(pidText, 10);
|
|
635
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
636
|
+
try {
|
|
637
|
+
process.kill(pid, 0);
|
|
638
|
+
return true;
|
|
639
|
+
} catch {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
627
642
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
643
|
+
function lockIsStale(cfg) {
|
|
644
|
+
const snapshot = readLockMetadata(cfg.lockDir);
|
|
645
|
+
const nowS = Math.floor(Date.now() / 1e3);
|
|
646
|
+
if (snapshot.pid) {
|
|
647
|
+
if (isPidAlive(snapshot.pid)) {
|
|
648
|
+
if (snapshot.ts === null) return {
|
|
649
|
+
stale: false,
|
|
650
|
+
snapshot
|
|
651
|
+
};
|
|
652
|
+
return {
|
|
653
|
+
stale: nowS - snapshot.ts > cfg.ttlSeconds,
|
|
654
|
+
snapshot
|
|
655
|
+
};
|
|
656
|
+
}
|
|
638
657
|
return {
|
|
639
|
-
stale:
|
|
658
|
+
stale: true,
|
|
640
659
|
snapshot
|
|
641
660
|
};
|
|
642
661
|
}
|
|
643
|
-
return {
|
|
644
|
-
stale:
|
|
662
|
+
if (snapshot.ts !== null) return {
|
|
663
|
+
stale: nowS - snapshot.ts > cfg.graceSeconds,
|
|
645
664
|
snapshot
|
|
646
665
|
};
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
666
|
+
let mtimeS;
|
|
667
|
+
try {
|
|
668
|
+
mtimeS = Math.floor(statSync(cfg.lockDir).mtimeMs / 1e3);
|
|
669
|
+
} catch {
|
|
670
|
+
return {
|
|
671
|
+
stale: true,
|
|
672
|
+
snapshot
|
|
673
|
+
};
|
|
674
|
+
}
|
|
656
675
|
return {
|
|
657
|
-
stale:
|
|
676
|
+
stale: nowS - mtimeS > cfg.graceSeconds,
|
|
658
677
|
snapshot
|
|
659
678
|
};
|
|
660
679
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
"ts",
|
|
670
|
-
"owner"
|
|
671
|
-
]) try {
|
|
672
|
-
unlinkSync(join(lockDir, name));
|
|
673
|
-
} catch {}
|
|
674
|
-
try {
|
|
675
|
-
rmdirSync(lockDir);
|
|
676
|
-
} catch {}
|
|
677
|
-
}
|
|
678
|
-
function snapshotsEqual(a, b) {
|
|
679
|
-
return a.pid === b.pid && a.ts === b.ts && a.owner === b.owner;
|
|
680
|
-
}
|
|
681
|
-
function cleanupLockDirIfUnchanged$1(lockDir, snapshot) {
|
|
682
|
-
if (snapshotsEqual(readLockMetadata$1(lockDir), snapshot)) cleanupLockDir$1(lockDir);
|
|
683
|
-
}
|
|
684
|
-
function isErrnoException$1(err) {
|
|
685
|
-
return typeof err === "object" && err !== null && "code" in err;
|
|
686
|
-
}
|
|
687
|
-
async function sleep$1(ms) {
|
|
688
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Run `fn` while holding the claude-hook-ingest lock. Throws
|
|
692
|
-
* `LockBusyError` when the lock cannot be acquired within
|
|
693
|
-
* `LOCK_ACQUIRE_ATTEMPTS` attempts.
|
|
694
|
-
*
|
|
695
|
-
* The lock is a directory at `lockDir`, with three sentinel files
|
|
696
|
-
* (`pid`, `ts`, `owner`) recording who currently holds it. Stale locks
|
|
697
|
-
* are detected via PID liveness, TTL, and a grace window for the
|
|
698
|
-
* race between mkdir and writing pid/ts.
|
|
699
|
-
*/
|
|
700
|
-
async function withClaudeHookIngestLock(fn) {
|
|
701
|
-
const cfg = lockConfig$1();
|
|
702
|
-
mkdirSync(dirname(cfg.lockDir), { recursive: true });
|
|
703
|
-
const ownerToken = `${process.pid}-${Math.floor(Date.now() / 1e3)}-${randomInt(1e3, 1e4)}`;
|
|
704
|
-
let acquired = false;
|
|
705
|
-
for (let attempt = 0; attempt < LOCK_ACQUIRE_ATTEMPTS$1; attempt++) {
|
|
680
|
+
function cleanupLockDir(lockDir) {
|
|
681
|
+
for (const name of [
|
|
682
|
+
"pid",
|
|
683
|
+
"ts",
|
|
684
|
+
"owner"
|
|
685
|
+
]) try {
|
|
686
|
+
unlinkSync(join(lockDir, name));
|
|
687
|
+
} catch {}
|
|
706
688
|
try {
|
|
707
|
-
|
|
708
|
-
} catch
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
689
|
+
rmdirSync(lockDir);
|
|
690
|
+
} catch {}
|
|
691
|
+
}
|
|
692
|
+
function snapshotsEqual(a, b) {
|
|
693
|
+
return a.pid === b.pid && a.ts === b.ts && a.owner === b.owner;
|
|
694
|
+
}
|
|
695
|
+
function cleanupLockDirIfUnchanged(lockDir, snapshot) {
|
|
696
|
+
if (snapshotsEqual(readLockMetadata(lockDir), snapshot)) cleanupLockDir(lockDir);
|
|
697
|
+
}
|
|
698
|
+
function isErrnoException(err) {
|
|
699
|
+
return typeof err === "object" && err !== null && "code" in err;
|
|
700
|
+
}
|
|
701
|
+
function sleep(ms) {
|
|
702
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Run `fn` while holding the client's ingest lock. Throws the
|
|
706
|
+
* client-specific `LockBusyError` when the lock cannot be acquired
|
|
707
|
+
* within `cfg.lockAcquireAttempts` attempts.
|
|
708
|
+
*
|
|
709
|
+
* The lock is a directory at `lockDir`, with three sentinel files
|
|
710
|
+
* (`pid`, `ts`, `owner`) recording who currently holds it. Stale locks
|
|
711
|
+
* are detected via PID liveness, TTL, and a grace window for the
|
|
712
|
+
* race between mkdir and writing pid/ts.
|
|
713
|
+
*/
|
|
714
|
+
async function withLock(fn) {
|
|
715
|
+
const lock = lockConfig();
|
|
716
|
+
mkdirSync(dirname(lock.lockDir), { recursive: true });
|
|
717
|
+
const ownerToken = `${process.pid}-${Math.floor(Date.now() / 1e3)}-${randomInt(1e3, 1e4)}`;
|
|
718
|
+
let acquired = false;
|
|
719
|
+
for (let attempt = 0; attempt < cfg.lockAcquireAttempts; attempt++) {
|
|
720
|
+
try {
|
|
721
|
+
mkdirSync(lock.lockDir);
|
|
722
|
+
} catch (err) {
|
|
723
|
+
if (isErrnoException(err) && err.code === "EEXIST") {
|
|
724
|
+
const { stale, snapshot } = lockIsStale(lock);
|
|
725
|
+
if (stale) cleanupLockDirIfUnchanged(lock.lockDir, snapshot);
|
|
726
|
+
}
|
|
727
|
+
await sleep(LOCK_ACQUIRE_BACKOFF_MS);
|
|
713
728
|
continue;
|
|
714
729
|
}
|
|
715
|
-
|
|
716
|
-
|
|
730
|
+
try {
|
|
731
|
+
writeFileSync(join(lock.lockDir, "ts"), String(Math.floor(Date.now() / 1e3)), { encoding: "utf8" });
|
|
732
|
+
writeFileSync(join(lock.lockDir, "pid"), String(process.pid), { encoding: "utf8" });
|
|
733
|
+
writeFileSync(join(lock.lockDir, "owner"), ownerToken, { encoding: "utf8" });
|
|
734
|
+
acquired = true;
|
|
735
|
+
break;
|
|
736
|
+
} catch {
|
|
737
|
+
cleanupLockDir(lock.lockDir);
|
|
738
|
+
await sleep(LOCK_ACQUIRE_BACKOFF_MS);
|
|
739
|
+
}
|
|
717
740
|
}
|
|
741
|
+
if (!acquired) throw new LockBusyError();
|
|
718
742
|
try {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
acquired = true;
|
|
723
|
-
break;
|
|
724
|
-
} catch {
|
|
725
|
-
cleanupLockDir$1(cfg.lockDir);
|
|
726
|
-
await sleep$1(LOCK_ACQUIRE_BACKOFF_MS$1);
|
|
743
|
+
return await fn();
|
|
744
|
+
} finally {
|
|
745
|
+
if (readFileTrimmedOrEmpty(join(lock.lockDir, "owner")) === ownerToken) cleanupLockDir(lock.lockDir);
|
|
727
746
|
}
|
|
728
747
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* Persist a payload to the spool directory using a tmp+rename so that
|
|
738
|
-
* a partially-written file is never visible to the drainer. Returns
|
|
739
|
-
* true on success, false on any I/O failure.
|
|
740
|
-
*/
|
|
741
|
-
function spoolPayload(payload) {
|
|
742
|
-
const dir = spoolDir();
|
|
743
|
-
try {
|
|
744
|
-
mkdirSync(dir, { recursive: true });
|
|
745
|
-
} catch {
|
|
746
|
-
logHookEvent("codemem claude-hook-ingest failed to create spool dir");
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
const payloadText = JSON.stringify(payload);
|
|
750
|
-
const tmpPath = join(dir, `.hook-tmp-${process.pid}-${Date.now()}-${randomInt(1e3, 1e4)}.json`);
|
|
751
|
-
try {
|
|
752
|
-
writeFileSync(tmpPath, payloadText, { encoding: "utf8" });
|
|
753
|
-
} catch {
|
|
754
|
-
logHookEvent("codemem claude-hook-ingest failed to allocate spool temp file");
|
|
755
|
-
return false;
|
|
756
|
-
}
|
|
757
|
-
const finalPath = join(dir, `hook-${Math.floor(Date.now() / 1e3)}-${process.pid}-${randomInt(1e3, 1e4)}.json`);
|
|
758
|
-
try {
|
|
759
|
-
renameSync(tmpPath, finalPath);
|
|
760
|
-
} catch {
|
|
748
|
+
/**
|
|
749
|
+
* Persist a payload to the spool directory using a tmp+rename so that
|
|
750
|
+
* a partially-written file is never visible to the drainer. Returns
|
|
751
|
+
* true on success, false on any I/O failure.
|
|
752
|
+
*/
|
|
753
|
+
function spoolPayload(payload) {
|
|
754
|
+
const dir = spoolDir();
|
|
761
755
|
try {
|
|
762
|
-
|
|
763
|
-
} catch {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Promote any `.hook-tmp-*.json` files older than `ttlSeconds` to a
|
|
772
|
-
* recovered name so they are picked up by the next drain. Caller is
|
|
773
|
-
* responsible for passing the same TTL used by lock acquisition so
|
|
774
|
-
* that an in-flight write inside an active locked region is never
|
|
775
|
-
* mistaken for a crashed-writer leftover.
|
|
776
|
-
*/
|
|
777
|
-
function recoverStaleTmpSpool(ttlSeconds) {
|
|
778
|
-
const dir = spoolDir();
|
|
779
|
-
try {
|
|
780
|
-
mkdirSync(dir, { recursive: true });
|
|
781
|
-
} catch {
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
let entries;
|
|
785
|
-
try {
|
|
786
|
-
entries = readdirSync(dir);
|
|
787
|
-
} catch {
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
const nowS = Date.now() / 1e3;
|
|
791
|
-
for (const name of entries) {
|
|
792
|
-
if (!name.startsWith(".hook-tmp-") || !name.endsWith(".json")) continue;
|
|
793
|
-
const tmpPath = join(dir, name);
|
|
794
|
-
let mtimeS;
|
|
756
|
+
mkdirSync(dir, { recursive: true });
|
|
757
|
+
} catch {
|
|
758
|
+
logHookEvent(`${cfg.logPrefix} failed to create spool dir`);
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
const payloadText = JSON.stringify(payload);
|
|
762
|
+
const tmpName = `.hook-tmp-${process.pid}-${Date.now()}-${randomInt(1e3, 1e4)}.json`;
|
|
763
|
+
const tmpPath = join(dir, tmpName);
|
|
795
764
|
try {
|
|
796
|
-
|
|
765
|
+
writeFileSync(tmpPath, payloadText, { encoding: "utf8" });
|
|
797
766
|
} catch {
|
|
798
|
-
|
|
767
|
+
logHookEvent(`${cfg.logPrefix} failed to allocate spool temp file`);
|
|
768
|
+
return false;
|
|
799
769
|
}
|
|
800
|
-
|
|
801
|
-
const
|
|
770
|
+
const finalName = `hook-${Math.floor(Date.now() / 1e3)}-${process.pid}-${randomInt(1e3, 1e4)}.json`;
|
|
771
|
+
const finalPath = join(dir, finalName);
|
|
802
772
|
try {
|
|
803
|
-
renameSync(tmpPath,
|
|
804
|
-
|
|
805
|
-
|
|
773
|
+
renameSync(tmpPath, finalPath);
|
|
774
|
+
} catch {
|
|
775
|
+
try {
|
|
776
|
+
unlinkSync(tmpPath);
|
|
777
|
+
} catch {}
|
|
778
|
+
logHookEvent(`${cfg.logPrefix} failed to spool payload`);
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
logHookEvent(`${cfg.logPrefix} spooled payload: ${finalPath}`);
|
|
782
|
+
return true;
|
|
806
783
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*/
|
|
814
|
-
function
|
|
815
|
-
|
|
816
|
-
const quarantineName = `.bad-${reason}-${Date.now()}-${randomInt(1e3, 1e4)}-${name}`;
|
|
817
|
-
try {
|
|
818
|
-
renameSync(sourcePath, join(dir, quarantineName));
|
|
819
|
-
logHookEvent(`codemem claude-hook-ingest quarantined corrupt spool payload (${reason}): ${quarantineName}`);
|
|
820
|
-
} catch {
|
|
784
|
+
/**
|
|
785
|
+
* Promote any `.hook-tmp-*.json` files older than `ttlSeconds` to a
|
|
786
|
+
* recovered name so they are picked up by the next drain. Caller is
|
|
787
|
+
* responsible for passing the same TTL used by lock acquisition so
|
|
788
|
+
* that an in-flight write inside an active locked region is never
|
|
789
|
+
* mistaken for a crashed-writer leftover.
|
|
790
|
+
*/
|
|
791
|
+
function recoverStaleTmpSpool(ttlSeconds) {
|
|
792
|
+
const dir = spoolDir();
|
|
821
793
|
try {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* Process every queued payload in the spool directory in lexicographic
|
|
829
|
-
* order (which approximates oldest-first because filenames embed the
|
|
830
|
-
* second-precision creation timestamp). The handler returns true to
|
|
831
|
-
* indicate the payload has been durably accepted; only then is the
|
|
832
|
-
* spool entry deleted. Failed entries are left on disk for the next
|
|
833
|
-
* drain attempt.
|
|
834
|
-
*/
|
|
835
|
-
async function drainSpool(handler) {
|
|
836
|
-
const dir = spoolDir();
|
|
837
|
-
try {
|
|
838
|
-
mkdirSync(dir, { recursive: true });
|
|
839
|
-
} catch {
|
|
840
|
-
return {
|
|
841
|
-
processed: 0,
|
|
842
|
-
failed: 0
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
let entries;
|
|
846
|
-
try {
|
|
847
|
-
entries = readdirSync(dir);
|
|
848
|
-
} catch {
|
|
849
|
-
return {
|
|
850
|
-
processed: 0,
|
|
851
|
-
failed: 0
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
const queued = entries.filter((name) => name.endsWith(".json") && !name.startsWith(".hook-tmp-") && !name.startsWith(".bad-")).sort();
|
|
855
|
-
const result = {
|
|
856
|
-
processed: 0,
|
|
857
|
-
failed: 0
|
|
858
|
-
};
|
|
859
|
-
for (const name of queued) {
|
|
860
|
-
const path = join(dir, name);
|
|
861
|
-
let raw;
|
|
794
|
+
mkdirSync(dir, { recursive: true });
|
|
795
|
+
} catch {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
let entries;
|
|
862
799
|
try {
|
|
863
|
-
|
|
800
|
+
entries = readdirSync(dir);
|
|
864
801
|
} catch {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const nowS = Date.now() / 1e3;
|
|
805
|
+
for (const name of entries) {
|
|
806
|
+
if (!name.startsWith(".hook-tmp-") || !name.endsWith(".json")) continue;
|
|
807
|
+
const tmpPath = join(dir, name);
|
|
808
|
+
let mtimeS;
|
|
809
|
+
try {
|
|
810
|
+
mtimeS = statSync(tmpPath).mtimeMs / 1e3;
|
|
811
|
+
} catch {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (nowS - mtimeS <= ttlSeconds) continue;
|
|
815
|
+
const recoveredName = `hook-recovered-${Math.floor(nowS)}-${process.pid}-${randomInt(1e3, 1e4)}.json`;
|
|
816
|
+
const recoveredPath = join(dir, recoveredName);
|
|
817
|
+
try {
|
|
818
|
+
renameSync(tmpPath, recoveredPath);
|
|
819
|
+
logHookEvent(`${cfg.logPrefix} recovered stale temp spool payload: ${recoveredPath}`);
|
|
820
|
+
} catch {}
|
|
868
821
|
}
|
|
869
|
-
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Move a permanently-broken spool entry out of the queue so that it
|
|
825
|
+
* stops being picked up by future drains. The entry is renamed in
|
|
826
|
+
* place with a `.bad-<reason>-` prefix so an operator can inspect or
|
|
827
|
+
* delete it manually.
|
|
828
|
+
*/
|
|
829
|
+
function quarantineSpoolEntry(dir, name, reason) {
|
|
830
|
+
const sourcePath = join(dir, name);
|
|
831
|
+
const quarantineName = `.bad-${reason}-${Date.now()}-${randomInt(1e3, 1e4)}-${name}`;
|
|
870
832
|
try {
|
|
871
|
-
|
|
833
|
+
renameSync(sourcePath, join(dir, quarantineName));
|
|
834
|
+
logHookEvent(`${cfg.logPrefix} quarantined corrupt spool payload (${reason}): ${quarantineName}`);
|
|
872
835
|
} catch {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
836
|
+
try {
|
|
837
|
+
unlinkSync(sourcePath);
|
|
838
|
+
logHookEvent(`${cfg.logPrefix} dropped corrupt spool payload (${reason}): ${name}`);
|
|
839
|
+
} catch {}
|
|
876
840
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Process every queued payload in the spool directory in lexicographic
|
|
844
|
+
* order (which approximates oldest-first because filenames embed the
|
|
845
|
+
* second-precision creation timestamp). The handler returns true to
|
|
846
|
+
* indicate the payload has been durably accepted; only then is the
|
|
847
|
+
* spool entry deleted. Failed entries are left on disk for the next
|
|
848
|
+
* drain attempt.
|
|
849
|
+
*/
|
|
850
|
+
async function drainSpool(handler) {
|
|
851
|
+
const dir = spoolDir();
|
|
852
|
+
try {
|
|
853
|
+
mkdirSync(dir, { recursive: true });
|
|
854
|
+
} catch {
|
|
855
|
+
return {
|
|
856
|
+
processed: 0,
|
|
857
|
+
failed: 0
|
|
858
|
+
};
|
|
880
859
|
}
|
|
881
|
-
let
|
|
860
|
+
let entries;
|
|
882
861
|
try {
|
|
883
|
-
|
|
862
|
+
entries = readdirSync(dir);
|
|
884
863
|
} catch {
|
|
885
|
-
|
|
864
|
+
return {
|
|
865
|
+
processed: 0,
|
|
866
|
+
failed: 0
|
|
867
|
+
};
|
|
886
868
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
869
|
+
const queued = entries.filter((name) => name.endsWith(".json") && !name.startsWith(".hook-tmp-") && !name.startsWith(".bad-")).sort();
|
|
870
|
+
const result = {
|
|
871
|
+
processed: 0,
|
|
872
|
+
failed: 0
|
|
873
|
+
};
|
|
874
|
+
for (const name of queued) {
|
|
875
|
+
const path = join(dir, name);
|
|
876
|
+
let raw;
|
|
877
|
+
try {
|
|
878
|
+
raw = readFileSync(path, "utf8");
|
|
879
|
+
} catch {
|
|
880
|
+
logHookEvent(`${cfg.logPrefix} failed to read spooled payload: ${path}`);
|
|
881
|
+
result.failed++;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
let parsed;
|
|
885
|
+
try {
|
|
886
|
+
parsed = JSON.parse(raw);
|
|
887
|
+
} catch {
|
|
888
|
+
quarantineSpoolEntry(dir, name, "parse-error");
|
|
889
|
+
result.failed++;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
893
|
+
quarantineSpoolEntry(dir, name, "wrong-shape");
|
|
894
|
+
result.failed++;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
let ok = false;
|
|
898
|
+
try {
|
|
899
|
+
ok = await handler(parsed);
|
|
900
|
+
} catch {
|
|
901
|
+
ok = false;
|
|
902
|
+
}
|
|
903
|
+
if (ok) try {
|
|
904
|
+
unlinkSync(path);
|
|
905
|
+
result.processed++;
|
|
906
|
+
} catch {}
|
|
907
|
+
else {
|
|
908
|
+
logHookEvent(`${cfg.logPrefix} failed processing spooled payload: ${path}`);
|
|
909
|
+
result.failed++;
|
|
910
|
+
}
|
|
894
911
|
}
|
|
912
|
+
return result;
|
|
895
913
|
}
|
|
896
|
-
return
|
|
914
|
+
return {
|
|
915
|
+
LockBusyError,
|
|
916
|
+
spoolDir,
|
|
917
|
+
lockTtlSeconds,
|
|
918
|
+
hasSpooledEntries,
|
|
919
|
+
withLock,
|
|
920
|
+
spoolPayload,
|
|
921
|
+
recoverStaleTmpSpool,
|
|
922
|
+
drainSpool
|
|
923
|
+
};
|
|
897
924
|
}
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/commands/claude-hook-ingest-spool.ts
|
|
927
|
+
/**
|
|
928
|
+
* Durability layer for `claude-hook-ingest`. The lock/spool/drain/
|
|
929
|
+
* quarantine machinery is shared in `hook-ingest-spool.ts`; this file
|
|
930
|
+
* wires the claude-specific config (dirs, TTL 300s, 100 acquire
|
|
931
|
+
* attempts, error name/message) and keeps the claude flush predicate.
|
|
932
|
+
*/
|
|
933
|
+
var spool$1 = createHookIngestSpool({
|
|
934
|
+
logPrefix: "codemem claude-hook-ingest",
|
|
935
|
+
lockDirEnv: "CODEMEM_CLAUDE_HOOK_LOCK_DIR",
|
|
936
|
+
lockDirDefault: "~/.codemem/claude-hook-ingest.lock",
|
|
937
|
+
lockTtlEnv: "CODEMEM_CLAUDE_HOOK_LOCK_TTL_S",
|
|
938
|
+
lockTtlDefault: 300,
|
|
939
|
+
lockGraceEnv: "CODEMEM_CLAUDE_HOOK_LOCK_GRACE_S",
|
|
940
|
+
lockGraceDefault: 2,
|
|
941
|
+
lockAcquireAttempts: 100,
|
|
942
|
+
spoolDirEnv: "CODEMEM_CLAUDE_HOOK_SPOOL_DIR",
|
|
943
|
+
spoolDirDefault: "~/.codemem/claude-hook-spool",
|
|
944
|
+
lockBusyErrorName: "LockBusyError",
|
|
945
|
+
lockBusyErrorMessage: "claude-hook-ingest lock busy"
|
|
946
|
+
});
|
|
947
|
+
var LockBusyError = spool$1.LockBusyError;
|
|
948
|
+
var withClaudeHookIngestLock = spool$1.withLock;
|
|
949
|
+
var spoolPayload = spool$1.spoolPayload;
|
|
950
|
+
var drainSpool = spool$1.drainSpool;
|
|
951
|
+
var hasSpooledEntries = spool$1.hasSpooledEntries;
|
|
952
|
+
var recoverStaleTmpSpool = spool$1.recoverStaleTmpSpool;
|
|
953
|
+
var lockTtlSeconds = spool$1.lockTtlSeconds;
|
|
954
|
+
spool$1.spoolDir;
|
|
898
955
|
/**
|
|
899
956
|
* Whether the boundary-flush write-through should run for this hook
|
|
900
957
|
* payload. SessionEnd defaults to forcing a flush; Stop only flushes
|
|
@@ -908,14 +965,6 @@ function shouldForceBoundaryFlush(payload) {
|
|
|
908
965
|
if (!envTruthy$2("CODEMEM_CLAUDE_HOOK_FLUSH", false)) return false;
|
|
909
966
|
return envTruthy$2("CODEMEM_CLAUDE_HOOK_FLUSH_ON_STOP", false);
|
|
910
967
|
}
|
|
911
|
-
/**
|
|
912
|
-
* Returns the configured lock TTL so callers (`claude-hook-ingest`)
|
|
913
|
-
* can pass the same value to `recoverStaleTmpSpool` without re-reading
|
|
914
|
-
* the env.
|
|
915
|
-
*/
|
|
916
|
-
function lockTtlSeconds() {
|
|
917
|
-
return lockConfig$1().ttlSeconds;
|
|
918
|
-
}
|
|
919
968
|
//#endregion
|
|
920
969
|
//#region src/commands/claude-hook-session-state.ts
|
|
921
970
|
/**
|
|
@@ -945,14 +994,14 @@ function defaultSessionState() {
|
|
|
945
994
|
updated_at: ""
|
|
946
995
|
};
|
|
947
996
|
}
|
|
948
|
-
function expandHome
|
|
997
|
+
function expandHome(value) {
|
|
949
998
|
if (value === "~") return homedir();
|
|
950
999
|
if (value.startsWith("~/")) return join(homedir(), value.slice(2));
|
|
951
1000
|
return value;
|
|
952
1001
|
}
|
|
953
1002
|
function contextDir() {
|
|
954
1003
|
const override = process.env.CODEMEM_CLAUDE_HOOK_CONTEXT_DIR;
|
|
955
|
-
return expandHome
|
|
1004
|
+
return expandHome(override?.trim() ? override : "~/.codemem/claude-hook-context");
|
|
956
1005
|
}
|
|
957
1006
|
function sessionFileStem(sessionId) {
|
|
958
1007
|
const trimmed = sessionId.trim();
|
|
@@ -1004,7 +1053,8 @@ function nowIso() {
|
|
|
1004
1053
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1005
1054
|
}
|
|
1006
1055
|
function saveSessionState(sessionId, state) {
|
|
1007
|
-
|
|
1056
|
+
const dir = contextDir();
|
|
1057
|
+
mkdirSync(dir, { recursive: true });
|
|
1008
1058
|
const path = statePathForSession(sessionId);
|
|
1009
1059
|
const tmpPath = `${path}.tmp`;
|
|
1010
1060
|
const payload = {
|
|
@@ -1381,8 +1431,8 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
|
|
|
1381
1431
|
};
|
|
1382
1432
|
const httpResult = await httpIngest(httpPayload(payload), opts.host, port);
|
|
1383
1433
|
if (httpResult.ok) {
|
|
1384
|
-
await flushOnBoundaryIfRequested();
|
|
1385
1434
|
await drainBacklogIfPresent();
|
|
1435
|
+
await flushOnBoundaryIfRequested();
|
|
1386
1436
|
return {
|
|
1387
1437
|
inserted: httpResult.inserted,
|
|
1388
1438
|
skipped: httpResult.skipped,
|
|
@@ -1526,7 +1576,8 @@ function extractInjectContext(payload) {
|
|
|
1526
1576
|
return normalizePromptText(payload.prompt) || null;
|
|
1527
1577
|
}
|
|
1528
1578
|
function resolveInjectProject$1(payload) {
|
|
1529
|
-
|
|
1579
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
|
|
1580
|
+
return resolveHookProject(cwd, payload.project);
|
|
1530
1581
|
}
|
|
1531
1582
|
async function buildLocalPack$1(context, project, dbPath, workingSetPaths = []) {
|
|
1532
1583
|
const store = new MemoryStore(dbPath);
|
|
@@ -1622,273 +1673,34 @@ var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
|
|
|
1622
1673
|
});
|
|
1623
1674
|
//#endregion
|
|
1624
1675
|
//#region src/commands/codex-hook-ingest-spool.ts
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
}
|
|
1654
|
-
function codexHookLockTtlSeconds() {
|
|
1655
|
-
return lockConfig().ttlSeconds;
|
|
1656
|
-
}
|
|
1657
|
-
function hasCodexHookSpooledEntries() {
|
|
1658
|
-
let entries;
|
|
1659
|
-
try {
|
|
1660
|
-
entries = readdirSync(codexHookSpoolDir());
|
|
1661
|
-
} catch {
|
|
1662
|
-
return false;
|
|
1663
|
-
}
|
|
1664
|
-
return entries.some((name) => name.endsWith(".json") && !name.startsWith(".hook-tmp-") && !name.startsWith(".bad-"));
|
|
1665
|
-
}
|
|
1666
|
-
function readTrimmed(path) {
|
|
1667
|
-
try {
|
|
1668
|
-
return readFileSync(path, "utf8").trim();
|
|
1669
|
-
} catch {
|
|
1670
|
-
return "";
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
function readLockMetadata(lockDir) {
|
|
1674
|
-
const rawTs = readTrimmed(join(lockDir, "ts"));
|
|
1675
|
-
const ts = rawTs === "" ? null : Number.parseInt(rawTs, 10);
|
|
1676
|
-
return {
|
|
1677
|
-
pid: readTrimmed(join(lockDir, "pid")),
|
|
1678
|
-
ts: ts === null || !Number.isFinite(ts) ? null : ts,
|
|
1679
|
-
owner: readTrimmed(join(lockDir, "owner"))
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
function isPidAlive(pidText) {
|
|
1683
|
-
const pid = Number.parseInt(pidText, 10);
|
|
1684
|
-
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
1685
|
-
try {
|
|
1686
|
-
process.kill(pid, 0);
|
|
1687
|
-
return true;
|
|
1688
|
-
} catch {
|
|
1689
|
-
return false;
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
function lockIsStale(cfg) {
|
|
1693
|
-
const snapshot = readLockMetadata(cfg.lockDir);
|
|
1694
|
-
const nowS = Math.floor(Date.now() / 1e3);
|
|
1695
|
-
if (snapshot.pid) {
|
|
1696
|
-
if (isPidAlive(snapshot.pid)) return {
|
|
1697
|
-
stale: snapshot.ts !== null && nowS - snapshot.ts > cfg.ttlSeconds,
|
|
1698
|
-
snapshot
|
|
1699
|
-
};
|
|
1700
|
-
return {
|
|
1701
|
-
stale: true,
|
|
1702
|
-
snapshot
|
|
1703
|
-
};
|
|
1704
|
-
}
|
|
1705
|
-
if (snapshot.ts !== null) return {
|
|
1706
|
-
stale: nowS - snapshot.ts > cfg.graceSeconds,
|
|
1707
|
-
snapshot
|
|
1708
|
-
};
|
|
1709
|
-
try {
|
|
1710
|
-
return {
|
|
1711
|
-
stale: nowS - Math.floor(statSync(cfg.lockDir).mtimeMs / 1e3) > cfg.graceSeconds,
|
|
1712
|
-
snapshot
|
|
1713
|
-
};
|
|
1714
|
-
} catch {
|
|
1715
|
-
return {
|
|
1716
|
-
stale: true,
|
|
1717
|
-
snapshot
|
|
1718
|
-
};
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
function cleanupLockDir(lockDir) {
|
|
1722
|
-
for (const name of [
|
|
1723
|
-
"pid",
|
|
1724
|
-
"ts",
|
|
1725
|
-
"owner"
|
|
1726
|
-
]) try {
|
|
1727
|
-
unlinkSync(join(lockDir, name));
|
|
1728
|
-
} catch {}
|
|
1729
|
-
try {
|
|
1730
|
-
rmdirSync(lockDir);
|
|
1731
|
-
} catch {}
|
|
1732
|
-
}
|
|
1733
|
-
function cleanupLockDirIfUnchanged(lockDir, snapshot) {
|
|
1734
|
-
const current = readLockMetadata(lockDir);
|
|
1735
|
-
if (current.pid === snapshot.pid && current.ts === snapshot.ts && current.owner === snapshot.owner) cleanupLockDir(lockDir);
|
|
1736
|
-
}
|
|
1737
|
-
function isErrnoException(err) {
|
|
1738
|
-
return typeof err === "object" && err !== null && "code" in err;
|
|
1739
|
-
}
|
|
1740
|
-
function sleep(ms) {
|
|
1741
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1742
|
-
}
|
|
1743
|
-
async function withCodexHookIngestLock(fn) {
|
|
1744
|
-
const cfg = lockConfig();
|
|
1745
|
-
mkdirSync(dirname(cfg.lockDir), { recursive: true });
|
|
1746
|
-
const ownerToken = `${process.pid}-${Math.floor(Date.now() / 1e3)}-${randomInt(1e3, 1e4)}`;
|
|
1747
|
-
let acquired = false;
|
|
1748
|
-
for (let attempt = 0; attempt < LOCK_ACQUIRE_ATTEMPTS; attempt++) {
|
|
1749
|
-
try {
|
|
1750
|
-
mkdirSync(cfg.lockDir);
|
|
1751
|
-
} catch (err) {
|
|
1752
|
-
if (isErrnoException(err) && err.code === "EEXIST") {
|
|
1753
|
-
const { stale, snapshot } = lockIsStale(cfg);
|
|
1754
|
-
if (stale) cleanupLockDirIfUnchanged(cfg.lockDir, snapshot);
|
|
1755
|
-
}
|
|
1756
|
-
await sleep(LOCK_ACQUIRE_BACKOFF_MS);
|
|
1757
|
-
continue;
|
|
1758
|
-
}
|
|
1759
|
-
try {
|
|
1760
|
-
writeFileSync(join(cfg.lockDir, "ts"), String(Math.floor(Date.now() / 1e3)), "utf8");
|
|
1761
|
-
writeFileSync(join(cfg.lockDir, "pid"), String(process.pid), "utf8");
|
|
1762
|
-
writeFileSync(join(cfg.lockDir, "owner"), ownerToken, "utf8");
|
|
1763
|
-
acquired = true;
|
|
1764
|
-
break;
|
|
1765
|
-
} catch {
|
|
1766
|
-
cleanupLockDir(cfg.lockDir);
|
|
1767
|
-
await sleep(LOCK_ACQUIRE_BACKOFF_MS);
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
if (!acquired) throw new CodexHookLockBusyError();
|
|
1771
|
-
try {
|
|
1772
|
-
return await fn();
|
|
1773
|
-
} finally {
|
|
1774
|
-
if (readTrimmed(join(cfg.lockDir, "owner")) === ownerToken) cleanupLockDir(cfg.lockDir);
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
function spoolCodexHookPayload(payload) {
|
|
1778
|
-
const dir = codexHookSpoolDir();
|
|
1779
|
-
try {
|
|
1780
|
-
mkdirSync(dir, { recursive: true });
|
|
1781
|
-
} catch {
|
|
1782
|
-
logHookEvent("codemem codex-hook-ingest failed to create spool dir");
|
|
1783
|
-
return false;
|
|
1784
|
-
}
|
|
1785
|
-
const tmpPath = join(dir, `.hook-tmp-${process.pid}-${Date.now()}-${randomInt(1e3, 1e4)}.json`);
|
|
1786
|
-
try {
|
|
1787
|
-
writeFileSync(tmpPath, JSON.stringify(payload), "utf8");
|
|
1788
|
-
} catch {
|
|
1789
|
-
logHookEvent("codemem codex-hook-ingest failed to allocate spool temp file");
|
|
1790
|
-
return false;
|
|
1791
|
-
}
|
|
1792
|
-
const finalPath = join(dir, `hook-${Math.floor(Date.now() / 1e3)}-${process.pid}-${randomInt(1e3, 1e4)}.json`);
|
|
1793
|
-
try {
|
|
1794
|
-
renameSync(tmpPath, finalPath);
|
|
1795
|
-
} catch {
|
|
1796
|
-
try {
|
|
1797
|
-
unlinkSync(tmpPath);
|
|
1798
|
-
} catch {}
|
|
1799
|
-
logHookEvent("codemem codex-hook-ingest failed to spool payload");
|
|
1800
|
-
return false;
|
|
1801
|
-
}
|
|
1802
|
-
logHookEvent(`codemem codex-hook-ingest spooled payload: ${finalPath}`);
|
|
1803
|
-
return true;
|
|
1804
|
-
}
|
|
1805
|
-
function recoverStaleCodexHookTmpSpool(ttlSeconds) {
|
|
1806
|
-
const dir = codexHookSpoolDir();
|
|
1807
|
-
try {
|
|
1808
|
-
mkdirSync(dir, { recursive: true });
|
|
1809
|
-
} catch {
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
let entries;
|
|
1813
|
-
try {
|
|
1814
|
-
entries = readdirSync(dir);
|
|
1815
|
-
} catch {
|
|
1816
|
-
return;
|
|
1817
|
-
}
|
|
1818
|
-
const nowS = Date.now() / 1e3;
|
|
1819
|
-
for (const name of entries) {
|
|
1820
|
-
if (!name.startsWith(".hook-tmp-") || !name.endsWith(".json")) continue;
|
|
1821
|
-
const tmpPath = join(dir, name);
|
|
1822
|
-
try {
|
|
1823
|
-
if (nowS - statSync(tmpPath).mtimeMs / 1e3 <= ttlSeconds) continue;
|
|
1824
|
-
renameSync(tmpPath, join(dir, `hook-recovered-${Math.floor(nowS)}-${process.pid}-${randomInt(1e3, 1e4)}.json`));
|
|
1825
|
-
} catch {}
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
function quarantineSpoolEntry(dir, name, reason) {
|
|
1829
|
-
try {
|
|
1830
|
-
renameSync(join(dir, name), join(dir, `.bad-${reason}-${Date.now()}-${randomInt(1e3, 1e4)}-${name}`));
|
|
1831
|
-
} catch {
|
|
1832
|
-
try {
|
|
1833
|
-
unlinkSync(join(dir, name));
|
|
1834
|
-
} catch {}
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
async function drainCodexHookSpool(handler) {
|
|
1838
|
-
const dir = codexHookSpoolDir();
|
|
1839
|
-
try {
|
|
1840
|
-
mkdirSync(dir, { recursive: true });
|
|
1841
|
-
} catch {
|
|
1842
|
-
return {
|
|
1843
|
-
processed: 0,
|
|
1844
|
-
failed: 0
|
|
1845
|
-
};
|
|
1846
|
-
}
|
|
1847
|
-
let entries;
|
|
1848
|
-
try {
|
|
1849
|
-
entries = readdirSync(dir);
|
|
1850
|
-
} catch {
|
|
1851
|
-
return {
|
|
1852
|
-
processed: 0,
|
|
1853
|
-
failed: 0
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
const result = {
|
|
1857
|
-
processed: 0,
|
|
1858
|
-
failed: 0
|
|
1859
|
-
};
|
|
1860
|
-
for (const name of entries.filter((entry) => entry.endsWith(".json") && !entry.startsWith(".hook-tmp-") && !entry.startsWith(".bad-")).sort()) {
|
|
1861
|
-
const path = join(dir, name);
|
|
1862
|
-
let parsed;
|
|
1863
|
-
try {
|
|
1864
|
-
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1865
|
-
} catch {
|
|
1866
|
-
quarantineSpoolEntry(dir, name, "parse-error");
|
|
1867
|
-
result.failed++;
|
|
1868
|
-
continue;
|
|
1869
|
-
}
|
|
1870
|
-
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1871
|
-
quarantineSpoolEntry(dir, name, "wrong-shape");
|
|
1872
|
-
result.failed++;
|
|
1873
|
-
continue;
|
|
1874
|
-
}
|
|
1875
|
-
let ok = false;
|
|
1876
|
-
try {
|
|
1877
|
-
ok = await handler(parsed);
|
|
1878
|
-
} catch {
|
|
1879
|
-
ok = false;
|
|
1880
|
-
}
|
|
1881
|
-
if (!ok) {
|
|
1882
|
-
result.failed++;
|
|
1883
|
-
continue;
|
|
1884
|
-
}
|
|
1885
|
-
try {
|
|
1886
|
-
unlinkSync(path);
|
|
1887
|
-
result.processed++;
|
|
1888
|
-
} catch {}
|
|
1889
|
-
}
|
|
1890
|
-
return result;
|
|
1891
|
-
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Durability layer for `codex-hook-ingest`. The lock/spool/drain/
|
|
1678
|
+
* quarantine machinery is shared in `hook-ingest-spool.ts`; this file
|
|
1679
|
+
* wires the codex-specific config (dirs, TTL 120s, 20 acquire attempts,
|
|
1680
|
+
* error name/message).
|
|
1681
|
+
*/
|
|
1682
|
+
var spool = createHookIngestSpool({
|
|
1683
|
+
logPrefix: "codemem codex-hook-ingest",
|
|
1684
|
+
lockDirEnv: "CODEMEM_CODEX_HOOK_LOCK_DIR",
|
|
1685
|
+
lockDirDefault: "~/.codemem/codex-hook-ingest.lock",
|
|
1686
|
+
lockTtlEnv: "CODEMEM_CODEX_HOOK_LOCK_TTL_S",
|
|
1687
|
+
lockTtlDefault: 120,
|
|
1688
|
+
lockGraceEnv: "CODEMEM_CODEX_HOOK_LOCK_GRACE_S",
|
|
1689
|
+
lockGraceDefault: 2,
|
|
1690
|
+
lockAcquireAttempts: 20,
|
|
1691
|
+
spoolDirEnv: "CODEMEM_CODEX_HOOK_SPOOL_DIR",
|
|
1692
|
+
spoolDirDefault: "~/.codemem/codex-hook-spool",
|
|
1693
|
+
lockBusyErrorName: "CodexHookLockBusyError",
|
|
1694
|
+
lockBusyErrorMessage: "codex-hook-ingest lock busy"
|
|
1695
|
+
});
|
|
1696
|
+
var CodexHookLockBusyError = spool.LockBusyError;
|
|
1697
|
+
var withCodexHookIngestLock = spool.withLock;
|
|
1698
|
+
var spoolCodexHookPayload = spool.spoolPayload;
|
|
1699
|
+
var drainCodexHookSpool = spool.drainSpool;
|
|
1700
|
+
var hasCodexHookSpooledEntries = spool.hasSpooledEntries;
|
|
1701
|
+
var recoverStaleCodexHookTmpSpool = spool.recoverStaleTmpSpool;
|
|
1702
|
+
var codexHookLockTtlSeconds = spool.lockTtlSeconds;
|
|
1703
|
+
spool.spoolDir;
|
|
1892
1704
|
//#endregion
|
|
1893
1705
|
//#region src/commands/codex-hook-ingest.ts
|
|
1894
1706
|
/**
|
|
@@ -2388,7 +2200,8 @@ function formatCodexAdditionalContext(packText, maxChars) {
|
|
|
2388
2200
|
return `${CODEMEM_CONTEXT_HEADER}${truncateAdditionalContext(normalized, bodyMaxChars)}`;
|
|
2389
2201
|
}
|
|
2390
2202
|
function resolveInjectProject(payload) {
|
|
2391
|
-
|
|
2203
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
|
|
2204
|
+
return resolveHookProject(cwd, payload.project);
|
|
2392
2205
|
}
|
|
2393
2206
|
function buildCodexInjectQuery(prompt, project) {
|
|
2394
2207
|
return [prompt, project ?? ""].filter((part) => part.trim().length > 0).join(" ").slice(0, 500) || "recent work";
|
|
@@ -3426,8 +3239,8 @@ function escapeSqlLikePattern(value) {
|
|
|
3426
3239
|
}
|
|
3427
3240
|
function formatBytes(bytes) {
|
|
3428
3241
|
if (bytes < 1024) return `${bytes} B`;
|
|
3429
|
-
if (bytes <
|
|
3430
|
-
return `${(bytes /
|
|
3242
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
3243
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
3431
3244
|
}
|
|
3432
3245
|
function parseOptionalPositiveInt$1(value) {
|
|
3433
3246
|
if (!value) return void 0;
|
|
@@ -4116,8 +3929,9 @@ function resolveCandidateTarget(candidate) {
|
|
|
4116
3929
|
path: join(homedir(), ".config", "opencode", "AGENTS.md"),
|
|
4117
3930
|
display: "~/.config/opencode/AGENTS.md"
|
|
4118
3931
|
};
|
|
3932
|
+
const root = resolveProjectRoot(process.cwd()) ?? process.cwd();
|
|
4119
3933
|
return {
|
|
4120
|
-
path: join(
|
|
3934
|
+
path: join(root, "AGENTS.md"),
|
|
4121
3935
|
display: "AGENTS.md"
|
|
4122
3936
|
};
|
|
4123
3937
|
}
|
|
@@ -4358,7 +4172,8 @@ function emitStructuredError(errorCode, message) {
|
|
|
4358
4172
|
async function runEnqueueRawEvent(opts, dependencies = {}) {
|
|
4359
4173
|
try {
|
|
4360
4174
|
const payload = await (dependencies.readPayload ?? readStdinJson)();
|
|
4361
|
-
const
|
|
4175
|
+
const dbPath = resolveDbPath(resolveDbOpt(opts));
|
|
4176
|
+
const store = new MemoryStore(dbPath);
|
|
4362
4177
|
try {
|
|
4363
4178
|
ingestRawEvents(store, payload);
|
|
4364
4179
|
} finally {
|
|
@@ -4480,7 +4295,8 @@ var defaultLogger = {
|
|
|
4480
4295
|
error: (message) => console.error(`Error: ${message}`)
|
|
4481
4296
|
};
|
|
4482
4297
|
function readWorkerSyncConfig(configPath) {
|
|
4483
|
-
|
|
4298
|
+
const config = configPath ? readCodememConfigFileAtPath(configPath) : readCodememConfigFile();
|
|
4299
|
+
return readCoordinatorSyncConfig(config);
|
|
4484
4300
|
}
|
|
4485
4301
|
function createSequentialBackfillCoordinator(store, jobPlans, options) {
|
|
4486
4302
|
const pollIntervalMs = 1e3;
|
|
@@ -4575,7 +4391,7 @@ function startMaintenanceWorkerRuntime(options = {}) {
|
|
|
4575
4391
|
} catch (error) {
|
|
4576
4392
|
logger.warn(`WAL checkpoint failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4577
4393
|
}
|
|
4578
|
-
},
|
|
4394
|
+
}, 3e5);
|
|
4579
4395
|
walCheckpointTimer.unref();
|
|
4580
4396
|
if (!isEmbeddingDisabled()) runners.push(new VectorModelMigrationRunner({
|
|
4581
4397
|
dbPath,
|
|
@@ -5319,7 +5135,7 @@ function createMemoryExtractionReplayCommand() {
|
|
|
5319
5135
|
observerReasoningEffort: opts.reasoningEffort === void 0 ? observerConfig.observerReasoningEffort : opts.reasoningEffort.trim() || null,
|
|
5320
5136
|
observerReasoningSummary: opts.reasoningSummary === void 0 ? observerConfig.observerReasoningSummary : opts.reasoningSummary.trim() || null,
|
|
5321
5137
|
observerMaxOutputTokens: maxOutputTokens ?? observerConfig.observerMaxOutputTokens ?? observerConfig.observerMaxTokens,
|
|
5322
|
-
observerExplicitConfigKeys: maxOutputTokens === null ? observerConfig.observerExplicitConfigKeys : [
|
|
5138
|
+
observerExplicitConfigKeys: maxOutputTokens === null ? observerConfig.observerExplicitConfigKeys : [.../* @__PURE__ */ new Set([...observerConfig.observerExplicitConfigKeys ?? [], "observerMaxOutputTokens"])]
|
|
5323
5139
|
};
|
|
5324
5140
|
const observer = new ObserverClient(observerConfigWithOverrides);
|
|
5325
5141
|
const result = opts.observerTierRouting === true ? await replayBatchExtractionWithTierRouting(resolveDbOpt(opts), observerConfigWithOverrides, {
|
|
@@ -5436,7 +5252,7 @@ function createMemoryExtractionBenchmarkCommand() {
|
|
|
5436
5252
|
observerReasoningEffort: opts.reasoningEffort === void 0 ? observerConfig.observerReasoningEffort : opts.reasoningEffort.trim() || null,
|
|
5437
5253
|
observerReasoningSummary: opts.reasoningSummary === void 0 ? observerConfig.observerReasoningSummary : opts.reasoningSummary.trim() || null,
|
|
5438
5254
|
observerMaxOutputTokens: maxOutputTokens ?? observerConfig.observerMaxOutputTokens ?? observerConfig.observerMaxTokens,
|
|
5439
|
-
observerExplicitConfigKeys: maxOutputTokens === null ? observerConfig.observerExplicitConfigKeys : [
|
|
5255
|
+
observerExplicitConfigKeys: maxOutputTokens === null ? observerConfig.observerExplicitConfigKeys : [.../* @__PURE__ */ new Set([...observerConfig.observerExplicitConfigKeys ?? [], "observerMaxOutputTokens"])]
|
|
5440
5256
|
};
|
|
5441
5257
|
const observer = new ObserverClient(observerConfigWithOverrides);
|
|
5442
5258
|
const runs = [];
|
|
@@ -5778,9 +5594,9 @@ memoryCommand.addCommand(createMemoryRelinkReportCommand());
|
|
|
5778
5594
|
memoryCommand.addCommand(createMemoryRelinkPlanCommand());
|
|
5779
5595
|
//#endregion
|
|
5780
5596
|
//#region src/commands/pack.ts
|
|
5781
|
-
var MAX_INTERNAL_LEDGER_INPUT_BYTES =
|
|
5597
|
+
var MAX_INTERNAL_LEDGER_INPUT_BYTES = 16384;
|
|
5782
5598
|
var INTERNAL_LEDGER_INPUT_TIMEOUT_MS = 1e3;
|
|
5783
|
-
var FORBIDDEN_INTERNAL_KEYS = new Set([
|
|
5599
|
+
var FORBIDDEN_INTERNAL_KEYS = /* @__PURE__ */ new Set([
|
|
5784
5600
|
"body",
|
|
5785
5601
|
"context",
|
|
5786
5602
|
"pack",
|
|
@@ -5792,7 +5608,7 @@ var FORBIDDEN_INTERNAL_KEYS = new Set([
|
|
|
5792
5608
|
"raw_prompt",
|
|
5793
5609
|
"title"
|
|
5794
5610
|
]);
|
|
5795
|
-
var INTERNAL_LEDGER_KEYS = new Set([
|
|
5611
|
+
var INTERNAL_LEDGER_KEYS = /* @__PURE__ */ new Set([
|
|
5796
5612
|
"action",
|
|
5797
5613
|
"attempt_id",
|
|
5798
5614
|
"started_at",
|
|
@@ -5807,13 +5623,13 @@ var INTERNAL_LEDGER_KEYS = new Set([
|
|
|
5807
5623
|
"failure_stage",
|
|
5808
5624
|
"original_attempt_id"
|
|
5809
5625
|
]);
|
|
5810
|
-
var INTERNAL_LEDGER_ACTIONS = new Set([
|
|
5626
|
+
var INTERNAL_LEDGER_ACTIONS = /* @__PURE__ */ new Set([
|
|
5811
5627
|
"record",
|
|
5812
5628
|
"delivery",
|
|
5813
5629
|
"cache_reuse"
|
|
5814
5630
|
]);
|
|
5815
|
-
var INTERNAL_RETRIEVAL_STATUSES = new Set(["skipped", "failed"]);
|
|
5816
|
-
var INTERNAL_DELIVERY_STATUSES = new Set([
|
|
5631
|
+
var INTERNAL_RETRIEVAL_STATUSES = /* @__PURE__ */ new Set(["skipped", "failed"]);
|
|
5632
|
+
var INTERNAL_DELIVERY_STATUSES = /* @__PURE__ */ new Set([
|
|
5817
5633
|
"handed_off",
|
|
5818
5634
|
"failed",
|
|
5819
5635
|
"unknown"
|
|
@@ -6724,7 +6540,8 @@ async function startForegroundViewer(invocation) {
|
|
|
6724
6540
|
const sweeper = new RawEventSweeper(store, { observer });
|
|
6725
6541
|
sweeper.start();
|
|
6726
6542
|
const syncAbort = new AbortController();
|
|
6727
|
-
const
|
|
6543
|
+
const config = invocation.configPath ? readCodememConfigFileAtPath(invocation.configPath) : readCodememConfigFile();
|
|
6544
|
+
const syncConfig = readCoordinatorSyncConfig(config);
|
|
6728
6545
|
const syncEnabled = syncConfig.syncEnabled;
|
|
6729
6546
|
const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
|
|
6730
6547
|
if (!await terminateTrustedMaintenanceWorker(dbPath, {
|
|
@@ -7020,29 +6837,39 @@ function migrateLegacyOpencodePlugin() {
|
|
|
7020
6837
|
p.log.step("Removed legacy compat lib: ~/.config/opencode/lib/compat.js");
|
|
7021
6838
|
} catch {}
|
|
7022
6839
|
}
|
|
7023
|
-
|
|
6840
|
+
function isExactStringArray(value, expected) {
|
|
6841
|
+
return Array.isArray(value) && value.length === expected.length && value.every((entry, index) => entry === expected[index]);
|
|
6842
|
+
}
|
|
6843
|
+
/** Detect and upgrade legacy uvx/uv or managed single-package MCP entries in OpenCode config. */
|
|
7024
6844
|
function migrateLegacyOpencodeMcp(config) {
|
|
7025
6845
|
const mcpConfig = config.mcp;
|
|
7026
6846
|
if (!mcpConfig || typeof mcpConfig !== "object") return false;
|
|
7027
6847
|
const entry = mcpConfig.codemem;
|
|
7028
6848
|
if (!entry || typeof entry !== "object") return false;
|
|
7029
6849
|
const command = entry.command;
|
|
7030
|
-
|
|
7031
|
-
|
|
6850
|
+
const isLegacyUv = Array.isArray(command) && command.some((arg) => typeof arg === "string" && (arg === "uvx" || arg === "uv")) || typeof command === "string" && (command === "uvx" || command === "uv");
|
|
6851
|
+
const isManagedSinglePackageNpx = isExactStringArray(command, [
|
|
6852
|
+
"npx",
|
|
6853
|
+
"-y",
|
|
6854
|
+
"codemem",
|
|
6855
|
+
"mcp"
|
|
6856
|
+
]) || isExactStringArray(command, [
|
|
6857
|
+
"npx",
|
|
6858
|
+
"codemem",
|
|
6859
|
+
"mcp"
|
|
6860
|
+
]);
|
|
6861
|
+
if (isLegacyUv || isManagedSinglePackageNpx) {
|
|
6862
|
+
p.log.step("Upgrading managed MCP entry to the current npm launcher");
|
|
6863
|
+
const launcher = codememMcpLauncher();
|
|
7032
6864
|
mcpConfig.codemem = {
|
|
7033
|
-
|
|
7034
|
-
command: [
|
|
7035
|
-
"npx",
|
|
7036
|
-
"codemem",
|
|
7037
|
-
"mcp"
|
|
7038
|
-
],
|
|
7039
|
-
enabled: true
|
|
6865
|
+
...entry,
|
|
6866
|
+
command: [launcher.command, ...launcher.args]
|
|
7040
6867
|
};
|
|
7041
6868
|
return true;
|
|
7042
6869
|
}
|
|
7043
6870
|
return false;
|
|
7044
6871
|
}
|
|
7045
|
-
/** Detect and upgrade legacy uvx-
|
|
6872
|
+
/** Detect and upgrade legacy uvx or managed single-package MCP entries in Claude settings. */
|
|
7046
6873
|
function migrateLegacyClaudeMcp(settings) {
|
|
7047
6874
|
const mcpServers = settings.mcpServers;
|
|
7048
6875
|
if (!mcpServers || typeof mcpServers !== "object") return false;
|
|
@@ -7050,15 +6877,19 @@ function migrateLegacyClaudeMcp(settings) {
|
|
|
7050
6877
|
if (!entry || typeof entry !== "object") return false;
|
|
7051
6878
|
const command = entry.command;
|
|
7052
6879
|
const args = entry.args;
|
|
7053
|
-
|
|
7054
|
-
|
|
6880
|
+
const isLegacyUv = typeof command === "string" && (command === "uvx" || command === "uv") || Array.isArray(args) && args.some((arg) => typeof arg === "string" && (arg.startsWith("codemem==") || arg === "uvx"));
|
|
6881
|
+
const isManagedSinglePackageNpx = command === "npx" && isExactStringArray(args, [
|
|
6882
|
+
"-y",
|
|
6883
|
+
"codemem",
|
|
6884
|
+
"mcp"
|
|
6885
|
+
]);
|
|
6886
|
+
if (isLegacyUv || isManagedSinglePackageNpx) {
|
|
6887
|
+
p.log.step("Upgrading managed Claude MCP entry to the current npm launcher");
|
|
6888
|
+
const launcher = codememMcpLauncher();
|
|
7055
6889
|
mcpServers.codemem = {
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
"codemem",
|
|
7060
|
-
"mcp"
|
|
7061
|
-
]
|
|
6890
|
+
...entry,
|
|
6891
|
+
command: launcher.command,
|
|
6892
|
+
args: launcher.args
|
|
7062
6893
|
};
|
|
7063
6894
|
return true;
|
|
7064
6895
|
}
|
|
@@ -7113,13 +6944,10 @@ function installMcp(force) {
|
|
|
7113
6944
|
return true;
|
|
7114
6945
|
}
|
|
7115
6946
|
if (!migrated) {
|
|
6947
|
+
const launcher = codememMcpLauncher();
|
|
7116
6948
|
mcpConfig.codemem = {
|
|
7117
6949
|
type: "local",
|
|
7118
|
-
command: [
|
|
7119
|
-
"npx",
|
|
7120
|
-
"codemem",
|
|
7121
|
-
"mcp"
|
|
7122
|
-
],
|
|
6950
|
+
command: [launcher.command, ...launcher.args],
|
|
7123
6951
|
enabled: true
|
|
7124
6952
|
};
|
|
7125
6953
|
config.mcp = mcpConfig;
|
|
@@ -7136,7 +6964,8 @@ function installMcp(force) {
|
|
|
7136
6964
|
function isClaudeHooksPluginInstalled() {
|
|
7137
6965
|
const pluginDir = join(claudeConfigDir(), "plugins", "codemem");
|
|
7138
6966
|
if (existsSync(pluginDir)) return true;
|
|
7139
|
-
|
|
6967
|
+
const hooksJson = join(pluginDir, "hooks", "hooks.json");
|
|
6968
|
+
if (existsSync(hooksJson)) return true;
|
|
7140
6969
|
return false;
|
|
7141
6970
|
}
|
|
7142
6971
|
function installClaudeMcp(force) {
|
|
@@ -7153,13 +6982,10 @@ function installClaudeMcp(force) {
|
|
|
7153
6982
|
if ("codemem" in mcpServers && !force && !migrated) p.log.info(`Claude MCP entry already exists in ${settingsPath}`);
|
|
7154
6983
|
else {
|
|
7155
6984
|
if (!migrated) {
|
|
6985
|
+
const launcher = codememMcpLauncher();
|
|
7156
6986
|
mcpServers.codemem = {
|
|
7157
|
-
command:
|
|
7158
|
-
args:
|
|
7159
|
-
"-y",
|
|
7160
|
-
"codemem",
|
|
7161
|
-
"mcp"
|
|
7162
|
-
]
|
|
6987
|
+
command: launcher.command,
|
|
6988
|
+
args: launcher.args
|
|
7163
6989
|
};
|
|
7164
6990
|
settings.mcpServers = mcpServers;
|
|
7165
6991
|
}
|
|
@@ -7182,25 +7008,47 @@ function installClaudeMcp(force) {
|
|
|
7182
7008
|
return true;
|
|
7183
7009
|
}
|
|
7184
7010
|
/** The MCP server table appended to Codex config.toml. */
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
]
|
|
7011
|
+
function codexMcpBlock() {
|
|
7012
|
+
const launcher = codememMcpLauncher();
|
|
7013
|
+
const args = launcher.args.map((arg) => JSON.stringify(arg)).join(", ");
|
|
7014
|
+
return [
|
|
7015
|
+
"[mcp_servers.codemem]",
|
|
7016
|
+
`command = ${JSON.stringify(launcher.command)}`,
|
|
7017
|
+
`args = [${args}]`,
|
|
7018
|
+
"startup_timeout_sec = 30",
|
|
7019
|
+
"tool_timeout_sec = 60"
|
|
7020
|
+
].join("\n");
|
|
7021
|
+
}
|
|
7192
7022
|
var CODEX_MCP_TABLE_RE = /^[ \t]*\[[ \t]*mcp_servers[ \t]*\.[ \t]*("?)codemem\1[ \t]*\]/m;
|
|
7023
|
+
function migrateManagedCodexMcp(existing) {
|
|
7024
|
+
const tableMatch = CODEX_MCP_TABLE_RE.exec(existing);
|
|
7025
|
+
if (!tableMatch || tableMatch.index == null) return null;
|
|
7026
|
+
const blockStart = tableMatch.index;
|
|
7027
|
+
const nextTable = /^[ \t]*\[/gm;
|
|
7028
|
+
nextTable.lastIndex = blockStart + tableMatch[0].length;
|
|
7029
|
+
const blockEnd = nextTable.exec(existing)?.index ?? existing.length;
|
|
7030
|
+
const block = existing.slice(blockStart, blockEnd);
|
|
7031
|
+
const commandPattern = /^([ \t]*command[ \t]*=[ \t]*)"npx"([ \t]*(?:#.*)?)$/m;
|
|
7032
|
+
const argsPattern = /^([ \t]*args[ \t]*=[ \t]*)\[[ \t]*"-y"[ \t]*,[ \t]*"codemem"[ \t]*,[ \t]*"mcp"[ \t]*\]([ \t]*(?:#.*)?)$/m;
|
|
7033
|
+
if (!commandPattern.test(block) || !argsPattern.test(block)) return null;
|
|
7034
|
+
const launcher = codememMcpLauncher();
|
|
7035
|
+
const args = launcher.args.map((arg) => JSON.stringify(arg)).join(", ");
|
|
7036
|
+
const migrated = block.replace(commandPattern, `$1${JSON.stringify(launcher.command)}$2`).replace(argsPattern, `$1[${args}]$2`);
|
|
7037
|
+
return `${existing.slice(0, blockStart)}${migrated}${existing.slice(blockEnd)}`;
|
|
7038
|
+
}
|
|
7193
7039
|
/** Marker substring identifying codemem-owned hook commands. */
|
|
7194
7040
|
var CODEMEM_HOOK_MARKER = "codemem codex-hook-";
|
|
7195
7041
|
/**
|
|
7196
7042
|
* Resolve how Codex hooks should invoke codemem. Prefer a direct `codemem` call
|
|
7197
|
-
* when it's on PATH (fast — no per-hook resolution); fall back to `npx
|
|
7198
|
-
*
|
|
7199
|
-
*
|
|
7200
|
-
*
|
|
7043
|
+
* when it's on PATH (fast — no per-hook resolution); fall back to an `npx`
|
|
7044
|
+
* invocation that requests both `codemem` and `@codemem/embeddings` when codemem
|
|
7045
|
+
* isn't installed (e.g. setup was run via `npx codemem setup`). The paired
|
|
7046
|
+
* runtime keeps the local-store inject path (codex-hook-inject) able to embed
|
|
7047
|
+
* instead of degrading to FTS. Mirrors the MCP launcher's two-package model.
|
|
7201
7048
|
*/
|
|
7202
7049
|
function codememCodexHookBase() {
|
|
7203
|
-
|
|
7050
|
+
if (codememOnPath(true)) return "codemem";
|
|
7051
|
+
return "npx -y --package codemem --package @codemem/embeddings codemem";
|
|
7204
7052
|
}
|
|
7205
7053
|
/**
|
|
7206
7054
|
* Build the codemem-owned hook groups keyed by Codex event name, given the
|
|
@@ -7242,28 +7090,127 @@ function isCodememHookGroup(group) {
|
|
|
7242
7090
|
return hooks.some((h) => h != null && typeof h === "object" && typeof h.command === "string" && h.command.includes(CODEMEM_HOOK_MARKER));
|
|
7243
7091
|
}
|
|
7244
7092
|
/**
|
|
7245
|
-
* True if a resolved bin path is a transient
|
|
7246
|
-
*
|
|
7247
|
-
*
|
|
7248
|
-
*
|
|
7093
|
+
* True if a resolved bin path is a transient/project-local bin that will not be
|
|
7094
|
+
* on PATH for the globally-configured MCP hosts (OpenCode/Claude/Codex).
|
|
7095
|
+
*
|
|
7096
|
+
* - `_npx` / `.pnpm/dlx`: npx/dlx caches exposed only for the duration of a
|
|
7097
|
+
* `npx -y codemem setup` run, then removed.
|
|
7098
|
+
* - `node_modules/.bin`: a project-local install exposed only while setup runs
|
|
7099
|
+
* through `npm exec` / `pnpm exec` / a package script. The global hosts do not
|
|
7100
|
+
* inherit that project-local PATH, so baking a bare `codemem` command would
|
|
7101
|
+
* later fail to resolve.
|
|
7102
|
+
*
|
|
7103
|
+
* Such paths must NOT count as "on PATH" for launcher/hook command baking.
|
|
7249
7104
|
*/
|
|
7250
7105
|
function isTransientNpxBinPath(resolved) {
|
|
7251
|
-
return /[/\\]_npx[/\\]/.test(resolved) || /[/\\]\.pnpm[/\\]dlx[/\\]/.test(resolved);
|
|
7106
|
+
return /[/\\]_npx[/\\]/.test(resolved) || /[/\\]\.pnpm[/\\]dlx[/\\]/.test(resolved) || /[/\\]node_modules[/\\]\.bin[/\\]/.test(resolved);
|
|
7107
|
+
}
|
|
7108
|
+
/** Parse a semver core (major.minor.patch) plus optional prerelease tail. */
|
|
7109
|
+
function parseSetupSemver(version) {
|
|
7110
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-.+)?$/.exec(version.trim());
|
|
7111
|
+
if (!match) return null;
|
|
7112
|
+
return {
|
|
7113
|
+
core: [
|
|
7114
|
+
Number(match[1]),
|
|
7115
|
+
Number(match[2]),
|
|
7116
|
+
Number(match[3])
|
|
7117
|
+
],
|
|
7118
|
+
prerelease: version.includes("-")
|
|
7119
|
+
};
|
|
7120
|
+
}
|
|
7121
|
+
/**
|
|
7122
|
+
* True when a durable `codemem --version` output should be trusted to launch
|
|
7123
|
+
* `codemem mcp` directly. Mirrors the OpenCode plugin's runner check: accept the
|
|
7124
|
+
* exact build version, or a clean (non-prerelease) release at least as new as
|
|
7125
|
+
* this build. This prevents baking a bare launcher that later resolves to an
|
|
7126
|
+
* older global install sitting alongside the transient npx bin.
|
|
7127
|
+
*/
|
|
7128
|
+
function isDurableCodememVersionTrusted(versionOutput) {
|
|
7129
|
+
if (versionOutput === VERSION) return true;
|
|
7130
|
+
const candidate = parseSetupSemver(versionOutput);
|
|
7131
|
+
const current = parseSetupSemver(VERSION);
|
|
7132
|
+
if (!candidate || !current || candidate.prerelease) return false;
|
|
7133
|
+
for (let i = 0; i < 3; i++) {
|
|
7134
|
+
const c = candidate.core[i] ?? 0;
|
|
7135
|
+
const v = current.core[i] ?? 0;
|
|
7136
|
+
if (c > v) return true;
|
|
7137
|
+
if (c < v) return false;
|
|
7138
|
+
}
|
|
7139
|
+
return true;
|
|
7252
7140
|
}
|
|
7253
7141
|
/**
|
|
7254
7142
|
* Detect whether a durable `codemem` resolves on PATH (excluding a transient
|
|
7255
|
-
* npx/dlx bin that vanishes after this process exits).
|
|
7143
|
+
* npx/dlx bin that vanishes after this process exits). When `requireTrustedVersion`
|
|
7144
|
+
* is set, the durable binary must also report a version compatible with this
|
|
7145
|
+
* build before it is trusted, so setup never bakes a bare launcher that resolves
|
|
7146
|
+
* to a stale global install.
|
|
7256
7147
|
*/
|
|
7257
|
-
function codememOnPath() {
|
|
7258
|
-
try {
|
|
7259
|
-
const
|
|
7260
|
-
|
|
7261
|
-
|
|
7148
|
+
function codememOnPath(requireTrustedVersion = false) {
|
|
7149
|
+
try {
|
|
7150
|
+
const command = process.platform === "win32" ? "where" : "which";
|
|
7151
|
+
const args = process.platform === "win32" ? ["codemem"] : ["-a", "codemem"];
|
|
7152
|
+
const durable = execFileSync(command, args, { encoding: "utf-8" }).split(/\r?\n/).map((line) => line.trim()).filter(Boolean).filter((candidate) => !isTransientNpxBinPath(candidate));
|
|
7153
|
+
if (durable.length === 0) return false;
|
|
7154
|
+
if (!requireTrustedVersion) return true;
|
|
7155
|
+
const [firstDurable] = durable;
|
|
7156
|
+
if (firstDurable == null) return false;
|
|
7157
|
+
const version = probeDurableCodememVersion(firstDurable);
|
|
7158
|
+
return version != null && isDurableCodememVersionTrusted(version);
|
|
7262
7159
|
} catch {
|
|
7263
7160
|
return false;
|
|
7264
7161
|
}
|
|
7265
7162
|
}
|
|
7266
7163
|
/**
|
|
7164
|
+
* Run `<binary> --version`, returning trimmed stdout or null on failure. On
|
|
7165
|
+
* Windows a global npm install resolves to a `.cmd`/`.bat` shim that Node's
|
|
7166
|
+
* shell-less execFileSync cannot execute directly, so invoke it through
|
|
7167
|
+
* `cmd.exe /c` there; POSIX binaries run directly.
|
|
7168
|
+
*/
|
|
7169
|
+
function probeDurableCodememVersion(binaryPath) {
|
|
7170
|
+
try {
|
|
7171
|
+
return (process.platform === "win32" ? execFileSync("cmd.exe", [
|
|
7172
|
+
"/c",
|
|
7173
|
+
binaryPath,
|
|
7174
|
+
"--version"
|
|
7175
|
+
], {
|
|
7176
|
+
encoding: "utf-8",
|
|
7177
|
+
timeout: 3e3,
|
|
7178
|
+
stdio: [
|
|
7179
|
+
"ignore",
|
|
7180
|
+
"pipe",
|
|
7181
|
+
"ignore"
|
|
7182
|
+
]
|
|
7183
|
+
}) : execFileSync(binaryPath, ["--version"], {
|
|
7184
|
+
encoding: "utf-8",
|
|
7185
|
+
timeout: 3e3,
|
|
7186
|
+
stdio: [
|
|
7187
|
+
"ignore",
|
|
7188
|
+
"pipe",
|
|
7189
|
+
"ignore"
|
|
7190
|
+
]
|
|
7191
|
+
})).trim();
|
|
7192
|
+
} catch {
|
|
7193
|
+
return null;
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
7196
|
+
function codememMcpLauncher(onPath = codememOnPath(true)) {
|
|
7197
|
+
return onPath ? {
|
|
7198
|
+
command: "codemem",
|
|
7199
|
+
args: ["mcp"]
|
|
7200
|
+
} : {
|
|
7201
|
+
command: "npx",
|
|
7202
|
+
args: [
|
|
7203
|
+
"-y",
|
|
7204
|
+
"--package",
|
|
7205
|
+
"codemem",
|
|
7206
|
+
"--package",
|
|
7207
|
+
"@codemem/embeddings",
|
|
7208
|
+
"codemem",
|
|
7209
|
+
"mcp"
|
|
7210
|
+
]
|
|
7211
|
+
};
|
|
7212
|
+
}
|
|
7213
|
+
/**
|
|
7267
7214
|
* Append the codemem MCP server table to Codex config.toml without rewriting
|
|
7268
7215
|
* unrelated content. Returns true on success.
|
|
7269
7216
|
*/
|
|
@@ -7271,6 +7218,20 @@ function installCodexMcp(codexHome, force) {
|
|
|
7271
7218
|
const configPath = join(codexHome, "config.toml");
|
|
7272
7219
|
const existing = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
|
|
7273
7220
|
if (CODEX_MCP_TABLE_RE.test(existing)) {
|
|
7221
|
+
const migrated = migrateManagedCodexMcp(existing);
|
|
7222
|
+
if (migrated != null) {
|
|
7223
|
+
try {
|
|
7224
|
+
copyFileSync(configPath, `${configPath}.codemem.bak`);
|
|
7225
|
+
} catch {}
|
|
7226
|
+
try {
|
|
7227
|
+
writeFileSync(configPath, migrated, "utf-8");
|
|
7228
|
+
p.log.success(`Codex MCP entry upgraded: ${configPath}`);
|
|
7229
|
+
return true;
|
|
7230
|
+
} catch (err) {
|
|
7231
|
+
p.log.error(`Failed to write ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7232
|
+
return false;
|
|
7233
|
+
}
|
|
7234
|
+
}
|
|
7274
7235
|
if (force) p.log.info(`Codex MCP entry already exists in ${configPath} — left as-is (TOML is not rewritten in place)`);
|
|
7275
7236
|
else p.log.info(`Codex MCP entry already exists in ${configPath}`);
|
|
7276
7237
|
return true;
|
|
@@ -7280,7 +7241,7 @@ function installCodexMcp(codexHome, force) {
|
|
|
7280
7241
|
} catch {}
|
|
7281
7242
|
let next = existing;
|
|
7282
7243
|
if (next.length > 0 && !next.endsWith("\n\n")) next += next.endsWith("\n") ? "\n" : "\n\n";
|
|
7283
|
-
next += `${
|
|
7244
|
+
next += `${codexMcpBlock()}\n`;
|
|
7284
7245
|
try {
|
|
7285
7246
|
writeFileSync(configPath, next, "utf-8");
|
|
7286
7247
|
p.log.success(`Codex MCP entry installed: ${configPath}`);
|
|
@@ -7307,12 +7268,38 @@ function installCodexHooks(codexHome, force) {
|
|
|
7307
7268
|
let hooks = config.hooks;
|
|
7308
7269
|
if (hooks == null || typeof hooks !== "object" || Array.isArray(hooks)) hooks = {};
|
|
7309
7270
|
const ours = buildCodememCodexHookGroups(codememCodexHookBase());
|
|
7271
|
+
const currentCommands = new Set(Object.values(ours).flatMap((groups) => groups.flatMap((group) => group.hooks.map((hook) => hook.command))));
|
|
7272
|
+
const groupNeedsMigration = (group) => {
|
|
7273
|
+
if (!isCodememHookGroup(group)) return false;
|
|
7274
|
+
const hooksList = group.hooks;
|
|
7275
|
+
if (!Array.isArray(hooksList)) return false;
|
|
7276
|
+
return hooksList.some((hook) => {
|
|
7277
|
+
if (hook == null || typeof hook !== "object") return false;
|
|
7278
|
+
const command = hook.command;
|
|
7279
|
+
if (typeof command !== "string") return false;
|
|
7280
|
+
return command.includes(CODEMEM_HOOK_MARKER) && !currentCommands.has(command);
|
|
7281
|
+
});
|
|
7282
|
+
};
|
|
7310
7283
|
let changed = false;
|
|
7284
|
+
const isCodememOwnedHook = (hook) => hook != null && typeof hook === "object" && typeof hook.command === "string" && hook.command.includes(CODEMEM_HOOK_MARKER);
|
|
7285
|
+
const stripCodememHooks = (group) => {
|
|
7286
|
+
if (!isCodememHookGroup(group)) return group;
|
|
7287
|
+
const hooksList = group.hooks;
|
|
7288
|
+
if (!Array.isArray(hooksList)) return null;
|
|
7289
|
+
const preservedHooks = hooksList.filter((hook) => !isCodememOwnedHook(hook));
|
|
7290
|
+
if (preservedHooks.length === 0) return null;
|
|
7291
|
+
return {
|
|
7292
|
+
...group,
|
|
7293
|
+
hooks: preservedHooks
|
|
7294
|
+
};
|
|
7295
|
+
};
|
|
7311
7296
|
for (const [event, ourGroups] of Object.entries(ours)) {
|
|
7312
7297
|
const current = hooks[event];
|
|
7313
7298
|
const existingGroups = Array.isArray(current) ? [...current] : [];
|
|
7314
|
-
|
|
7315
|
-
const
|
|
7299
|
+
const hasCodemem = existingGroups.some(isCodememHookGroup);
|
|
7300
|
+
const needsMigration = existingGroups.some(groupNeedsMigration);
|
|
7301
|
+
if (hasCodemem && !force && !needsMigration) continue;
|
|
7302
|
+
const preserved = existingGroups.map((group) => stripCodememHooks(group)).filter((group) => group != null);
|
|
7316
7303
|
hooks[event] = [...preserved, ...ourGroups];
|
|
7317
7304
|
changed = true;
|
|
7318
7305
|
}
|
|
@@ -7348,8 +7335,11 @@ function installCodex(force) {
|
|
|
7348
7335
|
p.log.error(`Failed to create Codex home ${codexHome}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7349
7336
|
return false;
|
|
7350
7337
|
}
|
|
7351
|
-
if (codememOnPath()) p.log.info("Codex hooks will call `codemem` directly (found on PATH).");
|
|
7352
|
-
else
|
|
7338
|
+
if (codememOnPath(true)) p.log.info("Codex hooks will call `codemem` directly (found on PATH).");
|
|
7339
|
+
else {
|
|
7340
|
+
const globalInstallCommand = process.platform === "linux" ? "env ONNXRUNTIME_NODE_INSTALL=skip npm install -g codemem @codemem/embeddings" : "npm install -g codemem @codemem/embeddings";
|
|
7341
|
+
p.log.info(`\`codemem\` is not on PATH, so Codex hooks will run via \`npx -y --package codemem --package @codemem/embeddings codemem\` (works without a global install). For lower hook latency: ${globalInstallCommand}`);
|
|
7342
|
+
}
|
|
7353
7343
|
let ok = true;
|
|
7354
7344
|
ok = installCodexMcp(codexHome, force) && ok;
|
|
7355
7345
|
ok = installCodexHooks(codexHome, force) && ok;
|
|
@@ -7479,7 +7469,7 @@ var statsCommand = statsCmd.action((opts) => {
|
|
|
7479
7469
|
//#region src/commands/status.ts
|
|
7480
7470
|
var MAX_ATTENTION = 20;
|
|
7481
7471
|
var DEFAULT_VIEWER_PORT = 38888;
|
|
7482
|
-
var RECENT_FAILURE_WINDOW_MS =
|
|
7472
|
+
var RECENT_FAILURE_WINDOW_MS = 864e5;
|
|
7483
7473
|
var OBSERVER_IDENTITY_KEYS = [
|
|
7484
7474
|
"observer_provider",
|
|
7485
7475
|
"observer_model",
|
|
@@ -8626,7 +8616,8 @@ bootstrapCmd.action(async (peerArg, opts) => {
|
|
|
8626
8616
|
process.exitCode = 2;
|
|
8627
8617
|
return;
|
|
8628
8618
|
}
|
|
8629
|
-
const
|
|
8619
|
+
const dbPath = resolveDbPath(resolveDbOpt(opts));
|
|
8620
|
+
const store = new MemoryStore(dbPath);
|
|
8630
8621
|
try {
|
|
8631
8622
|
const pageSize = Math.max(1, Number.parseInt(opts.pageSize, 10) || 2e3);
|
|
8632
8623
|
const keysDir = opts.keysDir ?? void 0;
|
|
@@ -8797,7 +8788,7 @@ syncCoordinatorAlias.hook("preAction", (_thisCmd, actionCmd) => {
|
|
|
8797
8788
|
syncCommand.addCommand(syncCoordinatorAlias, { hidden: true });
|
|
8798
8789
|
//#endregion
|
|
8799
8790
|
//#region src/commands/update.ts
|
|
8800
|
-
var INSTALL_TIMEOUT_MS =
|
|
8791
|
+
var INSTALL_TIMEOUT_MS = 42e4;
|
|
8801
8792
|
var VERIFY_TIMEOUT_MS = 3e4;
|
|
8802
8793
|
var PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
8803
8794
|
var UPDATE_LOCK_FILE = "update-install.lock";
|
|
@@ -8819,7 +8810,7 @@ async function removeStaleInstallLock(lockPath) {
|
|
|
8819
8810
|
const pid = Number.parseInt(rawPid.trim(), 10);
|
|
8820
8811
|
if (Number.isSafeInteger(pid) && pid > 0) {
|
|
8821
8812
|
if (isProcessAlive(pid)) return false;
|
|
8822
|
-
} else if (Date.now() - lockStat.mtimeMs <=
|
|
8813
|
+
} else if (Date.now() - lockStat.mtimeMs <= 45e4) return false;
|
|
8823
8814
|
await unlink(lockPath);
|
|
8824
8815
|
return true;
|
|
8825
8816
|
} catch {
|
|
@@ -8850,6 +8841,7 @@ function runCommand(command, args, timeoutMs, options = {}) {
|
|
|
8850
8841
|
const child = spawn(command, args, {
|
|
8851
8842
|
cwd: options.cwd,
|
|
8852
8843
|
detached: process.platform !== "win32",
|
|
8844
|
+
env: options.env,
|
|
8853
8845
|
shell: false,
|
|
8854
8846
|
stdio: [
|
|
8855
8847
|
"ignore",
|
|
@@ -8983,12 +8975,13 @@ function renderHumanStatus(status) {
|
|
|
8983
8975
|
}
|
|
8984
8976
|
var checkCommand = addJsonOption(new Command("check").description("Check for a newer stable codemem release")).addOption(new Option("-r, --refresh", "bypass the six-hour release cache")).configureHelp(helpStyle).action(async (options) => {
|
|
8985
8977
|
try {
|
|
8978
|
+
const installKind = detectInstallKind({
|
|
8979
|
+
entryPath: process.argv[1] ?? "",
|
|
8980
|
+
env: process.env
|
|
8981
|
+
});
|
|
8986
8982
|
const status = await getUpdateStatus({
|
|
8987
8983
|
currentVersion: VERSION,
|
|
8988
|
-
installKind
|
|
8989
|
-
entryPath: process.argv[1] ?? "",
|
|
8990
|
-
env: process.env
|
|
8991
|
-
}),
|
|
8984
|
+
installKind,
|
|
8992
8985
|
refresh: options.refresh
|
|
8993
8986
|
});
|
|
8994
8987
|
if (status.latest_version === null) {
|
|
@@ -9014,15 +9007,29 @@ var checkCommand = addJsonOption(new Command("check").description("Check for a n
|
|
|
9014
9007
|
}
|
|
9015
9008
|
}
|
|
9016
9009
|
});
|
|
9017
|
-
|
|
9010
|
+
function updateInstallArgs(npmArgs, targetVersion) {
|
|
9011
|
+
const installArgs = [
|
|
9012
|
+
"install",
|
|
9013
|
+
"-g",
|
|
9014
|
+
"--registry",
|
|
9015
|
+
PUBLIC_NPM_REGISTRY,
|
|
9016
|
+
`--@codemem:registry=${PUBLIC_NPM_REGISTRY}`,
|
|
9017
|
+
`codemem@${targetVersion}`,
|
|
9018
|
+
`@codemem/embeddings@${targetVersion}`
|
|
9019
|
+
];
|
|
9020
|
+
if (process.platform !== "win32") return installArgs;
|
|
9021
|
+
return [...npmArgs.slice(0, 3), windowsCommandLine(npmArgs[3] ?? "", installArgs)];
|
|
9022
|
+
}
|
|
9023
|
+
async function installUpdate(options) {
|
|
9018
9024
|
let releaseInstallLock = null;
|
|
9019
9025
|
try {
|
|
9026
|
+
const installKind = detectInstallKind({
|
|
9027
|
+
entryPath: process.argv[1] ?? "",
|
|
9028
|
+
env: process.env
|
|
9029
|
+
});
|
|
9020
9030
|
const status = await getUpdateStatus({
|
|
9021
9031
|
currentVersion: VERSION,
|
|
9022
|
-
installKind
|
|
9023
|
-
entryPath: process.argv[1] ?? "",
|
|
9024
|
-
env: process.env
|
|
9025
|
-
}),
|
|
9032
|
+
installKind,
|
|
9026
9033
|
refresh: true
|
|
9027
9034
|
});
|
|
9028
9035
|
if (!status.auto_update_eligible || !status.latest_version) {
|
|
@@ -9036,20 +9043,12 @@ var installCommand = addJsonOption(new Command("install").description("Install a
|
|
|
9036
9043
|
}
|
|
9037
9044
|
releaseInstallLock = await acquireInstallLock();
|
|
9038
9045
|
const npm = await resolveInstallCommand();
|
|
9039
|
-
const installation = await runCommand(npm.command,
|
|
9040
|
-
"install",
|
|
9041
|
-
"-g",
|
|
9042
|
-
"--registry",
|
|
9043
|
-
PUBLIC_NPM_REGISTRY,
|
|
9044
|
-
`codemem@${targetVersion}`
|
|
9045
|
-
])] : [
|
|
9046
|
-
"install",
|
|
9047
|
-
"-g",
|
|
9048
|
-
"--registry",
|
|
9049
|
-
PUBLIC_NPM_REGISTRY,
|
|
9050
|
-
`codemem@${targetVersion}`
|
|
9051
|
-
], INSTALL_TIMEOUT_MS, {
|
|
9046
|
+
const installation = await runCommand(npm.command, updateInstallArgs(npm.args, targetVersion), INSTALL_TIMEOUT_MS, {
|
|
9052
9047
|
cwd: npm.cwd,
|
|
9048
|
+
env: process.platform === "linux" ? {
|
|
9049
|
+
...process.env,
|
|
9050
|
+
ONNXRUNTIME_NODE_INSTALL: "skip"
|
|
9051
|
+
} : void 0,
|
|
9053
9052
|
windowsVerbatimArguments: process.platform === "win32"
|
|
9054
9053
|
});
|
|
9055
9054
|
if (installation.exitCode !== 0) {
|
|
@@ -9076,8 +9075,9 @@ var installCommand = addJsonOption(new Command("install").description("Install a
|
|
|
9076
9075
|
} finally {
|
|
9077
9076
|
await releaseInstallLock?.();
|
|
9078
9077
|
}
|
|
9079
|
-
}
|
|
9080
|
-
var
|
|
9078
|
+
}
|
|
9079
|
+
var installCommand = addJsonOption(new Command("install").description("Install an eligible stable codemem update")).configureHelp(helpStyle).action(installUpdate);
|
|
9080
|
+
var updateCommand = new Command("update").description("Inspect and manage codemem updates").enablePositionalOptions().configureHelp(helpStyle).addCommand(checkCommand).addCommand(installCommand);
|
|
9081
9081
|
//#endregion
|
|
9082
9082
|
//#region src/commands/version.ts
|
|
9083
9083
|
var versionCommand = new Command("version").configureHelp(helpStyle).description("Print codemem version").action(() => {
|