caveat-cli 0.15.0 → 0.16.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/dist/{chunk-Z2XLYCR3.js → chunk-HCLEIAGO.js} +2531 -1170
- package/dist/chunk-HCLEIAGO.js.map +1 -0
- package/dist/index.js +1385 -646
- package/dist/index.js.map +1 -1
- package/dist/{server-DCE3ODHI.js → server-EASKBCNK.js} +2 -2
- package/package.json +5 -4
- package/dist/chunk-Z2XLYCR3.js.map +0 -1
- /package/dist/{server-DCE3ODHI.js.map → server-EASKBCNK.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
import { createRequire as __caveat_createRequire } from 'node:module';
|
|
3
3
|
const require = __caveat_createRequire(import.meta.url);
|
|
4
4
|
import {
|
|
5
|
+
AUTO_SYNC_DEBOUNCE_MS,
|
|
6
|
+
CAVEAT_AUTO_SYNC_ENV,
|
|
5
7
|
KNOWLEDGE_GITIGNORE,
|
|
8
|
+
PublishScanError,
|
|
9
|
+
SyncError,
|
|
6
10
|
__commonJS,
|
|
7
11
|
__export,
|
|
8
12
|
__toESM,
|
|
@@ -11,6 +15,7 @@ import {
|
|
|
11
15
|
buildCodexSidecarDiagnosticsCommand,
|
|
12
16
|
buildCodexSidecarReadOnlySmokeCommand,
|
|
13
17
|
buildCodexSidecarRunCommand,
|
|
18
|
+
buildHookSignalSidecarContextBlock,
|
|
14
19
|
caveatEntriesToSidecarContextBlocks,
|
|
15
20
|
cleanupStalePendingDirs,
|
|
16
21
|
communityAdd,
|
|
@@ -18,8 +23,10 @@ import {
|
|
|
18
23
|
communityPull,
|
|
19
24
|
communityRemove,
|
|
20
25
|
computeEntriesDigest,
|
|
26
|
+
createKeyserverKeyProvider,
|
|
21
27
|
decideCodexSidecarExecution,
|
|
22
28
|
defaultSelfIdentityTokens,
|
|
29
|
+
drainGlobalPendingReminders,
|
|
23
30
|
drainPendingReminders,
|
|
24
31
|
ensureUserConfig,
|
|
25
32
|
findCaveatHome,
|
|
@@ -30,9 +37,11 @@ import {
|
|
|
30
37
|
listRecent,
|
|
31
38
|
listStale,
|
|
32
39
|
loadConfig,
|
|
40
|
+
logHookQueryMiss,
|
|
33
41
|
markHit,
|
|
34
42
|
maybeSweepPendingDirs,
|
|
35
43
|
openDb,
|
|
44
|
+
prewarmSealedKeys,
|
|
36
45
|
publishOwn,
|
|
37
46
|
readCodexSessionSignals,
|
|
38
47
|
readDigestMarker,
|
|
@@ -41,8 +50,9 @@ import {
|
|
|
41
50
|
recordEntry,
|
|
42
51
|
reindexAllSources,
|
|
43
52
|
releaseReindexLock,
|
|
53
|
+
resetAutoSyncFailureState,
|
|
44
54
|
resolvePaths,
|
|
45
|
-
|
|
55
|
+
runAutoSync,
|
|
46
56
|
search,
|
|
47
57
|
stderrLogger,
|
|
48
58
|
stopReminderText,
|
|
@@ -53,7 +63,7 @@ import {
|
|
|
53
63
|
userPromptSubmitReminderText,
|
|
54
64
|
writeDigestMarker,
|
|
55
65
|
writeUserConfigPatch
|
|
56
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-HCLEIAGO.js";
|
|
57
67
|
|
|
58
68
|
// ../../node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/compile/codegen/code.js
|
|
59
69
|
var require_code = __commonJS({
|
|
@@ -432,11 +442,11 @@ var require_codegen = __commonJS({
|
|
|
432
442
|
const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
|
|
433
443
|
return `${varKind} ${this.name}${rhs};` + _n;
|
|
434
444
|
}
|
|
435
|
-
optimizeNames(names,
|
|
445
|
+
optimizeNames(names, constants2) {
|
|
436
446
|
if (!names[this.name.str])
|
|
437
447
|
return;
|
|
438
448
|
if (this.rhs)
|
|
439
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
449
|
+
this.rhs = optimizeExpr(this.rhs, names, constants2);
|
|
440
450
|
return this;
|
|
441
451
|
}
|
|
442
452
|
get names() {
|
|
@@ -453,10 +463,10 @@ var require_codegen = __commonJS({
|
|
|
453
463
|
render({ _n }) {
|
|
454
464
|
return `${this.lhs} = ${this.rhs};` + _n;
|
|
455
465
|
}
|
|
456
|
-
optimizeNames(names,
|
|
466
|
+
optimizeNames(names, constants2) {
|
|
457
467
|
if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
|
|
458
468
|
return;
|
|
459
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
469
|
+
this.rhs = optimizeExpr(this.rhs, names, constants2);
|
|
460
470
|
return this;
|
|
461
471
|
}
|
|
462
472
|
get names() {
|
|
@@ -517,8 +527,8 @@ var require_codegen = __commonJS({
|
|
|
517
527
|
optimizeNodes() {
|
|
518
528
|
return `${this.code}` ? this : void 0;
|
|
519
529
|
}
|
|
520
|
-
optimizeNames(names,
|
|
521
|
-
this.code = optimizeExpr(this.code, names,
|
|
530
|
+
optimizeNames(names, constants2) {
|
|
531
|
+
this.code = optimizeExpr(this.code, names, constants2);
|
|
522
532
|
return this;
|
|
523
533
|
}
|
|
524
534
|
get names() {
|
|
@@ -547,12 +557,12 @@ var require_codegen = __commonJS({
|
|
|
547
557
|
}
|
|
548
558
|
return nodes.length > 0 ? this : void 0;
|
|
549
559
|
}
|
|
550
|
-
optimizeNames(names,
|
|
560
|
+
optimizeNames(names, constants2) {
|
|
551
561
|
const { nodes } = this;
|
|
552
562
|
let i = nodes.length;
|
|
553
563
|
while (i--) {
|
|
554
564
|
const n = nodes[i];
|
|
555
|
-
if (n.optimizeNames(names,
|
|
565
|
+
if (n.optimizeNames(names, constants2))
|
|
556
566
|
continue;
|
|
557
567
|
subtractNames(names, n.names);
|
|
558
568
|
nodes.splice(i, 1);
|
|
@@ -605,12 +615,12 @@ var require_codegen = __commonJS({
|
|
|
605
615
|
return void 0;
|
|
606
616
|
return this;
|
|
607
617
|
}
|
|
608
|
-
optimizeNames(names,
|
|
618
|
+
optimizeNames(names, constants2) {
|
|
609
619
|
var _a3;
|
|
610
|
-
this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names,
|
|
611
|
-
if (!(super.optimizeNames(names,
|
|
620
|
+
this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants2);
|
|
621
|
+
if (!(super.optimizeNames(names, constants2) || this.else))
|
|
612
622
|
return;
|
|
613
|
-
this.condition = optimizeExpr(this.condition, names,
|
|
623
|
+
this.condition = optimizeExpr(this.condition, names, constants2);
|
|
614
624
|
return this;
|
|
615
625
|
}
|
|
616
626
|
get names() {
|
|
@@ -633,10 +643,10 @@ var require_codegen = __commonJS({
|
|
|
633
643
|
render(opts) {
|
|
634
644
|
return `for(${this.iteration})` + super.render(opts);
|
|
635
645
|
}
|
|
636
|
-
optimizeNames(names,
|
|
637
|
-
if (!super.optimizeNames(names,
|
|
646
|
+
optimizeNames(names, constants2) {
|
|
647
|
+
if (!super.optimizeNames(names, constants2))
|
|
638
648
|
return;
|
|
639
|
-
this.iteration = optimizeExpr(this.iteration, names,
|
|
649
|
+
this.iteration = optimizeExpr(this.iteration, names, constants2);
|
|
640
650
|
return this;
|
|
641
651
|
}
|
|
642
652
|
get names() {
|
|
@@ -672,10 +682,10 @@ var require_codegen = __commonJS({
|
|
|
672
682
|
render(opts) {
|
|
673
683
|
return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
|
|
674
684
|
}
|
|
675
|
-
optimizeNames(names,
|
|
676
|
-
if (!super.optimizeNames(names,
|
|
685
|
+
optimizeNames(names, constants2) {
|
|
686
|
+
if (!super.optimizeNames(names, constants2))
|
|
677
687
|
return;
|
|
678
|
-
this.iterable = optimizeExpr(this.iterable, names,
|
|
688
|
+
this.iterable = optimizeExpr(this.iterable, names, constants2);
|
|
679
689
|
return this;
|
|
680
690
|
}
|
|
681
691
|
get names() {
|
|
@@ -717,11 +727,11 @@ var require_codegen = __commonJS({
|
|
|
717
727
|
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
|
|
718
728
|
return this;
|
|
719
729
|
}
|
|
720
|
-
optimizeNames(names,
|
|
730
|
+
optimizeNames(names, constants2) {
|
|
721
731
|
var _a3, _b;
|
|
722
|
-
super.optimizeNames(names,
|
|
723
|
-
(_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names,
|
|
724
|
-
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names,
|
|
732
|
+
super.optimizeNames(names, constants2);
|
|
733
|
+
(_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants2);
|
|
734
|
+
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants2);
|
|
725
735
|
return this;
|
|
726
736
|
}
|
|
727
737
|
get names() {
|
|
@@ -1022,7 +1032,7 @@ var require_codegen = __commonJS({
|
|
|
1022
1032
|
function addExprNames(names, from) {
|
|
1023
1033
|
return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
|
|
1024
1034
|
}
|
|
1025
|
-
function optimizeExpr(expr, names,
|
|
1035
|
+
function optimizeExpr(expr, names, constants2) {
|
|
1026
1036
|
if (expr instanceof code_1.Name)
|
|
1027
1037
|
return replaceName(expr);
|
|
1028
1038
|
if (!canOptimize(expr))
|
|
@@ -1037,14 +1047,14 @@ var require_codegen = __commonJS({
|
|
|
1037
1047
|
return items;
|
|
1038
1048
|
}, []));
|
|
1039
1049
|
function replaceName(n) {
|
|
1040
|
-
const c =
|
|
1050
|
+
const c = constants2[n.str];
|
|
1041
1051
|
if (c === void 0 || names[n.str] !== 1)
|
|
1042
1052
|
return n;
|
|
1043
1053
|
delete names[n.str];
|
|
1044
1054
|
return c;
|
|
1045
1055
|
}
|
|
1046
1056
|
function canOptimize(e) {
|
|
1047
|
-
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 &&
|
|
1057
|
+
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants2[c.str] !== void 0);
|
|
1048
1058
|
}
|
|
1049
1059
|
}
|
|
1050
1060
|
function subtractNames(names, from) {
|
|
@@ -6865,8 +6875,9 @@ var stdoutLogger = {
|
|
|
6865
6875
|
};
|
|
6866
6876
|
|
|
6867
6877
|
// src/commands/init.ts
|
|
6868
|
-
import {
|
|
6869
|
-
import {
|
|
6878
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
6879
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, renameSync, rmdirSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
6880
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
6870
6881
|
|
|
6871
6882
|
// src/claudeInstall.ts
|
|
6872
6883
|
import { spawnSync } from "node:child_process";
|
|
@@ -7061,9 +7072,438 @@ function uninstallClaudeIntegration(opts) {
|
|
|
7061
7072
|
};
|
|
7062
7073
|
}
|
|
7063
7074
|
|
|
7075
|
+
// src/codexHookInstall.ts
|
|
7076
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7077
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
7078
|
+
import { parse as parseToml } from "smol-toml";
|
|
7079
|
+
function quote2(p) {
|
|
7080
|
+
return p.includes(" ") ? `"${p}"` : p;
|
|
7081
|
+
}
|
|
7082
|
+
function hookCommand2(nodePath, cliScriptPath, event) {
|
|
7083
|
+
return `${quote2(nodePath)} ${quote2(cliScriptPath)} codex-hook ${event}`;
|
|
7084
|
+
}
|
|
7085
|
+
function eventCommandFragment(event) {
|
|
7086
|
+
return `codex-hook ${event}`;
|
|
7087
|
+
}
|
|
7088
|
+
function isSameHookCommand2(actual, expected) {
|
|
7089
|
+
return actual === expected || actual.endsWith(` ${expected}`);
|
|
7090
|
+
}
|
|
7091
|
+
function isCaveatCodexHookCommand(actual, event) {
|
|
7092
|
+
const lower = actual.toLowerCase();
|
|
7093
|
+
return lower.includes("caveat") && actual.includes(eventCommandFragment(event));
|
|
7094
|
+
}
|
|
7095
|
+
function hasCaveatHook(hooksJson, event, fragment) {
|
|
7096
|
+
return hooksJson.hooks?.[event]?.some(
|
|
7097
|
+
(entry) => entry.hooks?.some((h) => h.command.includes(fragment))
|
|
7098
|
+
) ?? false;
|
|
7099
|
+
}
|
|
7100
|
+
function readHooks(path) {
|
|
7101
|
+
if (!existsSync2(path)) return {};
|
|
7102
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
7103
|
+
}
|
|
7104
|
+
function writeJsonWithBackup(path, value) {
|
|
7105
|
+
const dir = dirname3(path);
|
|
7106
|
+
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
7107
|
+
let backupPath = "";
|
|
7108
|
+
if (existsSync2(path)) {
|
|
7109
|
+
backupPath = `${path}.caveat-backup-${Date.now()}`;
|
|
7110
|
+
copyFileSync2(path, backupPath);
|
|
7111
|
+
}
|
|
7112
|
+
writeFileSync2(path, `${JSON.stringify(value, null, 2)}
|
|
7113
|
+
`, "utf-8");
|
|
7114
|
+
return backupPath;
|
|
7115
|
+
}
|
|
7116
|
+
function upsertHook2(hooksJson, event, command, subcommand) {
|
|
7117
|
+
hooksJson.hooks ??= {};
|
|
7118
|
+
const list = hooksJson.hooks[event] ??= [];
|
|
7119
|
+
for (const entry of list) {
|
|
7120
|
+
for (const hook of entry.hooks ?? []) {
|
|
7121
|
+
if (isSameHookCommand2(hook.command, command)) return "unchanged";
|
|
7122
|
+
if (isCaveatCodexHookCommand(hook.command, subcommand)) {
|
|
7123
|
+
hook.command = command;
|
|
7124
|
+
hook.timeoutSec = 5;
|
|
7125
|
+
hook.async = false;
|
|
7126
|
+
hook.statusMessage = null;
|
|
7127
|
+
return "added";
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
list.push({
|
|
7132
|
+
hooks: [
|
|
7133
|
+
{
|
|
7134
|
+
type: "command",
|
|
7135
|
+
command,
|
|
7136
|
+
timeoutSec: 5,
|
|
7137
|
+
async: false,
|
|
7138
|
+
statusMessage: null
|
|
7139
|
+
}
|
|
7140
|
+
]
|
|
7141
|
+
});
|
|
7142
|
+
return "added";
|
|
7143
|
+
}
|
|
7144
|
+
function removeHook2(hooksJson, event, command, subcommand) {
|
|
7145
|
+
const list = hooksJson.hooks?.[event];
|
|
7146
|
+
if (!list) return false;
|
|
7147
|
+
const before = list.length;
|
|
7148
|
+
const filtered = list.filter(
|
|
7149
|
+
(entry) => !entry.hooks?.some(
|
|
7150
|
+
(h) => isSameHookCommand2(h.command, command) || isCaveatCodexHookCommand(h.command, subcommand)
|
|
7151
|
+
)
|
|
7152
|
+
);
|
|
7153
|
+
if (filtered.length === before) return false;
|
|
7154
|
+
hooksJson.hooks[event] = filtered;
|
|
7155
|
+
return true;
|
|
7156
|
+
}
|
|
7157
|
+
function enableCodexHooksFeature(raw) {
|
|
7158
|
+
let parsed;
|
|
7159
|
+
try {
|
|
7160
|
+
parsed = parseToml(raw);
|
|
7161
|
+
} catch {
|
|
7162
|
+
return {
|
|
7163
|
+
status: "blocked",
|
|
7164
|
+
text: raw,
|
|
7165
|
+
changed: false,
|
|
7166
|
+
reason: "config.toml is invalid TOML; fix it before installing Caveat hooks"
|
|
7167
|
+
};
|
|
7168
|
+
}
|
|
7169
|
+
const featureValue = parsed.features;
|
|
7170
|
+
if (featureValue !== void 0 && !isPlainRecord(featureValue)) {
|
|
7171
|
+
return {
|
|
7172
|
+
status: "blocked",
|
|
7173
|
+
text: raw,
|
|
7174
|
+
changed: false,
|
|
7175
|
+
reason: "config.toml features value is not a table"
|
|
7176
|
+
};
|
|
7177
|
+
}
|
|
7178
|
+
const features = featureValue;
|
|
7179
|
+
const canonicalValue = features?.hooks;
|
|
7180
|
+
const legacyValue = features?.codex_hooks;
|
|
7181
|
+
if (canonicalValue !== void 0 && typeof canonicalValue !== "boolean") {
|
|
7182
|
+
return { status: "blocked", text: raw, changed: false, reason: "[features].hooks must be boolean" };
|
|
7183
|
+
}
|
|
7184
|
+
if (legacyValue !== void 0 && typeof legacyValue !== "boolean") {
|
|
7185
|
+
return { status: "blocked", text: raw, changed: false, reason: "[features].codex_hooks must be boolean" };
|
|
7186
|
+
}
|
|
7187
|
+
if (canonicalValue === false) {
|
|
7188
|
+
return { status: "blocked", text: raw, changed: false, reason: "[features].hooks is explicitly false" };
|
|
7189
|
+
}
|
|
7190
|
+
if (legacyValue === false) {
|
|
7191
|
+
return {
|
|
7192
|
+
status: "blocked",
|
|
7193
|
+
text: raw,
|
|
7194
|
+
changed: false,
|
|
7195
|
+
reason: canonicalValue === true ? "[features].hooks conflicts with the deprecated codex_hooks alias" : "[features].codex_hooks is explicitly false"
|
|
7196
|
+
};
|
|
7197
|
+
}
|
|
7198
|
+
if (canonicalValue === true && legacyValue === void 0) {
|
|
7199
|
+
return { status: "enabled", text: raw, changed: false };
|
|
7200
|
+
}
|
|
7201
|
+
const lines = raw.split(/\r?\n/);
|
|
7202
|
+
const codeLines = maskTomlStringsAndComments(raw).split(/\r?\n/);
|
|
7203
|
+
const featureHeaders = codeLines.map((line, index) => /^\s*\[\s*features\s*]\s*$/.test(line) ? index : -1).filter((index) => index >= 0);
|
|
7204
|
+
if (featureValue === void 0) {
|
|
7205
|
+
const prefix = raw.trimEnd();
|
|
7206
|
+
const text = `${prefix}${prefix ? "\n\n" : ""}[features]
|
|
7207
|
+
hooks = true
|
|
7208
|
+
`;
|
|
7209
|
+
return { status: "enabled", text, changed: true };
|
|
7210
|
+
}
|
|
7211
|
+
if (featureHeaders.length !== 1) {
|
|
7212
|
+
return {
|
|
7213
|
+
status: "blocked",
|
|
7214
|
+
text: raw,
|
|
7215
|
+
changed: false,
|
|
7216
|
+
reason: "config.toml uses a features table shape that Caveat will not rewrite safely"
|
|
7217
|
+
};
|
|
7218
|
+
}
|
|
7219
|
+
const featuresStart = featureHeaders[0];
|
|
7220
|
+
const assignments = [];
|
|
7221
|
+
for (let i = featuresStart + 1; i < lines.length; i += 1) {
|
|
7222
|
+
if (/^\s*\[\[?/.test(codeLines[i])) {
|
|
7223
|
+
break;
|
|
7224
|
+
}
|
|
7225
|
+
const assignment = /^\s*(hooks|codex_hooks)\s*=/.exec(codeLines[i]);
|
|
7226
|
+
if (assignment) {
|
|
7227
|
+
assignments.push({ index: i, key: assignment[1] });
|
|
7228
|
+
}
|
|
7229
|
+
}
|
|
7230
|
+
const canonical = assignments.filter((assignment) => assignment.key === "hooks");
|
|
7231
|
+
const legacy = assignments.filter((assignment) => assignment.key === "codex_hooks");
|
|
7232
|
+
if (canonicalValue !== void 0 && canonical.length !== 1 || legacyValue !== void 0 && legacy.length !== 1) {
|
|
7233
|
+
return {
|
|
7234
|
+
status: "blocked",
|
|
7235
|
+
text: raw,
|
|
7236
|
+
changed: false,
|
|
7237
|
+
reason: "[features].hooks uses a key shape that Caveat will not rewrite safely"
|
|
7238
|
+
};
|
|
7239
|
+
}
|
|
7240
|
+
const current = canonical[0];
|
|
7241
|
+
const deprecated = legacy[0];
|
|
7242
|
+
if (canonicalValue === true && legacyValue === true && current && deprecated) {
|
|
7243
|
+
const inlineComment = /(#.*)$/.exec(lines[deprecated.index])?.[1];
|
|
7244
|
+
lines.splice(deprecated.index, 1);
|
|
7245
|
+
if (inlineComment) {
|
|
7246
|
+
const canonicalIndex = current.index > deprecated.index ? current.index - 1 : current.index;
|
|
7247
|
+
lines.splice(canonicalIndex + 1, 0, inlineComment);
|
|
7248
|
+
}
|
|
7249
|
+
return { status: "enabled", text: `${lines.join("\n").trimEnd()}
|
|
7250
|
+
`, changed: true };
|
|
7251
|
+
}
|
|
7252
|
+
if (legacyValue === true && canonicalValue === void 0 && deprecated) {
|
|
7253
|
+
lines[deprecated.index] = lines[deprecated.index].replace(/\bcodex_hooks\b/, "hooks");
|
|
7254
|
+
return { status: "enabled", text: `${lines.join("\n").trimEnd()}
|
|
7255
|
+
`, changed: true };
|
|
7256
|
+
}
|
|
7257
|
+
lines.splice(featuresStart + 1, 0, "hooks = true");
|
|
7258
|
+
return { status: "enabled", text: `${lines.join("\n").trimEnd()}
|
|
7259
|
+
`, changed: true };
|
|
7260
|
+
}
|
|
7261
|
+
function isPlainRecord(value) {
|
|
7262
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7263
|
+
}
|
|
7264
|
+
function maskTomlStringsAndComments(raw) {
|
|
7265
|
+
let state = "code";
|
|
7266
|
+
let output = "";
|
|
7267
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
7268
|
+
const char = raw[i];
|
|
7269
|
+
const triple = raw.slice(i, i + 3);
|
|
7270
|
+
if (char === "\n") {
|
|
7271
|
+
output += "\n";
|
|
7272
|
+
if (state === "comment") state = "code";
|
|
7273
|
+
continue;
|
|
7274
|
+
}
|
|
7275
|
+
if (state === "comment") {
|
|
7276
|
+
output += " ";
|
|
7277
|
+
continue;
|
|
7278
|
+
}
|
|
7279
|
+
if (state === "basic") {
|
|
7280
|
+
output += " ";
|
|
7281
|
+
if (char === "\\" && i + 1 < raw.length) {
|
|
7282
|
+
output += raw[i + 1] === "\n" ? "\n" : " ";
|
|
7283
|
+
i += 1;
|
|
7284
|
+
} else if (char === '"') state = "code";
|
|
7285
|
+
continue;
|
|
7286
|
+
}
|
|
7287
|
+
if (state === "literal") {
|
|
7288
|
+
output += " ";
|
|
7289
|
+
if (char === "'") state = "code";
|
|
7290
|
+
continue;
|
|
7291
|
+
}
|
|
7292
|
+
if (state === "multi-basic") {
|
|
7293
|
+
if (triple === '"""') {
|
|
7294
|
+
output += " ";
|
|
7295
|
+
i += 2;
|
|
7296
|
+
state = "code";
|
|
7297
|
+
} else {
|
|
7298
|
+
output += " ";
|
|
7299
|
+
if (char === "\\" && i + 1 < raw.length) {
|
|
7300
|
+
output += raw[i + 1] === "\n" ? "\n" : " ";
|
|
7301
|
+
i += 1;
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7304
|
+
continue;
|
|
7305
|
+
}
|
|
7306
|
+
if (state === "multi-literal") {
|
|
7307
|
+
if (triple === "'''") {
|
|
7308
|
+
output += " ";
|
|
7309
|
+
i += 2;
|
|
7310
|
+
state = "code";
|
|
7311
|
+
} else output += " ";
|
|
7312
|
+
continue;
|
|
7313
|
+
}
|
|
7314
|
+
if (char === "#") {
|
|
7315
|
+
output += " ";
|
|
7316
|
+
state = "comment";
|
|
7317
|
+
} else if (triple === '"""') {
|
|
7318
|
+
output += " ";
|
|
7319
|
+
i += 2;
|
|
7320
|
+
state = "multi-basic";
|
|
7321
|
+
} else if (triple === "'''") {
|
|
7322
|
+
output += " ";
|
|
7323
|
+
i += 2;
|
|
7324
|
+
state = "multi-literal";
|
|
7325
|
+
} else if (char === '"') {
|
|
7326
|
+
output += " ";
|
|
7327
|
+
state = "basic";
|
|
7328
|
+
} else if (char === "'") {
|
|
7329
|
+
output += " ";
|
|
7330
|
+
state = "literal";
|
|
7331
|
+
} else output += char;
|
|
7332
|
+
}
|
|
7333
|
+
return output;
|
|
7334
|
+
}
|
|
7335
|
+
function writeConfigWithBackup(path, text) {
|
|
7336
|
+
const dir = dirname3(path);
|
|
7337
|
+
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
7338
|
+
let backupPath = "";
|
|
7339
|
+
if (existsSync2(path)) {
|
|
7340
|
+
backupPath = `${path}.caveat-backup-${Date.now()}`;
|
|
7341
|
+
copyFileSync2(path, backupPath);
|
|
7342
|
+
}
|
|
7343
|
+
writeFileSync2(path, text, "utf-8");
|
|
7344
|
+
return backupPath;
|
|
7345
|
+
}
|
|
7346
|
+
function installCodexHooks(opts) {
|
|
7347
|
+
const hooksPath = join4(opts.codexHome, "hooks.json");
|
|
7348
|
+
const configPath = join4(opts.codexHome, "config.toml");
|
|
7349
|
+
const rawConfig = existsSync2(configPath) ? readFileSync3(configPath, "utf-8") : "";
|
|
7350
|
+
const enabled = enableCodexHooksFeature(rawConfig);
|
|
7351
|
+
if (enabled.status === "blocked") {
|
|
7352
|
+
return {
|
|
7353
|
+
hooks: { userPromptSubmit: "skipped", postToolUse: "skipped", stop: "skipped" },
|
|
7354
|
+
feature: "blocked",
|
|
7355
|
+
blockedReason: enabled.reason
|
|
7356
|
+
};
|
|
7357
|
+
}
|
|
7358
|
+
const hooksJson = readHooks(hooksPath);
|
|
7359
|
+
const userPromptSubmit = upsertHook2(
|
|
7360
|
+
hooksJson,
|
|
7361
|
+
"UserPromptSubmit",
|
|
7362
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
|
|
7363
|
+
"user-prompt-submit"
|
|
7364
|
+
);
|
|
7365
|
+
const postToolUse = upsertHook2(
|
|
7366
|
+
hooksJson,
|
|
7367
|
+
"PostToolUse",
|
|
7368
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
|
|
7369
|
+
"post-tool-use"
|
|
7370
|
+
);
|
|
7371
|
+
const stop = upsertHook2(
|
|
7372
|
+
hooksJson,
|
|
7373
|
+
"Stop",
|
|
7374
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
|
|
7375
|
+
"stop"
|
|
7376
|
+
);
|
|
7377
|
+
const anyHookAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
|
|
7378
|
+
let backupPath;
|
|
7379
|
+
let configBackupPath;
|
|
7380
|
+
if (opts.dryRun) {
|
|
7381
|
+
opts.logger.info(`[dry-run] would update ${hooksPath}`);
|
|
7382
|
+
opts.logger.info(`[dry-run] would ensure [features].hooks = true in ${configPath}`);
|
|
7383
|
+
} else {
|
|
7384
|
+
if (anyHookAdded) {
|
|
7385
|
+
const backup = writeJsonWithBackup(hooksPath, hooksJson);
|
|
7386
|
+
if (backup) backupPath = backup;
|
|
7387
|
+
}
|
|
7388
|
+
if (enabled.changed) {
|
|
7389
|
+
const backup = writeConfigWithBackup(configPath, enabled.text);
|
|
7390
|
+
if (backup) configBackupPath = backup;
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
return {
|
|
7394
|
+
hooks: { userPromptSubmit, postToolUse, stop },
|
|
7395
|
+
feature: enabled.changed ? "enabled" : "unchanged",
|
|
7396
|
+
backupPath,
|
|
7397
|
+
configBackupPath
|
|
7398
|
+
};
|
|
7399
|
+
}
|
|
7400
|
+
function uninstallCodexHooks(opts) {
|
|
7401
|
+
const hooksPath = join4(opts.codexHome, "hooks.json");
|
|
7402
|
+
const hooksJson = readHooks(hooksPath);
|
|
7403
|
+
const userPromptSubmitRemoved = removeHook2(
|
|
7404
|
+
hooksJson,
|
|
7405
|
+
"UserPromptSubmit",
|
|
7406
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
|
|
7407
|
+
"user-prompt-submit"
|
|
7408
|
+
);
|
|
7409
|
+
const postToolUseRemoved = removeHook2(
|
|
7410
|
+
hooksJson,
|
|
7411
|
+
"PostToolUse",
|
|
7412
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
|
|
7413
|
+
"post-tool-use"
|
|
7414
|
+
);
|
|
7415
|
+
const stopRemoved = removeHook2(
|
|
7416
|
+
hooksJson,
|
|
7417
|
+
"Stop",
|
|
7418
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
|
|
7419
|
+
"stop"
|
|
7420
|
+
);
|
|
7421
|
+
let backupPath;
|
|
7422
|
+
if (opts.dryRun) {
|
|
7423
|
+
opts.logger.info(`[dry-run] would remove Caveat Codex hooks from ${hooksPath}`);
|
|
7424
|
+
} else if (userPromptSubmitRemoved || postToolUseRemoved || stopRemoved) {
|
|
7425
|
+
const backup = writeJsonWithBackup(hooksPath, hooksJson);
|
|
7426
|
+
if (backup) backupPath = backup;
|
|
7427
|
+
}
|
|
7428
|
+
return {
|
|
7429
|
+
hooks: {
|
|
7430
|
+
userPromptSubmit: userPromptSubmitRemoved ? "added" : "unchanged",
|
|
7431
|
+
postToolUse: postToolUseRemoved ? "added" : "unchanged",
|
|
7432
|
+
stop: stopRemoved ? "added" : "unchanged"
|
|
7433
|
+
},
|
|
7434
|
+
feature: "unchanged",
|
|
7435
|
+
backupPath
|
|
7436
|
+
};
|
|
7437
|
+
}
|
|
7438
|
+
function detectCodexHookInstallation(codexHome) {
|
|
7439
|
+
const hooksPath = join4(codexHome, "hooks.json");
|
|
7440
|
+
const hooksJson = readHooks(hooksPath);
|
|
7441
|
+
const hooks = {
|
|
7442
|
+
userPromptSubmit: hasCaveatHook(
|
|
7443
|
+
hooksJson,
|
|
7444
|
+
"UserPromptSubmit",
|
|
7445
|
+
eventCommandFragment("user-prompt-submit")
|
|
7446
|
+
),
|
|
7447
|
+
postToolUse: hasCaveatHook(hooksJson, "PostToolUse", eventCommandFragment("post-tool-use")),
|
|
7448
|
+
stop: hasCaveatHook(hooksJson, "Stop", eventCommandFragment("stop"))
|
|
7449
|
+
};
|
|
7450
|
+
const count = Object.values(hooks).filter(Boolean).length;
|
|
7451
|
+
return {
|
|
7452
|
+
installation: count === 0 ? "not-installed" : count === 3 ? "installed" : "partial",
|
|
7453
|
+
hooksPath,
|
|
7454
|
+
hooks
|
|
7455
|
+
};
|
|
7456
|
+
}
|
|
7457
|
+
|
|
7458
|
+
// src/ghSetup.ts
|
|
7459
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7460
|
+
import { readSync } from "node:fs";
|
|
7461
|
+
function runGh(args) {
|
|
7462
|
+
const result = spawnSync2("gh", args, { encoding: "utf-8" });
|
|
7463
|
+
return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "", stderr: typeof result.stderr === "string" ? result.stderr : "" };
|
|
7464
|
+
}
|
|
7465
|
+
function commandError(result, fallback) {
|
|
7466
|
+
return new Error((result.stderr || result.stdout || fallback).trim());
|
|
7467
|
+
}
|
|
7468
|
+
function defaultGitHubRepoUrl(opts) {
|
|
7469
|
+
const version2 = opts.ghRunner(["--version"]);
|
|
7470
|
+
if (version2.status !== 0) {
|
|
7471
|
+
throw new Error(
|
|
7472
|
+
`gh is not available. create a ${opts.visibility} repository named ${opts.repositoryName} on GitHub, then run:
|
|
7473
|
+
${opts.retryCommand}
|
|
7474
|
+
# or install gh and retry`
|
|
7475
|
+
);
|
|
7476
|
+
}
|
|
7477
|
+
const user = opts.ghRunner(["api", "user", "-q", ".login"]);
|
|
7478
|
+
if (user.status !== 0 || !user.stdout.trim()) {
|
|
7479
|
+
throw new Error(`gh is not authenticated; run \`gh auth login\`, then retry \`${opts.retryCommand}\``);
|
|
7480
|
+
}
|
|
7481
|
+
const login = user.stdout.trim();
|
|
7482
|
+
const qualifiedName = `${login}/${opts.repositoryName}`;
|
|
7483
|
+
const view = opts.ghRunner(["repo", "view", qualifiedName]);
|
|
7484
|
+
if (view.status !== 0) {
|
|
7485
|
+
const question = `create ${opts.visibility} repo github.com/${qualifiedName}? [y/N]`;
|
|
7486
|
+
if (!opts.yes) {
|
|
7487
|
+
if (!opts.isTty()) {
|
|
7488
|
+
throw new Error(`${question}; rerun with --yes to approve non-interactively`);
|
|
7489
|
+
}
|
|
7490
|
+
if (!opts.confirm(question)) throw new Error(`${opts.visibility} repository creation cancelled`);
|
|
7491
|
+
}
|
|
7492
|
+
const create = opts.ghRunner(["repo", "create", opts.repositoryName, `--${opts.visibility}`]);
|
|
7493
|
+
if (create.status !== 0) throw commandError(create, "gh repo create failed");
|
|
7494
|
+
}
|
|
7495
|
+
return `https://github.com/${qualifiedName}.git`;
|
|
7496
|
+
}
|
|
7497
|
+
function askOnce(question) {
|
|
7498
|
+
process.stdout.write(`${question} `);
|
|
7499
|
+
const buf = Buffer.alloc(256);
|
|
7500
|
+
const bytes = readSync(0, buf, 0, buf.length, null);
|
|
7501
|
+
return /^(?:y|yes)$/i.test(buf.toString("utf-8", 0, bytes).trim());
|
|
7502
|
+
}
|
|
7503
|
+
|
|
7064
7504
|
// src/nodePath.ts
|
|
7065
|
-
import { existsSync as
|
|
7066
|
-
import { delimiter, join as
|
|
7505
|
+
import { existsSync as existsSync3, realpathSync } from "node:fs";
|
|
7506
|
+
import { delimiter, join as join5 } from "node:path";
|
|
7067
7507
|
var defaultRealpath = (path) => realpathSync.native(path);
|
|
7068
7508
|
function safeRealpath(path, realpath = defaultRealpath) {
|
|
7069
7509
|
try {
|
|
@@ -7076,7 +7516,7 @@ function resolveHookNodePath({
|
|
|
7076
7516
|
env = process.env,
|
|
7077
7517
|
execPath = process.execPath,
|
|
7078
7518
|
platform = process.platform,
|
|
7079
|
-
exists =
|
|
7519
|
+
exists = existsSync3,
|
|
7080
7520
|
realpath = defaultRealpath
|
|
7081
7521
|
} = {}) {
|
|
7082
7522
|
const execRealpath = safeRealpath(execPath, realpath);
|
|
@@ -7084,7 +7524,7 @@ function resolveHookNodePath({
|
|
|
7084
7524
|
const names = platform === "win32" ? ["node.exe", "node.cmd", "node.bat", "node"] : ["node"];
|
|
7085
7525
|
for (const dir of pathEnv.split(delimiter).filter(Boolean)) {
|
|
7086
7526
|
for (const name of names) {
|
|
7087
|
-
const candidate =
|
|
7527
|
+
const candidate = join5(dir, name);
|
|
7088
7528
|
if (!exists(candidate)) continue;
|
|
7089
7529
|
const candidateRealpath = safeRealpath(candidate, realpath);
|
|
7090
7530
|
if (execRealpath && candidateRealpath && candidateRealpath === execRealpath) {
|
|
@@ -7095,29 +7535,241 @@ function resolveHookNodePath({
|
|
|
7095
7535
|
return execPath;
|
|
7096
7536
|
}
|
|
7097
7537
|
|
|
7538
|
+
// src/commands/publish.ts
|
|
7539
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
7540
|
+
import { join as join7 } from "node:path";
|
|
7541
|
+
|
|
7542
|
+
// src/commands/codexSidecarAdvisory.ts
|
|
7543
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
7544
|
+
import { mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7545
|
+
import { tmpdir } from "node:os";
|
|
7546
|
+
import { join as join6 } from "node:path";
|
|
7547
|
+
function compactFailureMessage(message) {
|
|
7548
|
+
const singleLine = message.replace(/\s+/g, " ").trim();
|
|
7549
|
+
if (!singleLine) return "unknown error";
|
|
7550
|
+
return singleLine.length > 220 ? `${singleLine.slice(0, 220)}...` : singleLine;
|
|
7551
|
+
}
|
|
7552
|
+
function formatCodexSidecarAdvisoryUnavailable(error51) {
|
|
7553
|
+
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
7554
|
+
return `[caveat:codex-sidecar] advisory unavailable: ${compactFailureMessage(message)}`;
|
|
7555
|
+
}
|
|
7556
|
+
function sidecarFailureDetail(output) {
|
|
7557
|
+
const protocol = output.match(/PROTOCOL_ERROR:[^"\r\n]+/);
|
|
7558
|
+
if (protocol) return compactFailureMessage(protocol[0]);
|
|
7559
|
+
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
7560
|
+
const diagnostic = lines.find(
|
|
7561
|
+
(line) => !line.startsWith("[caveat] [codex-sidecar] codex-sidecar ")
|
|
7562
|
+
);
|
|
7563
|
+
return compactFailureMessage(diagnostic ?? output);
|
|
7564
|
+
}
|
|
7565
|
+
function runCodexSidecarAdvisory(input) {
|
|
7566
|
+
const cliScript = process.argv[1];
|
|
7567
|
+
if (!cliScript) {
|
|
7568
|
+
return { status: "failed", message: "current Caveat CLI script path is unavailable" };
|
|
7569
|
+
}
|
|
7570
|
+
const args = [
|
|
7571
|
+
...process.execArgv,
|
|
7572
|
+
"--disable-warning=ExperimentalWarning",
|
|
7573
|
+
cliScript,
|
|
7574
|
+
"codex-sidecar",
|
|
7575
|
+
"run",
|
|
7576
|
+
"explore",
|
|
7577
|
+
input.prompt,
|
|
7578
|
+
"--preset",
|
|
7579
|
+
"advisory",
|
|
7580
|
+
"--query",
|
|
7581
|
+
input.searchText,
|
|
7582
|
+
"--limit",
|
|
7583
|
+
String(Math.max(1, input.limit)),
|
|
7584
|
+
"--host-agent",
|
|
7585
|
+
"claude",
|
|
7586
|
+
"--availability",
|
|
7587
|
+
"operational"
|
|
7588
|
+
];
|
|
7589
|
+
let resultDir;
|
|
7590
|
+
let advisory;
|
|
7591
|
+
try {
|
|
7592
|
+
resultDir = mkdtempSync(join6(tmpdir(), "caveat-codex-advisory-"));
|
|
7593
|
+
const resultFile = join6(resultDir, "result.json");
|
|
7594
|
+
args.push("--save-result", resultFile);
|
|
7595
|
+
if (input.additionalContext) {
|
|
7596
|
+
const additionalContextFile = join6(resultDir, "hook-signal.json");
|
|
7597
|
+
writeFileSync3(additionalContextFile, JSON.stringify({ context: [input.additionalContext] }) + "\n", { encoding: "utf-8", mode: 384, flag: "wx" });
|
|
7598
|
+
args.push("--additional-context-file", additionalContextFile);
|
|
7599
|
+
}
|
|
7600
|
+
const nodeCli = process.env.CAVEAT_CODEX_SIDECAR_NODE_CLI;
|
|
7601
|
+
if (nodeCli) {
|
|
7602
|
+
args.push("--node-cli", nodeCli);
|
|
7603
|
+
} else {
|
|
7604
|
+
const command = process.env.CAVEAT_CODEX_SIDECAR_COMMAND;
|
|
7605
|
+
if (command) args.push("--command", command);
|
|
7606
|
+
}
|
|
7607
|
+
const result = spawnSync3(process.execPath, args, {
|
|
7608
|
+
cwd: input.projectRoot,
|
|
7609
|
+
encoding: "utf-8",
|
|
7610
|
+
timeout: Number(process.env.CAVEAT_HOOK_CODEX_SIDECAR_TIMEOUT_MS ?? 12e4),
|
|
7611
|
+
maxBuffer: 10 * 1024 * 1024
|
|
7612
|
+
});
|
|
7613
|
+
if (result.error) {
|
|
7614
|
+
advisory = { status: "failed", message: compactFailureMessage(result.error.message) };
|
|
7615
|
+
} else if (result.status !== 0) {
|
|
7616
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status}`).trim();
|
|
7617
|
+
advisory = {
|
|
7618
|
+
status: "failed",
|
|
7619
|
+
message: `sidecar command failed: ${sidecarFailureDetail(detail)}`
|
|
7620
|
+
};
|
|
7621
|
+
} else {
|
|
7622
|
+
const parsed = JSON.parse(readFileSync4(resultFile, "utf-8"));
|
|
7623
|
+
if (parsed.status !== "ok" || typeof parsed.summary !== "string") {
|
|
7624
|
+
advisory = {
|
|
7625
|
+
status: "failed",
|
|
7626
|
+
message: compactFailureMessage(`unexpected SidecarResult status: ${String(parsed.status)}`)
|
|
7627
|
+
};
|
|
7628
|
+
} else {
|
|
7629
|
+
advisory = {
|
|
7630
|
+
status: "ok",
|
|
7631
|
+
summary: parsed.summary,
|
|
7632
|
+
...typeof parsed.rawEventLogRef === "string" ? { rawEventLogRef: parsed.rawEventLogRef } : {}
|
|
7633
|
+
};
|
|
7634
|
+
}
|
|
7635
|
+
}
|
|
7636
|
+
} catch (err) {
|
|
7637
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7638
|
+
advisory = {
|
|
7639
|
+
status: "failed",
|
|
7640
|
+
message: compactFailureMessage(`invalid SidecarResult JSON: ${msg}`)
|
|
7641
|
+
};
|
|
7642
|
+
} finally {
|
|
7643
|
+
if (resultDir) {
|
|
7644
|
+
try {
|
|
7645
|
+
rmSync(resultDir, { recursive: true });
|
|
7646
|
+
} catch (err) {
|
|
7647
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7648
|
+
advisory = {
|
|
7649
|
+
status: "failed",
|
|
7650
|
+
message: compactFailureMessage(`sidecar advisory cleanup failed: ${msg}`)
|
|
7651
|
+
};
|
|
7652
|
+
}
|
|
7653
|
+
}
|
|
7654
|
+
}
|
|
7655
|
+
return advisory;
|
|
7656
|
+
}
|
|
7657
|
+
function formatCodexSidecarAdvisory(advisory) {
|
|
7658
|
+
if (advisory.status === "ok") {
|
|
7659
|
+
return [
|
|
7660
|
+
"[caveat:codex-sidecar] Codex advisory:",
|
|
7661
|
+
advisory.summary,
|
|
7662
|
+
advisory.rawEventLogRef ? `rawEventLogRef: ${advisory.rawEventLogRef}` : ""
|
|
7663
|
+
].filter(Boolean).join("\n");
|
|
7664
|
+
}
|
|
7665
|
+
return `[caveat:codex-sidecar] advisory unavailable: ${advisory.message}`;
|
|
7666
|
+
}
|
|
7667
|
+
|
|
7668
|
+
// src/commands/publish.ts
|
|
7669
|
+
var HTTPS_TARGET_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+?(?:\.git)?\/?$/;
|
|
7670
|
+
var SCP_TARGET_RE = /^git@github\.com:[^/]+\/[^/]+?(?:\.git)?$/;
|
|
7671
|
+
function validatePublishTarget(url2) {
|
|
7672
|
+
if (!HTTPS_TARGET_RE.test(url2) && !SCP_TARGET_RE.test(url2)) throw new Error("invalid publish target: use https://github.com/<org>/<repo>(.git) or git@github.com:<org>/<repo>(.git)");
|
|
7673
|
+
return url2;
|
|
7674
|
+
}
|
|
7675
|
+
async function defaultPublicRepoUrl(opts, deps) {
|
|
7676
|
+
return defaultGitHubRepoUrl({
|
|
7677
|
+
repositoryName: "Caveat-Public",
|
|
7678
|
+
visibility: "public",
|
|
7679
|
+
yes: opts.yes,
|
|
7680
|
+
retryCommand: "caveat publish --init <url>",
|
|
7681
|
+
...deps
|
|
7682
|
+
});
|
|
7683
|
+
}
|
|
7684
|
+
async function runPublish(ctx, opts, dependencies = {}) {
|
|
7685
|
+
try {
|
|
7686
|
+
const ghRunner = dependencies.ghRunner ?? runGh;
|
|
7687
|
+
const isTty = dependencies.isTty ?? (() => Boolean(process.stdin.isTTY));
|
|
7688
|
+
const confirm = dependencies.confirm ?? askOnce;
|
|
7689
|
+
const projectRoot = process.cwd();
|
|
7690
|
+
const hasCodexSidecarConfig = dependencies.hasCodexSidecarConfig ?? ((root) => existsSync4(join7(root, ".codex-sidecar.yml")));
|
|
7691
|
+
const publishAdvisory = hasCodexSidecarConfig(projectRoot) ? (changes) => {
|
|
7692
|
+
try {
|
|
7693
|
+
return dependencies.runCodexSidecarAdvisory?.(changes, projectRoot) ?? formatCodexSidecarAdvisory(runCodexSidecarAdvisory({
|
|
7694
|
+
searchText: changes.lines.join("\n"),
|
|
7695
|
+
limit: Math.max(changes.lines.length, 1),
|
|
7696
|
+
projectRoot,
|
|
7697
|
+
prompt: "A public Caveat publish is pending. Use Caveat context to give concise advice about privacy or secret risk. Advisory only: do not authorize or block this publish."
|
|
7698
|
+
}));
|
|
7699
|
+
} catch (err) {
|
|
7700
|
+
return formatCodexSidecarAdvisoryUnavailable(err);
|
|
7701
|
+
}
|
|
7702
|
+
} : void 0;
|
|
7703
|
+
let target = ctx.config.publishTarget;
|
|
7704
|
+
if (opts.init !== void 0) {
|
|
7705
|
+
target = validatePublishTarget(opts.init);
|
|
7706
|
+
writeUserConfigPatch(ctx.userConfigPath, { publishTarget: target });
|
|
7707
|
+
}
|
|
7708
|
+
if (!target) {
|
|
7709
|
+
target = await defaultPublicRepoUrl(opts, { ghRunner, isTty, confirm });
|
|
7710
|
+
writeUserConfigPatch(ctx.userConfigPath, { publishTarget: target });
|
|
7711
|
+
}
|
|
7712
|
+
const result = await publishOwn({
|
|
7713
|
+
paths: ctx.paths,
|
|
7714
|
+
config: { ...ctx.config, publishTarget: target },
|
|
7715
|
+
logger: ctx.logger,
|
|
7716
|
+
confirmImpl: confirm,
|
|
7717
|
+
isTty,
|
|
7718
|
+
dryRun: opts.dryRun,
|
|
7719
|
+
yes: opts.yes,
|
|
7720
|
+
allow: opts.allow,
|
|
7721
|
+
saveAllow: opts.save,
|
|
7722
|
+
advisory: publishAdvisory
|
|
7723
|
+
});
|
|
7724
|
+
if (result.dryRun) ctx.logger.info(`[dry-run] ${result.fileCount} public entry(ies) ready to publish`);
|
|
7725
|
+
else if (result.changed) ctx.logger.info(`published ${result.fileCount} public entry(ies)`);
|
|
7726
|
+
} catch (err) {
|
|
7727
|
+
if (err instanceof PublishScanError) {
|
|
7728
|
+
for (const finding of err.findings) {
|
|
7729
|
+
ctx.logger.error(`${finding.relPath}:${finding.line} ${finding.rule} ${finding.excerpt}`);
|
|
7730
|
+
ctx.logger.error(` --allow ${finding.matchDigest}`);
|
|
7731
|
+
}
|
|
7732
|
+
process.exitCode = 1;
|
|
7733
|
+
return;
|
|
7734
|
+
}
|
|
7735
|
+
ctx.logger.error(err instanceof Error ? err.message : String(err));
|
|
7736
|
+
process.exitCode = 1;
|
|
7737
|
+
}
|
|
7738
|
+
}
|
|
7739
|
+
|
|
7098
7740
|
// src/commands/init.ts
|
|
7099
|
-
async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
|
|
7741
|
+
async function runInit(ctx, opts = { skipClaude: false, dryRun: false }, dependencies = {}) {
|
|
7742
|
+
const isTty = dependencies.isTty ?? (() => Boolean(process.stdin.isTTY));
|
|
7743
|
+
const confirm = dependencies.confirm ?? askOnce;
|
|
7744
|
+
const ghRunner = dependencies.ghRunner ?? runGh;
|
|
7745
|
+
let publishTarget = ctx.config.publishTarget;
|
|
7746
|
+
let codexHookState = "not-installed";
|
|
7100
7747
|
ensureUserConfig(ctx.userConfigPath);
|
|
7101
7748
|
ctx.logger.info(`user config: ${ctx.userConfigPath}`);
|
|
7102
|
-
if (!
|
|
7103
|
-
|
|
7104
|
-
|
|
7749
|
+
if (!existsSync5(ctx.paths.knowledgeRepo)) {
|
|
7750
|
+
mkdirSync3(ctx.paths.knowledgeRepo, { recursive: true });
|
|
7751
|
+
mkdirSync3(ctx.paths.entriesDir, { recursive: true });
|
|
7105
7752
|
ctx.logger.info(`knowledge repo scaffolded: ${ctx.paths.knowledgeRepo}`);
|
|
7106
7753
|
} else {
|
|
7107
7754
|
ctx.logger.info(`knowledge repo: ${ctx.paths.knowledgeRepo}`);
|
|
7108
7755
|
}
|
|
7109
7756
|
migrateLegacyCommunityDir(ctx);
|
|
7110
|
-
const gitignorePath =
|
|
7111
|
-
if (!
|
|
7112
|
-
|
|
7757
|
+
const gitignorePath = join8(ctx.paths.knowledgeRepo, ".gitignore");
|
|
7758
|
+
if (!existsSync5(gitignorePath)) {
|
|
7759
|
+
writeFileSync4(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
|
|
7113
7760
|
ctx.logger.info(`.gitignore created: ${gitignorePath}`);
|
|
7114
7761
|
}
|
|
7115
7762
|
if (!opts.dryRun) {
|
|
7116
|
-
const dbDir =
|
|
7117
|
-
if (!
|
|
7763
|
+
const dbDir = dirname4(ctx.paths.dbPath);
|
|
7764
|
+
if (!existsSync5(dbDir)) mkdirSync3(dbDir, { recursive: true });
|
|
7765
|
+
const keyProvider = createKeyserverKeyProvider({ caveatHome: ctx.caveatHome });
|
|
7766
|
+
const failures = await prewarmSealedKeys({ paths: ctx.paths, keyProvider });
|
|
7767
|
+
for (const failure of failures) {
|
|
7768
|
+
ctx.logger.warn(`${failure.source}: sealed key prewarm failed: ${errorMessage(failure.error)}`);
|
|
7769
|
+
}
|
|
7118
7770
|
const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
|
|
7119
7771
|
try {
|
|
7120
|
-
reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger });
|
|
7772
|
+
reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger, keyProvider });
|
|
7121
7773
|
writeDigestMarker(ctx.caveatHome, computeEntriesDigest(ctx.paths));
|
|
7122
7774
|
} finally {
|
|
7123
7775
|
db.close();
|
|
@@ -7140,39 +7792,179 @@ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
|
|
|
7140
7792
|
ctx.logger.info(
|
|
7141
7793
|
"tip: subscribe to a group repo with `caveat community add <github-url>`, then `caveat pull`."
|
|
7142
7794
|
);
|
|
7795
|
+
let syncRequested = opts.sync !== void 0 && opts.sync !== false;
|
|
7796
|
+
let syncNudgeAccepted = false;
|
|
7797
|
+
if (opts.sync === void 0 && isTty()) {
|
|
7798
|
+
syncNudgeAccepted = confirm("private \u540C\u671F\u3092\u4ECA\u3059\u3050\u8A2D\u5B9A\u3059\u308B\uFF1F [y/N]");
|
|
7799
|
+
syncRequested = syncNudgeAccepted;
|
|
7800
|
+
}
|
|
7801
|
+
if (syncRequested) {
|
|
7802
|
+
try {
|
|
7803
|
+
if (opts.dryRun) {
|
|
7804
|
+
const target = typeof opts.sync === "string" ? opts.sync : "<user>/Caveat-Private";
|
|
7805
|
+
ctx.logger.info(`[dry-run] would initialize private sync: ${target}`);
|
|
7806
|
+
} else {
|
|
7807
|
+
const url2 = typeof opts.sync === "string" ? opts.sync : defaultGitHubRepoUrl({
|
|
7808
|
+
repositoryName: "Caveat-Private",
|
|
7809
|
+
visibility: "private",
|
|
7810
|
+
yes: (opts.yes ?? false) || syncNudgeAccepted,
|
|
7811
|
+
retryCommand: "caveat init --sync <url>",
|
|
7812
|
+
ghRunner,
|
|
7813
|
+
isTty,
|
|
7814
|
+
confirm
|
|
7815
|
+
});
|
|
7816
|
+
const result = await initOwnSync({
|
|
7817
|
+
ownDir: ctx.paths.knowledgeRepo,
|
|
7818
|
+
url: url2,
|
|
7819
|
+
caveatHome: ctx.caveatHome,
|
|
7820
|
+
paths: ctx.paths,
|
|
7821
|
+
logger: ctx.logger,
|
|
7822
|
+
// file: remotes are governed by local filesystem permissions and have
|
|
7823
|
+
// no anonymous HTTP surface for the remote-visibility probe to inspect.
|
|
7824
|
+
trustRemotePrivate: isFileRemote(url2)
|
|
7825
|
+
});
|
|
7826
|
+
ctx.logger.info(
|
|
7827
|
+
result.remoteWasEmpty ? `initialized private sync remote: ${url2}` : `checked out existing private remote: ${url2} (branch ${result.branch})`
|
|
7828
|
+
);
|
|
7829
|
+
}
|
|
7830
|
+
} catch (err) {
|
|
7831
|
+
if (err instanceof SyncError && err.code === "OWN_REPO_EXISTS") {
|
|
7832
|
+
ctx.logger.info("private sync already configured (own repo exists); skipped");
|
|
7833
|
+
} else {
|
|
7834
|
+
ctx.logger.warn(`private sync setup skipped: ${errorMessage(err)}`);
|
|
7835
|
+
}
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
let publishRequested = opts.publishTarget !== void 0 && opts.publishTarget !== false;
|
|
7839
|
+
let publishNudgeAccepted = false;
|
|
7840
|
+
if (opts.publishTarget === void 0 && isTty()) {
|
|
7841
|
+
publishNudgeAccepted = confirm("\u516C\u958B repo\uFF08\u5C01\u7DD8\u30DF\u30E9\u30FC\uFF09\u3082\u8A2D\u5B9A\u3059\u308B\uFF1F [y/N]");
|
|
7842
|
+
publishRequested = publishNudgeAccepted;
|
|
7843
|
+
}
|
|
7844
|
+
if (publishRequested) {
|
|
7845
|
+
try {
|
|
7846
|
+
const requested = typeof opts.publishTarget === "string" ? validatePublishTarget(opts.publishTarget) : publishTarget;
|
|
7847
|
+
const target = requested ?? (opts.dryRun ? "<user>/Caveat-Public" : defaultGitHubRepoUrl({
|
|
7848
|
+
repositoryName: "Caveat-Public",
|
|
7849
|
+
visibility: "public",
|
|
7850
|
+
yes: (opts.yes ?? false) || publishNudgeAccepted,
|
|
7851
|
+
retryCommand: "caveat init --publish-target <url>",
|
|
7852
|
+
ghRunner,
|
|
7853
|
+
isTty,
|
|
7854
|
+
confirm
|
|
7855
|
+
}));
|
|
7856
|
+
if (opts.dryRun) {
|
|
7857
|
+
ctx.logger.info(`[dry-run] would configure publish target: ${target}`);
|
|
7858
|
+
publishTarget = target;
|
|
7859
|
+
} else if (target === publishTarget) {
|
|
7860
|
+
ctx.logger.info(`publish target already configured: ${target}`);
|
|
7861
|
+
} else {
|
|
7862
|
+
writeUserConfigPatch(ctx.userConfigPath, { publishTarget: target });
|
|
7863
|
+
publishTarget = target;
|
|
7864
|
+
ctx.logger.info(`publish target configured: ${target}`);
|
|
7865
|
+
}
|
|
7866
|
+
} catch (err) {
|
|
7867
|
+
ctx.logger.warn(`publish target setup skipped: ${errorMessage(err)}`);
|
|
7868
|
+
}
|
|
7869
|
+
}
|
|
7143
7870
|
if (opts.skipClaude) {
|
|
7144
7871
|
ctx.logger.info("Claude Code integration skipped (--skip-claude)");
|
|
7145
|
-
|
|
7872
|
+
} else {
|
|
7873
|
+
const cliScriptPath = process.argv[1];
|
|
7874
|
+
if (!cliScriptPath) {
|
|
7875
|
+
ctx.logger.warn("cannot determine CLI script path; skipping Claude integration");
|
|
7876
|
+
} else {
|
|
7877
|
+
const result = installClaudeIntegration({
|
|
7878
|
+
claudeDir: join8(ctx.userHome, ".claude"),
|
|
7879
|
+
cliScriptPath,
|
|
7880
|
+
nodePath: resolveHookNodePath(),
|
|
7881
|
+
dryRun: opts.dryRun,
|
|
7882
|
+
logger: ctx.logger
|
|
7883
|
+
});
|
|
7884
|
+
reportInstallResult(ctx, result, opts.dryRun);
|
|
7885
|
+
}
|
|
7146
7886
|
}
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
ctx.logger.
|
|
7150
|
-
|
|
7887
|
+
if (opts.skipCodexHook) {
|
|
7888
|
+
codexHookState = "skipped";
|
|
7889
|
+
ctx.logger.info("Codex hook integration skipped (--skip-codex-hook)");
|
|
7890
|
+
} else if ((dependencies.codexAvailable ?? detectCodexAvailability)()) {
|
|
7891
|
+
const cliScriptPath = process.argv[1];
|
|
7892
|
+
if (!cliScriptPath) {
|
|
7893
|
+
codexHookState = "skipped";
|
|
7894
|
+
ctx.logger.warn("cannot determine CLI script path; skipping Codex hook integration");
|
|
7895
|
+
} else {
|
|
7896
|
+
const codexHome = join8(ctx.userHome, ".codex");
|
|
7897
|
+
const result = installCodexHooks({
|
|
7898
|
+
codexHome,
|
|
7899
|
+
cliScriptPath,
|
|
7900
|
+
nodePath: resolveHookNodePath(),
|
|
7901
|
+
dryRun: opts.dryRun,
|
|
7902
|
+
logger: ctx.logger
|
|
7903
|
+
});
|
|
7904
|
+
if (result.feature === "blocked") {
|
|
7905
|
+
codexHookState = "skipped";
|
|
7906
|
+
ctx.logger.warn(
|
|
7907
|
+
`${result.blockedReason}; preserving explicit consent. Set \`hooks = true\` in ${join8(codexHome, "config.toml")}, then rerun \`caveat init\`.`
|
|
7908
|
+
);
|
|
7909
|
+
} else if (opts.dryRun) {
|
|
7910
|
+
codexHookState = "skipped";
|
|
7911
|
+
ctx.logger.info("[dry-run] would install Codex hooks");
|
|
7912
|
+
} else {
|
|
7913
|
+
codexHookState = detectCodexHookInstallation(codexHome).installation;
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7151
7916
|
}
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7917
|
+
reportEnvironmentSummary(ctx, publishTarget, codexHookState, opts.dryRun);
|
|
7918
|
+
}
|
|
7919
|
+
function detectCodexAvailability() {
|
|
7920
|
+
const result = spawnSync4("codex --version", {
|
|
7921
|
+
shell: true,
|
|
7922
|
+
encoding: "utf-8",
|
|
7923
|
+
stdio: "ignore"
|
|
7158
7924
|
});
|
|
7159
|
-
|
|
7925
|
+
return !result.error && result.status === 0;
|
|
7926
|
+
}
|
|
7927
|
+
function isFileRemote(url2) {
|
|
7928
|
+
try {
|
|
7929
|
+
return new URL(url2).protocol === "file:";
|
|
7930
|
+
} catch {
|
|
7931
|
+
return false;
|
|
7932
|
+
}
|
|
7933
|
+
}
|
|
7934
|
+
function gitOutput(args) {
|
|
7935
|
+
const result = spawnSync4("git", args, { encoding: "utf-8" });
|
|
7936
|
+
return result.status === 0 && typeof result.stdout === "string" ? result.stdout.trim() : null;
|
|
7937
|
+
}
|
|
7938
|
+
function reportEnvironmentSummary(ctx, publishTarget, codexHookState, dryRun) {
|
|
7939
|
+
const prefix = dryRun ? "[dry-run] would have " : "";
|
|
7940
|
+
const isRepo = gitOutput(["-C", ctx.paths.knowledgeRepo, "rev-parse", "--is-inside-work-tree"]) === "true";
|
|
7941
|
+
const remote = isRepo ? gitOutput(["-C", ctx.paths.knowledgeRepo, "config", "--get", "remote.origin.url"]) : null;
|
|
7942
|
+
const communityCount = existsSync5(ctx.paths.communityDir) ? readdirSync(ctx.paths.communityDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length : 0;
|
|
7943
|
+
ctx.logger.info(`${prefix}environment summary:`);
|
|
7944
|
+
ctx.logger.info(` own git: ${isRepo ? "repository" : "not initialized"}`);
|
|
7945
|
+
ctx.logger.info(` private remote: ${remote || "not configured"}`);
|
|
7946
|
+
ctx.logger.info(` publish target: ${publishTarget || "not configured"}`);
|
|
7947
|
+
ctx.logger.info(` community sources: ${communityCount}`);
|
|
7948
|
+
ctx.logger.info(` codex hook: ${codexHookState}`);
|
|
7949
|
+
}
|
|
7950
|
+
function errorMessage(err) {
|
|
7951
|
+
return err instanceof Error ? err.message : String(err);
|
|
7160
7952
|
}
|
|
7161
7953
|
function migrateLegacyCommunityDir(ctx) {
|
|
7162
|
-
const legacy =
|
|
7954
|
+
const legacy = join8(ctx.paths.knowledgeRepo, "community");
|
|
7163
7955
|
const current = ctx.paths.communityDir;
|
|
7164
7956
|
if (legacy === current) return;
|
|
7165
|
-
if (!
|
|
7166
|
-
if (
|
|
7957
|
+
if (!existsSync5(legacy)) return;
|
|
7958
|
+
if (existsSync5(current)) {
|
|
7167
7959
|
ctx.logger.warn(
|
|
7168
7960
|
`legacy community dir still exists at ${legacy} \u2014 remove manually (new location in use)`
|
|
7169
7961
|
);
|
|
7170
7962
|
return;
|
|
7171
7963
|
}
|
|
7172
|
-
|
|
7964
|
+
mkdirSync3(current, { recursive: true });
|
|
7173
7965
|
for (const entry of readdirSync(legacy, { withFileTypes: true })) {
|
|
7174
7966
|
if (!entry.isDirectory()) continue;
|
|
7175
|
-
renameSync(
|
|
7967
|
+
renameSync(join8(legacy, entry.name), join8(current, entry.name));
|
|
7176
7968
|
}
|
|
7177
7969
|
try {
|
|
7178
7970
|
rmdirSync(legacy);
|
|
@@ -7187,7 +7979,7 @@ function runUninstall(ctx, opts) {
|
|
|
7187
7979
|
process.exit(1);
|
|
7188
7980
|
}
|
|
7189
7981
|
const result = uninstallClaudeIntegration({
|
|
7190
|
-
claudeDir:
|
|
7982
|
+
claudeDir: join8(ctx.userHome, ".claude"),
|
|
7191
7983
|
cliScriptPath,
|
|
7192
7984
|
nodePath: resolveHookNodePath(),
|
|
7193
7985
|
dryRun: opts.dryRun,
|
|
@@ -7234,18 +8026,23 @@ function reportInstallResult(ctx, result, dryRun) {
|
|
|
7234
8026
|
}
|
|
7235
8027
|
|
|
7236
8028
|
// src/commands/indexCmd.ts
|
|
7237
|
-
import { existsSync as
|
|
7238
|
-
import { dirname as
|
|
7239
|
-
function runIndex(ctx, opts) {
|
|
7240
|
-
const dbDir =
|
|
7241
|
-
if (!
|
|
8029
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "node:fs";
|
|
8030
|
+
import { dirname as dirname5 } from "node:path";
|
|
8031
|
+
async function runIndex(ctx, opts) {
|
|
8032
|
+
const dbDir = dirname5(ctx.paths.dbPath);
|
|
8033
|
+
if (!existsSync6(dbDir)) mkdirSync4(dbDir, { recursive: true });
|
|
8034
|
+
const keyProvider = createKeyserverKeyProvider({ caveatHome: ctx.caveatHome });
|
|
8035
|
+
const failures = await prewarmSealedKeys({ paths: ctx.paths, keyProvider });
|
|
8036
|
+
for (const failure of failures) {
|
|
8037
|
+
ctx.logger.warn(`${failure.source}: sealed key prewarm failed: ${errorMessage2(failure.error)}`);
|
|
8038
|
+
}
|
|
7242
8039
|
const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
|
|
7243
8040
|
try {
|
|
7244
8041
|
if (opts.full) {
|
|
7245
8042
|
ctx.logger.info("full rebuild: DELETE FROM entries");
|
|
7246
8043
|
rebuildAll(db);
|
|
7247
8044
|
}
|
|
7248
|
-
const result = reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger });
|
|
8045
|
+
const result = reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger, keyProvider });
|
|
7249
8046
|
for (const [source, scan] of Object.entries(result.perSource)) {
|
|
7250
8047
|
if (source === "own") {
|
|
7251
8048
|
ctx.logger.info(`own: +${scan.added} ~${scan.updated} -${scan.deleted}`);
|
|
@@ -7258,6 +8055,9 @@ function runIndex(ctx, opts) {
|
|
|
7258
8055
|
db.close();
|
|
7259
8056
|
}
|
|
7260
8057
|
}
|
|
8058
|
+
function errorMessage2(err) {
|
|
8059
|
+
return err instanceof Error ? err.message : String(err);
|
|
8060
|
+
}
|
|
7261
8061
|
|
|
7262
8062
|
// src/commands/search.ts
|
|
7263
8063
|
function runSearch(ctx, opts) {
|
|
@@ -7405,7 +8205,7 @@ function runStats(ctx) {
|
|
|
7405
8205
|
|
|
7406
8206
|
// src/commands/serve.ts
|
|
7407
8207
|
async function runServe(opts) {
|
|
7408
|
-
const { startServer } = await import("./server-
|
|
8208
|
+
const { startServer } = await import("./server-EASKBCNK.js");
|
|
7409
8209
|
const { port, host } = startServer({ port: opts.port });
|
|
7410
8210
|
process.stdout.write(`[caveat] web portal: http://${host}:${port}/
|
|
7411
8211
|
`);
|
|
@@ -7784,15 +8584,15 @@ var makeIssue = (params) => {
|
|
|
7784
8584
|
message: issueData.message
|
|
7785
8585
|
};
|
|
7786
8586
|
}
|
|
7787
|
-
let
|
|
8587
|
+
let errorMessage6 = "";
|
|
7788
8588
|
const maps = errorMaps.filter((m) => !!m).slice().reverse();
|
|
7789
8589
|
for (const map2 of maps) {
|
|
7790
|
-
|
|
8590
|
+
errorMessage6 = map2(fullIssue, { data, defaultError: errorMessage6 }).message;
|
|
7791
8591
|
}
|
|
7792
8592
|
return {
|
|
7793
8593
|
...issueData,
|
|
7794
8594
|
path: fullPath,
|
|
7795
|
-
message:
|
|
8595
|
+
message: errorMessage6
|
|
7796
8596
|
};
|
|
7797
8597
|
};
|
|
7798
8598
|
function addIssueToContext(ctx, issueData) {
|
|
@@ -27584,19 +28384,19 @@ var getRefs = (options) => {
|
|
|
27584
28384
|
};
|
|
27585
28385
|
|
|
27586
28386
|
// ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@4.4.3/node_modules/zod-to-json-schema/dist/esm/errorMessages.js
|
|
27587
|
-
function addErrorMessage(res, key,
|
|
28387
|
+
function addErrorMessage(res, key, errorMessage6, refs) {
|
|
27588
28388
|
if (!refs?.errorMessages)
|
|
27589
28389
|
return;
|
|
27590
|
-
if (
|
|
28390
|
+
if (errorMessage6) {
|
|
27591
28391
|
res.errorMessage = {
|
|
27592
28392
|
...res.errorMessage,
|
|
27593
|
-
[key]:
|
|
28393
|
+
[key]: errorMessage6
|
|
27594
28394
|
};
|
|
27595
28395
|
}
|
|
27596
28396
|
}
|
|
27597
|
-
function setResponseValueAndErrors(res, key, value,
|
|
28397
|
+
function setResponseValueAndErrors(res, key, value, errorMessage6, refs) {
|
|
27598
28398
|
res[key] = value;
|
|
27599
|
-
addErrorMessage(res, key,
|
|
28399
|
+
addErrorMessage(res, key, errorMessage6, refs);
|
|
27600
28400
|
}
|
|
27601
28401
|
|
|
27602
28402
|
// ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@4.4.3/node_modules/zod-to-json-schema/dist/esm/getRelativePath.js
|
|
@@ -28907,8 +29707,8 @@ var Protocol = class {
|
|
|
28907
29707
|
if (queuedMessage.type === "response") {
|
|
28908
29708
|
resolver(message);
|
|
28909
29709
|
} else {
|
|
28910
|
-
const
|
|
28911
|
-
const error51 = new McpError(
|
|
29710
|
+
const errorMessage6 = message;
|
|
29711
|
+
const error51 = new McpError(errorMessage6.error.code, errorMessage6.error.message, errorMessage6.error.data);
|
|
28912
29712
|
resolver(error51);
|
|
28913
29713
|
}
|
|
28914
29714
|
} else {
|
|
@@ -30208,23 +31008,23 @@ var Server = class extends Protocol {
|
|
|
30208
31008
|
const wrappedHandler = async (request, extra) => {
|
|
30209
31009
|
const validatedRequest = safeParse2(CallToolRequestSchema, request);
|
|
30210
31010
|
if (!validatedRequest.success) {
|
|
30211
|
-
const
|
|
30212
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${
|
|
31011
|
+
const errorMessage6 = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
|
|
31012
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage6}`);
|
|
30213
31013
|
}
|
|
30214
31014
|
const { params } = validatedRequest.data;
|
|
30215
31015
|
const result = await Promise.resolve(handler(request, extra));
|
|
30216
31016
|
if (params.task) {
|
|
30217
31017
|
const taskValidationResult = safeParse2(CreateTaskResultSchema, result);
|
|
30218
31018
|
if (!taskValidationResult.success) {
|
|
30219
|
-
const
|
|
30220
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${
|
|
31019
|
+
const errorMessage6 = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
|
|
31020
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage6}`);
|
|
30221
31021
|
}
|
|
30222
31022
|
return taskValidationResult.data;
|
|
30223
31023
|
}
|
|
30224
31024
|
const validationResult = safeParse2(CallToolResultSchema, result);
|
|
30225
31025
|
if (!validationResult.success) {
|
|
30226
|
-
const
|
|
30227
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${
|
|
31026
|
+
const errorMessage6 = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
|
|
31027
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage6}`);
|
|
30228
31028
|
}
|
|
30229
31029
|
return validationResult.data;
|
|
30230
31030
|
};
|
|
@@ -30718,12 +31518,12 @@ var McpServer = class {
|
|
|
30718
31518
|
* @param errorMessage - The error message.
|
|
30719
31519
|
* @returns The tool error result.
|
|
30720
31520
|
*/
|
|
30721
|
-
createToolError(
|
|
31521
|
+
createToolError(errorMessage6) {
|
|
30722
31522
|
return {
|
|
30723
31523
|
content: [
|
|
30724
31524
|
{
|
|
30725
31525
|
type: "text",
|
|
30726
|
-
text:
|
|
31526
|
+
text: errorMessage6
|
|
30727
31527
|
}
|
|
30728
31528
|
],
|
|
30729
31529
|
isError: true
|
|
@@ -30741,8 +31541,8 @@ var McpServer = class {
|
|
|
30741
31541
|
const parseResult = await safeParseAsync2(schemaToParse, args);
|
|
30742
31542
|
if (!parseResult.success) {
|
|
30743
31543
|
const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
30744
|
-
const
|
|
30745
|
-
throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${
|
|
31544
|
+
const errorMessage6 = getParseErrorMessage(error51);
|
|
31545
|
+
throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage6}`);
|
|
30746
31546
|
}
|
|
30747
31547
|
return parseResult.data;
|
|
30748
31548
|
}
|
|
@@ -30766,8 +31566,8 @@ var McpServer = class {
|
|
|
30766
31566
|
const parseResult = await safeParseAsync2(outputObj, result.structuredContent);
|
|
30767
31567
|
if (!parseResult.success) {
|
|
30768
31568
|
const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
30769
|
-
const
|
|
30770
|
-
throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${
|
|
31569
|
+
const errorMessage6 = getParseErrorMessage(error51);
|
|
31570
|
+
throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage6}`);
|
|
30771
31571
|
}
|
|
30772
31572
|
}
|
|
30773
31573
|
/**
|
|
@@ -30979,8 +31779,8 @@ var McpServer = class {
|
|
|
30979
31779
|
const parseResult = await safeParseAsync2(argsObj, request.params.arguments);
|
|
30980
31780
|
if (!parseResult.success) {
|
|
30981
31781
|
const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
30982
|
-
const
|
|
30983
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${
|
|
31782
|
+
const errorMessage6 = getParseErrorMessage(error51);
|
|
31783
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage6}`);
|
|
30984
31784
|
}
|
|
30985
31785
|
const args = parseResult.data;
|
|
30986
31786
|
const cb = prompt.callback;
|
|
@@ -31477,11 +32277,11 @@ var StdioServerTransport = class {
|
|
|
31477
32277
|
|
|
31478
32278
|
// ../mcp/dist/context.js
|
|
31479
32279
|
import { homedir as homedir2 } from "node:os";
|
|
31480
|
-
import { join as
|
|
32280
|
+
import { join as join9 } from "node:path";
|
|
31481
32281
|
function buildMcpContext(overrides = {}) {
|
|
31482
32282
|
const userHome = overrides.userHome ?? homedir2();
|
|
31483
32283
|
const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
|
|
31484
|
-
const userConfigPath =
|
|
32284
|
+
const userConfigPath = join9(userHome, ".caveatrc.json");
|
|
31485
32285
|
const logger = overrides.logger ?? stderrLogger;
|
|
31486
32286
|
const config2 = loadConfig(userConfigPath);
|
|
31487
32287
|
const paths = resolvePaths(caveatHome, config2.knowledgeRepo, userHome);
|
|
@@ -31494,7 +32294,9 @@ var sourceFilter = external_exports.enum(["own", "community", "all"]);
|
|
|
31494
32294
|
var confidenceSchema = external_exports.enum(["confirmed", "reproduced", "tentative"]);
|
|
31495
32295
|
var visibilityFilter = external_exports.enum(["public", "private", "all"]);
|
|
31496
32296
|
var searchInputShape = {
|
|
31497
|
-
query: external_exports.string().describe(
|
|
32297
|
+
query: external_exports.string().describe(
|
|
32298
|
+
"FTS query (3+ chars for trigram). Empty string lists without text filter. If a search returns 0 hits, retry with synonyms and Japanese/English paraphrases. Use plain tokens only; FTS operators are not supported."
|
|
32299
|
+
),
|
|
31498
32300
|
filters: external_exports.object({
|
|
31499
32301
|
tags: external_exports.array(external_exports.string()).optional(),
|
|
31500
32302
|
confidence: external_exports.array(confidenceSchema).optional(),
|
|
@@ -31543,7 +32345,9 @@ var outcomeSchema = external_exports.enum(["resolved", "impossible"]);
|
|
|
31543
32345
|
var visibilitySchema = external_exports.enum(["public", "private"]);
|
|
31544
32346
|
var recordInputShape = {
|
|
31545
32347
|
title: external_exports.string().min(1),
|
|
31546
|
-
symptom: external_exports.string().min(1)
|
|
32348
|
+
symptom: external_exports.string().min(1).describe(
|
|
32349
|
+
"Describe the observed failure. Preserve raw errors and exact error strings verbatim; when a stable translation is known, also include the main symptom keywords in Japanese and English to improve retrieval. Do not force a full translation or guess at uncertain wording."
|
|
32350
|
+
),
|
|
31547
32351
|
cause: external_exports.string().optional(),
|
|
31548
32352
|
resolution: external_exports.string().optional(),
|
|
31549
32353
|
evidence: external_exports.string().optional(),
|
|
@@ -31592,14 +32396,19 @@ var patchFrontmatterSchema = external_exports.object({
|
|
|
31592
32396
|
});
|
|
31593
32397
|
var updateInputShape = {
|
|
31594
32398
|
id: external_exports.string(),
|
|
31595
|
-
source: external_exports.string().optional(),
|
|
32399
|
+
source: external_exports.string().optional().describe("Only own entries may be updated. community entries are subscriptions; edit them upstream."),
|
|
31596
32400
|
patch: external_exports.object({
|
|
31597
32401
|
frontmatter: patchFrontmatterSchema.optional(),
|
|
31598
|
-
sections: external_exports.record(external_exports.string(), external_exports.string()).optional()
|
|
32402
|
+
sections: external_exports.record(external_exports.string(), external_exports.string()).optional().describe(
|
|
32403
|
+
"Patch H2 sections by heading. When changing the Symptom section, preserve raw errors and exact error strings verbatim; when a stable translation is known, also include the main symptom keywords in Japanese and English to improve retrieval. Do not force a full translation or guess at uncertain wording."
|
|
32404
|
+
)
|
|
31599
32405
|
})
|
|
31600
32406
|
};
|
|
31601
32407
|
function handleUpdate(ctx, args) {
|
|
31602
32408
|
const source = args.source ?? "own";
|
|
32409
|
+
if (source !== "own") {
|
|
32410
|
+
throw new Error("community \u30A8\u30F3\u30C8\u30EA\u306F\u8CFC\u8AAD\u7269\u3067\u3059; \u7DE8\u96C6\u306F\u4E0A\u6D41\u3067\u884C\u3063\u3066\u304F\u3060\u3055\u3044");
|
|
32411
|
+
}
|
|
31603
32412
|
return updateEntry(args.id, args.patch, {
|
|
31604
32413
|
db: ctx.db,
|
|
31605
32414
|
entriesRoot: ctx.paths.entriesDir,
|
|
@@ -31616,13 +32425,12 @@ function handleListRecent(ctx, args) {
|
|
|
31616
32425
|
}
|
|
31617
32426
|
|
|
31618
32427
|
// ../mcp/dist/tools/pull.js
|
|
31619
|
-
import { existsSync as
|
|
31620
|
-
import { join as join7 } from "node:path";
|
|
32428
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
31621
32429
|
var pullInputShape = {};
|
|
31622
32430
|
async function handlePull(ctx, _args = {}) {
|
|
31623
32431
|
const pulled = [];
|
|
31624
32432
|
const indexed = [];
|
|
31625
|
-
if (
|
|
32433
|
+
if (existsSync7(ctx.paths.communityDir)) {
|
|
31626
32434
|
const results = await communityPull({
|
|
31627
32435
|
communityDir: ctx.paths.communityDir,
|
|
31628
32436
|
logger: ctx.logger
|
|
@@ -31631,23 +32439,25 @@ async function handlePull(ctx, _args = {}) {
|
|
|
31631
32439
|
pulled.push({ handle: r.handle, status: r.status, message: r.message });
|
|
31632
32440
|
}
|
|
31633
32441
|
}
|
|
31634
|
-
|
|
31635
|
-
|
|
31636
|
-
|
|
31637
|
-
|
|
32442
|
+
const keyProvider = createKeyserverKeyProvider({ caveatHome: ctx.caveatHome });
|
|
32443
|
+
const failures = await prewarmSealedKeys({ paths: ctx.paths, keyProvider });
|
|
32444
|
+
for (const failure of failures) {
|
|
32445
|
+
ctx.logger.warn(`${failure.source}: sealed key prewarm failed: ${errorMessage3(failure.error)}`);
|
|
31638
32446
|
}
|
|
31639
|
-
|
|
31640
|
-
|
|
31641
|
-
|
|
31642
|
-
|
|
31643
|
-
|
|
31644
|
-
|
|
31645
|
-
|
|
31646
|
-
|
|
31647
|
-
}
|
|
32447
|
+
const result = reindexAllSources({
|
|
32448
|
+
db: ctx.db,
|
|
32449
|
+
paths: ctx.paths,
|
|
32450
|
+
logger: ctx.logger,
|
|
32451
|
+
keyProvider
|
|
32452
|
+
});
|
|
32453
|
+
for (const [source, scan] of Object.entries(result.perSource)) {
|
|
32454
|
+
indexed.push({ source, ...scan });
|
|
31648
32455
|
}
|
|
31649
32456
|
return { pulled, indexed };
|
|
31650
32457
|
}
|
|
32458
|
+
function errorMessage3(err) {
|
|
32459
|
+
return err instanceof Error ? err.message : String(err);
|
|
32460
|
+
}
|
|
31651
32461
|
|
|
31652
32462
|
// ../mcp/dist/registerTools.js
|
|
31653
32463
|
function jsonResult(data) {
|
|
@@ -31665,7 +32475,7 @@ function registerAllTools(server, ctx) {
|
|
|
31665
32475
|
"caveat_search",
|
|
31666
32476
|
{
|
|
31667
32477
|
title: "caveat_search",
|
|
31668
|
-
description: 'Search the "caveats" knowledge base \u2014 records of time-wasting traps
|
|
32478
|
+
description: 'Search the "caveats" knowledge base \u2014 records of time-wasting traps someone already diagnosed, including external-spec issues (GPU/driver/CUDA versions, native-module builds, IDE/shell quirks, platform-specific behavior, library version incompatibilities) and reusable repo-specific context. Call this FIRST when a problem smells like a known trap \u2014 before reading stack traces top-to-bottom or trying fixes. Query: 3+ chars, plain tokens only (no OR/NEAR/other FTS5 operators). If it returns 0 hits, retry with synonyms and Japanese/English paraphrases. Returns summary rows including `{id, source}` \u2014 pass BOTH to caveat_get for the full body.',
|
|
31669
32479
|
inputSchema: searchInputShape
|
|
31670
32480
|
},
|
|
31671
32481
|
async (args) => jsonResult(handleSearch(ctx, args))
|
|
@@ -31683,7 +32493,7 @@ function registerAllTools(server, ctx) {
|
|
|
31683
32493
|
"caveat_record",
|
|
31684
32494
|
{
|
|
31685
32495
|
title: "caveat_record",
|
|
31686
|
-
description: "Create a new caveat: a record of
|
|
32496
|
+
description: "Create a new caveat: a reusable record of a time-wasting trap (wrong driver, version mismatch, platform bug, IDE quirk, native-module issue, or repo-specific context) so future sessions can find it via caveat_search. REQUIRED BEFORE CALLING: run caveat_search first to avoid duplicates. Set visibility using this binary criterion: `public` if a third party could reproduce it; `private` if it is repo/workflow-specific or depends on intentional local design; when unclear, prefer `private`; follow an explicit user visibility choice. Qualifies: specific symptom + diagnosed cause (or `outcome: impossible` verdict) + environment fingerprint. Does NOT qualify: ordinary project-internal logic bugs, user preferences, session summaries, or ephemeral task notes; reusable repo-specific traps may be `private`. Auto-fills source_session and environment defaults; source_project is left null by design (shared knowledge must not leak per-user project names).",
|
|
31687
32497
|
inputSchema: recordInputShape
|
|
31688
32498
|
},
|
|
31689
32499
|
async (args) => jsonResult(handleRecord(ctx, args))
|
|
@@ -31692,7 +32502,7 @@ function registerAllTools(server, ctx) {
|
|
|
31692
32502
|
"caveat_update",
|
|
31693
32503
|
{
|
|
31694
32504
|
title: "caveat_update",
|
|
31695
|
-
description: "Patch an existing caveat \u2014 use when newer evidence extends or corrects one that already exists. Frontmatter shallow-merges, but array fields (tags etc.) REPLACE rather than append \u2014 to add one tag, read the current list first, then patch with the full new array. Sections match by case-insensitive H2 heading. Immutable keys: id, created_at, source_session, source_project. Common uses: bump `last_verified` after re-confirming, add a resolution when it was `tentative`, flip `outcome` to `impossible`.",
|
|
32505
|
+
description: "Patch an existing caveat \u2014 use when newer evidence extends or corrects one that already exists. Frontmatter shallow-merges, but array fields (tags etc.) REPLACE rather than append \u2014 to add one tag, read the current list first, then patch with the full new array. Sections match by case-insensitive H2 heading. When changing `Symptom`, preserve raw errors verbatim and add stable Japanese/English symptom keywords when known; do not force or guess translations. Immutable keys: id, created_at, source_session, source_project. Common uses: bump `last_verified` after re-confirming, add a resolution when it was `tentative`, flip `outcome` to `impossible`.",
|
|
31696
32506
|
inputSchema: updateInputShape
|
|
31697
32507
|
},
|
|
31698
32508
|
async (args) => jsonResult(handleUpdate(ctx, args))
|
|
@@ -31754,28 +32564,31 @@ async function runMcpServer() {
|
|
|
31754
32564
|
}
|
|
31755
32565
|
|
|
31756
32566
|
// src/commands/hookCmd.ts
|
|
31757
|
-
import { spawn as
|
|
32567
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
31758
32568
|
import {
|
|
31759
|
-
|
|
31760
|
-
|
|
31761
|
-
|
|
31762
|
-
|
|
31763
|
-
|
|
31764
|
-
|
|
31765
|
-
|
|
32569
|
+
chmodSync,
|
|
32570
|
+
existsSync as existsSync10,
|
|
32571
|
+
lstatSync,
|
|
32572
|
+
mkdtempSync as mkdtempSync2,
|
|
32573
|
+
mkdirSync as mkdirSync5,
|
|
32574
|
+
readdirSync as readdirSync2,
|
|
32575
|
+
readFileSync as readFileSync5,
|
|
32576
|
+
realpathSync as realpathSync2,
|
|
32577
|
+
rmSync as rmSync2,
|
|
32578
|
+
writeFileSync as writeFileSync5
|
|
31766
32579
|
} from "node:fs";
|
|
31767
|
-
import { tmpdir } from "node:os";
|
|
31768
|
-
import { join as
|
|
32580
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
32581
|
+
import { basename, dirname as dirname6, join as join12 } from "node:path";
|
|
31769
32582
|
import { createHash, randomBytes } from "node:crypto";
|
|
31770
32583
|
|
|
31771
32584
|
// src/autoReindexTrigger.ts
|
|
31772
32585
|
import { spawn } from "node:child_process";
|
|
31773
|
-
import { existsSync as
|
|
31774
|
-
import { join as
|
|
32586
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
32587
|
+
import { join as join10 } from "node:path";
|
|
31775
32588
|
function maybeTriggerAutoReindex(ctx) {
|
|
31776
32589
|
if (process.env.CAVEAT_INDEX_AUTOSYNC === "off") return;
|
|
31777
|
-
if (!
|
|
31778
|
-
if (
|
|
32590
|
+
if (!existsSync8(ctx.paths.dbPath)) return;
|
|
32591
|
+
if (existsSync8(join10(ctx.caveatHome, "index", ".reindex-lock"))) return;
|
|
31779
32592
|
const current = computeEntriesDigest(ctx.paths);
|
|
31780
32593
|
const marker = readDigestMarker(ctx.caveatHome);
|
|
31781
32594
|
if (marker?.digest === current.digest && marker.fileCount === current.fileCount) return;
|
|
@@ -31791,6 +32604,28 @@ function maybeTriggerAutoReindex(ctx) {
|
|
|
31791
32604
|
child.unref();
|
|
31792
32605
|
}
|
|
31793
32606
|
|
|
32607
|
+
// src/autoSyncTrigger.ts
|
|
32608
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
32609
|
+
import { existsSync as existsSync9, statSync } from "node:fs";
|
|
32610
|
+
import { join as join11 } from "node:path";
|
|
32611
|
+
function maybeTriggerAutoSync(ctx) {
|
|
32612
|
+
if (process.env[CAVEAT_AUTO_SYNC_ENV] === "off") return;
|
|
32613
|
+
const statePath = join11(ctx.caveatHome, "sync", ".last-autosync.json");
|
|
32614
|
+
const debounceMs = Number(process.env.CAVEAT_AUTO_SYNC_DEBOUNCE_MS ?? AUTO_SYNC_DEBOUNCE_MS);
|
|
32615
|
+
try {
|
|
32616
|
+
if (existsSync9(statePath) && Date.now() - statSync(statePath).mtimeMs < debounceMs) return;
|
|
32617
|
+
} catch {
|
|
32618
|
+
}
|
|
32619
|
+
const cliScript = process.argv[1];
|
|
32620
|
+
if (!cliScript) throw new Error("current Caveat CLI script path is unavailable");
|
|
32621
|
+
const child = spawn2(
|
|
32622
|
+
process.execPath,
|
|
32623
|
+
[...process.execArgv, "--disable-warning=ExperimentalWarning", cliScript, "hook", "autosync"],
|
|
32624
|
+
{ detached: true, stdio: "ignore", windowsHide: true }
|
|
32625
|
+
);
|
|
32626
|
+
child.unref();
|
|
32627
|
+
}
|
|
32628
|
+
|
|
31794
32629
|
// src/commands/hookCmd.ts
|
|
31795
32630
|
var silentLogger = {
|
|
31796
32631
|
info: () => {
|
|
@@ -31835,31 +32670,44 @@ function buildContextSafely() {
|
|
|
31835
32670
|
return null;
|
|
31836
32671
|
}
|
|
31837
32672
|
}
|
|
31838
|
-
function searchCaveatsFromTextSafely(text) {
|
|
32673
|
+
function searchCaveatsFromTextSafely(text, surface) {
|
|
31839
32674
|
if (!text) return [];
|
|
31840
32675
|
let db;
|
|
32676
|
+
let caveatHome;
|
|
32677
|
+
let hits;
|
|
31841
32678
|
try {
|
|
31842
32679
|
const ctx = buildContextSafely();
|
|
31843
|
-
if (!ctx || !
|
|
32680
|
+
if (!ctx || !existsSync10(ctx.paths.dbPath)) return [];
|
|
32681
|
+
caveatHome = ctx.caveatHome;
|
|
31844
32682
|
db = openDb({ path: ctx.paths.dbPath });
|
|
31845
|
-
|
|
32683
|
+
hits = findCaveatsForPrompt(db, text, {
|
|
31846
32684
|
selfIdentity: defaultSelfIdentityTokens()
|
|
31847
32685
|
});
|
|
31848
|
-
if (hits.length > 0) {
|
|
31849
|
-
try {
|
|
31850
|
-
markHit(db, hits);
|
|
31851
|
-
} catch (err) {
|
|
31852
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
31853
|
-
process.stderr.write(`[caveat:hook] markHit error: ${msg}
|
|
31854
|
-
`);
|
|
31855
|
-
}
|
|
31856
|
-
}
|
|
31857
|
-
return hits;
|
|
31858
32686
|
} catch (err) {
|
|
31859
32687
|
const msg = err instanceof Error ? err.message : String(err);
|
|
31860
32688
|
process.stderr.write(`[caveat:hook] search error: ${msg}
|
|
31861
32689
|
`);
|
|
31862
32690
|
return [];
|
|
32691
|
+
}
|
|
32692
|
+
if (hits.length > 0) {
|
|
32693
|
+
try {
|
|
32694
|
+
markHit(db, hits);
|
|
32695
|
+
} catch (err) {
|
|
32696
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32697
|
+
process.stderr.write(`[caveat:hook] markHit error: ${msg}
|
|
32698
|
+
`);
|
|
32699
|
+
}
|
|
32700
|
+
} else {
|
|
32701
|
+
try {
|
|
32702
|
+
logHookQueryMiss({ caveatHome, agent: "claude", surface, query: text });
|
|
32703
|
+
} catch (err) {
|
|
32704
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32705
|
+
process.stderr.write(`[caveat:hook] query log error: ${msg}
|
|
32706
|
+
`);
|
|
32707
|
+
}
|
|
32708
|
+
}
|
|
32709
|
+
try {
|
|
32710
|
+
return hits;
|
|
31863
32711
|
} finally {
|
|
31864
32712
|
db?.close();
|
|
31865
32713
|
}
|
|
@@ -31875,12 +32723,15 @@ function loadSignalsSafely(path) {
|
|
|
31875
32723
|
}
|
|
31876
32724
|
}
|
|
31877
32725
|
function systemReminderOutput(text) {
|
|
31878
|
-
return `<system-reminder>${text}</system-reminder>`;
|
|
32726
|
+
return `<system-reminder>${text.replace(/</g, "\u2039").replace(/>/g, "\u203A")}</system-reminder>`;
|
|
31879
32727
|
}
|
|
31880
32728
|
function drainForSession(sessionId) {
|
|
31881
32729
|
const ctx = buildContextSafely();
|
|
31882
32730
|
if (!ctx) return [];
|
|
31883
|
-
return
|
|
32731
|
+
return [
|
|
32732
|
+
...drainPendingReminders(ctx.caveatHome, sessionId),
|
|
32733
|
+
...drainGlobalPendingReminders(ctx.caveatHome)
|
|
32734
|
+
];
|
|
31884
32735
|
}
|
|
31885
32736
|
function claudeContextDedupeKey(text) {
|
|
31886
32737
|
if (text.startsWith(CLAUDE_STOP_REMINDER_PREFIX)) return "claude-stop-reminder";
|
|
@@ -31924,20 +32775,20 @@ function stopSignalKey(signals, related) {
|
|
|
31924
32775
|
return createHash("sha256").update(body).digest("hex");
|
|
31925
32776
|
}
|
|
31926
32777
|
function stopStatePath(caveatHome, sessionId) {
|
|
31927
|
-
return
|
|
32778
|
+
return join12(caveatHome, CLAUDE_STOP_STATE_DIR, `${sanitizeClaudeStateId(sessionId)}.txt`);
|
|
31928
32779
|
}
|
|
31929
32780
|
function wasStopReminderQueued(caveatHome, sessionId, key) {
|
|
31930
32781
|
const path = stopStatePath(caveatHome, sessionId);
|
|
31931
32782
|
try {
|
|
31932
|
-
return
|
|
32783
|
+
return readFileSync5(path, "utf-8") === key;
|
|
31933
32784
|
} catch {
|
|
31934
32785
|
return false;
|
|
31935
32786
|
}
|
|
31936
32787
|
}
|
|
31937
32788
|
function markStopReminderQueued(caveatHome, sessionId, key) {
|
|
31938
32789
|
const path = stopStatePath(caveatHome, sessionId);
|
|
31939
|
-
|
|
31940
|
-
|
|
32790
|
+
mkdirSync5(join12(caveatHome, CLAUDE_STOP_STATE_DIR), { recursive: true });
|
|
32791
|
+
writeFileSync5(path, key, "utf-8");
|
|
31941
32792
|
}
|
|
31942
32793
|
function queueStopForSession(sessionId, signals, related) {
|
|
31943
32794
|
const ctx = buildContextSafely();
|
|
@@ -31986,23 +32837,34 @@ function isToolError(payload) {
|
|
|
31986
32837
|
if (typeof payload.error === "string" && payload.error.length > 0) return true;
|
|
31987
32838
|
return false;
|
|
31988
32839
|
}
|
|
32840
|
+
var WORKER_STALE_MS = 24 * 60 * 60 * 1e3;
|
|
32841
|
+
var WORKER_ROOT = "caveat-worker-v1";
|
|
32842
|
+
var WORKER_MARKER = ".caveat-worker-schema-v1";
|
|
31989
32843
|
function spawnWorker(job) {
|
|
31990
|
-
|
|
31991
|
-
|
|
31992
|
-
|
|
31993
|
-
);
|
|
32844
|
+
let root;
|
|
32845
|
+
let workDir;
|
|
32846
|
+
let workFile;
|
|
31994
32847
|
try {
|
|
31995
|
-
|
|
32848
|
+
root = workerRoot();
|
|
32849
|
+
sweepStaleWorkerDirs(Date.now(), root);
|
|
32850
|
+
workDir = mkdtempSync2(join12(root, "job-"));
|
|
32851
|
+
chmodSync(workDir, 448);
|
|
32852
|
+
workFile = join12(workDir, `${randomBytes(4).toString("hex")}.json`);
|
|
32853
|
+
writeFileSync5(workFile, JSON.stringify({ ...job, schemaVersion: "caveat-worker-job/v1" }), { encoding: "utf-8", mode: 384, flag: "wx" });
|
|
31996
32854
|
} catch (err) {
|
|
31997
32855
|
const msg = err instanceof Error ? err.message : String(err);
|
|
31998
32856
|
process.stderr.write(`[caveat:hook] worker writefile error: ${msg}
|
|
31999
32857
|
`);
|
|
32858
|
+
cleanupWorkerDir(workDir, workDir ? dirname6(workDir) : void 0);
|
|
32000
32859
|
return;
|
|
32001
32860
|
}
|
|
32002
32861
|
const cliScript = process.argv[1];
|
|
32003
|
-
if (!cliScript)
|
|
32862
|
+
if (!cliScript) {
|
|
32863
|
+
cleanupWorkerDir(workDir, root);
|
|
32864
|
+
return;
|
|
32865
|
+
}
|
|
32004
32866
|
try {
|
|
32005
|
-
const child =
|
|
32867
|
+
const child = spawn3(
|
|
32006
32868
|
process.execPath,
|
|
32007
32869
|
["--disable-warning=ExperimentalWarning", cliScript, "hook", "worker", workFile],
|
|
32008
32870
|
{ detached: true, stdio: "ignore", windowsHide: true }
|
|
@@ -32013,7 +32875,7 @@ function spawnWorker(job) {
|
|
|
32013
32875
|
process.stderr.write(`[caveat:hook] worker spawn error: ${msg}
|
|
32014
32876
|
`);
|
|
32015
32877
|
try {
|
|
32016
|
-
|
|
32878
|
+
cleanupWorkerDir(workDir, root);
|
|
32017
32879
|
} catch {
|
|
32018
32880
|
}
|
|
32019
32881
|
}
|
|
@@ -32021,14 +32883,11 @@ function spawnWorker(job) {
|
|
|
32021
32883
|
async function runWorker(workFile) {
|
|
32022
32884
|
let raw;
|
|
32023
32885
|
try {
|
|
32024
|
-
raw =
|
|
32886
|
+
raw = readFileSync5(workFile, "utf-8");
|
|
32025
32887
|
} catch {
|
|
32026
32888
|
process.exit(0);
|
|
32027
32889
|
}
|
|
32028
|
-
|
|
32029
|
-
unlinkSync(workFile);
|
|
32030
|
-
} catch {
|
|
32031
|
-
}
|
|
32890
|
+
cleanupWorkerDir(dirname6(workFile), dirname6(dirname6(workFile)));
|
|
32032
32891
|
let job;
|
|
32033
32892
|
try {
|
|
32034
32893
|
job = JSON.parse(raw);
|
|
@@ -32036,7 +32895,7 @@ async function runWorker(workFile) {
|
|
|
32036
32895
|
process.exit(0);
|
|
32037
32896
|
}
|
|
32038
32897
|
if (!job.searchText || !job.sessionId) process.exit(0);
|
|
32039
|
-
const hits = searchCaveatsFromTextSafely(job.searchText);
|
|
32898
|
+
const hits = searchCaveatsFromTextSafely(job.searchText, "tool_error");
|
|
32040
32899
|
if (hits.length === 0) process.exit(0);
|
|
32041
32900
|
const ctx = buildContextSafely();
|
|
32042
32901
|
if (!ctx) process.exit(0);
|
|
@@ -32046,22 +32905,101 @@ async function runWorker(workFile) {
|
|
|
32046
32905
|
}
|
|
32047
32906
|
process.exit(0);
|
|
32048
32907
|
}
|
|
32908
|
+
function cleanupWorkerDir(workDir, root) {
|
|
32909
|
+
if (!workDir || !root || !isOwnedWorkerDir(workDir, root)) return;
|
|
32910
|
+
try {
|
|
32911
|
+
rmSync2(workDir, { recursive: true, force: true });
|
|
32912
|
+
} catch {
|
|
32913
|
+
}
|
|
32914
|
+
}
|
|
32915
|
+
function isOwnedWorkerDir(path, root = workerRoot()) {
|
|
32916
|
+
try {
|
|
32917
|
+
const inputStat = lstatSync(path);
|
|
32918
|
+
if (inputStat.isSymbolicLink()) return false;
|
|
32919
|
+
const tmpRoot = realpathSync2(root);
|
|
32920
|
+
const resolved = realpathSync2(path);
|
|
32921
|
+
const stat = lstatSync(resolved);
|
|
32922
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
32923
|
+
return dirname6(resolved) === tmpRoot && basename(resolved).startsWith("job-") && stat.isDirectory() && !stat.isSymbolicLink() && hasPrivateOwnership(stat, uid);
|
|
32924
|
+
} catch {
|
|
32925
|
+
return false;
|
|
32926
|
+
}
|
|
32927
|
+
}
|
|
32928
|
+
function sweepStaleWorkerDirs(now = Date.now(), root = workerRoot()) {
|
|
32929
|
+
let entries;
|
|
32930
|
+
try {
|
|
32931
|
+
entries = readdirSync2(root);
|
|
32932
|
+
} catch {
|
|
32933
|
+
process.stderr.write("[caveat:hook] worker stale cleanup failed\n");
|
|
32934
|
+
return;
|
|
32935
|
+
}
|
|
32936
|
+
for (const entry of entries) {
|
|
32937
|
+
if (!entry.startsWith("job-")) continue;
|
|
32938
|
+
const path = join12(root, entry);
|
|
32939
|
+
try {
|
|
32940
|
+
if (!isOwnedWorkerDir(path, root)) continue;
|
|
32941
|
+
const stat = lstatSync(path);
|
|
32942
|
+
if (now - stat.mtimeMs <= WORKER_STALE_MS || !isStaleWorkerJobDir(path)) continue;
|
|
32943
|
+
rmSync2(path, { recursive: true, force: true });
|
|
32944
|
+
} catch {
|
|
32945
|
+
process.stderr.write("[caveat:hook] worker stale cleanup failed\n");
|
|
32946
|
+
}
|
|
32947
|
+
}
|
|
32948
|
+
}
|
|
32949
|
+
function workerRoot(base = tmpdir2()) {
|
|
32950
|
+
const root = join12(base, WORKER_ROOT);
|
|
32951
|
+
mkdirSync5(root, { recursive: true, mode: 448 });
|
|
32952
|
+
const stat = lstatSync(root);
|
|
32953
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
32954
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || !hasPrivateOwnership(stat, uid)) throw new Error("worker root is unsafe");
|
|
32955
|
+
const marker = join12(root, WORKER_MARKER);
|
|
32956
|
+
try {
|
|
32957
|
+
writeFileSync5(marker, "caveat-worker/v1\n", { mode: 384, flag: "wx" });
|
|
32958
|
+
} catch (error51) {
|
|
32959
|
+
if (!(error51 && typeof error51 === "object" && "code" in error51 && error51.code === "EEXIST")) throw error51;
|
|
32960
|
+
}
|
|
32961
|
+
const markerStat = lstatSync(marker);
|
|
32962
|
+
if (!markerStat.isFile() || markerStat.isSymbolicLink() || !hasPrivateOwnership(markerStat, uid) || readFileSync5(marker, "utf-8") !== "caveat-worker/v1\n") throw new Error("worker root marker is invalid");
|
|
32963
|
+
return root;
|
|
32964
|
+
}
|
|
32965
|
+
function isStaleWorkerJobDir(path) {
|
|
32966
|
+
const entries = readdirSync2(path);
|
|
32967
|
+
if (entries.length !== 1 || !entries[0].endsWith(".json")) return false;
|
|
32968
|
+
const file2 = join12(path, entries[0]);
|
|
32969
|
+
const stat = lstatSync(file2);
|
|
32970
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
32971
|
+
if (!stat.isFile() || stat.isSymbolicLink() || !hasPrivateOwnership(stat, uid)) return false;
|
|
32972
|
+
return isWorkerJob(JSON.parse(readFileSync5(file2, "utf-8")));
|
|
32973
|
+
}
|
|
32974
|
+
function hasPrivateOwnership(stat, uid) {
|
|
32975
|
+
return process.platform === "win32" || (stat.mode & 63) === 0 && (uid === void 0 || stat.uid === uid);
|
|
32976
|
+
}
|
|
32977
|
+
function isWorkerJob(value) {
|
|
32978
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
32979
|
+
const job = value;
|
|
32980
|
+
const keys = Object.keys(job).sort();
|
|
32981
|
+
if (!(keys.length === 3 && keys.join(",") === "schemaVersion,searchText,sessionId") && !(keys.length === 4 && keys.join(",") === "additionalContext,schemaVersion,searchText,sessionId")) return false;
|
|
32982
|
+
return job.schemaVersion === "caveat-worker-job/v1" && typeof job.sessionId === "string" && typeof job.searchText === "string" && (job.additionalContext === void 0 || job.additionalContext !== null && typeof job.additionalContext === "object" && !Array.isArray(job.additionalContext));
|
|
32983
|
+
}
|
|
32049
32984
|
function writeLastReindex(caveatHome, value) {
|
|
32050
32985
|
try {
|
|
32051
|
-
|
|
32986
|
+
writeFileSync5(join12(caveatHome, "index", ".last-reindex.json"), JSON.stringify(value), "utf-8");
|
|
32052
32987
|
} catch (err) {
|
|
32053
32988
|
const msg = err instanceof Error ? err.message : String(err);
|
|
32054
32989
|
process.stderr.write(`[caveat:hook] reindex status write error: ${msg}
|
|
32055
32990
|
`);
|
|
32056
32991
|
}
|
|
32057
32992
|
}
|
|
32058
|
-
function
|
|
32993
|
+
function errorMessage4(err) {
|
|
32994
|
+
return err instanceof Error ? err.message : String(err);
|
|
32995
|
+
}
|
|
32996
|
+
async function runReindexWorker() {
|
|
32059
32997
|
if (process.env.CAVEAT_INDEX_AUTOSYNC === "off") {
|
|
32060
32998
|
process.stderr.write("[caveat:hook] auto reindex disabled by CAVEAT_INDEX_AUTOSYNC=off\n");
|
|
32061
32999
|
return;
|
|
32062
33000
|
}
|
|
32063
33001
|
const ctx = buildContextSafely();
|
|
32064
|
-
if (!ctx || !
|
|
33002
|
+
if (!ctx || !existsSync10(ctx.paths.dbPath)) {
|
|
32065
33003
|
process.stderr.write("[caveat:hook] auto reindex skipped: index database does not exist\n");
|
|
32066
33004
|
return;
|
|
32067
33005
|
}
|
|
@@ -32071,8 +33009,14 @@ function runReindexWorker() {
|
|
|
32071
33009
|
let db;
|
|
32072
33010
|
try {
|
|
32073
33011
|
const digest = computeEntriesDigest(ctx.paths);
|
|
33012
|
+
const keyProvider = createKeyserverKeyProvider({ caveatHome: ctx.caveatHome });
|
|
33013
|
+
const failures = await prewarmSealedKeys({ paths: ctx.paths, keyProvider });
|
|
33014
|
+
for (const failure of failures) {
|
|
33015
|
+
process.stderr.write(`[caveat:hook] ${failure.source}: sealed key prewarm failed: ${errorMessage4(failure.error)}
|
|
33016
|
+
`);
|
|
33017
|
+
}
|
|
32074
33018
|
db = openDb({ path: ctx.paths.dbPath, logger: silentLogger });
|
|
32075
|
-
const result = reindexAllSources({ db, paths: ctx.paths, logger: silentLogger });
|
|
33019
|
+
const result = reindexAllSources({ db, paths: ctx.paths, logger: silentLogger, keyProvider });
|
|
32076
33020
|
writeDigestMarker(ctx.caveatHome, digest);
|
|
32077
33021
|
writeLastReindex(ctx.caveatHome, {
|
|
32078
33022
|
startedAt,
|
|
@@ -32099,12 +33043,31 @@ function runReindexWorker() {
|
|
|
32099
33043
|
}
|
|
32100
33044
|
}
|
|
32101
33045
|
}
|
|
33046
|
+
async function runAutoSyncWorker() {
|
|
33047
|
+
if (process.env[CAVEAT_AUTO_SYNC_ENV] === "off") {
|
|
33048
|
+
process.stderr.write("[caveat:hook] auto sync disabled by CAVEAT_AUTO_SYNC=off\n");
|
|
33049
|
+
return;
|
|
33050
|
+
}
|
|
33051
|
+
const ctx = buildContextSafely();
|
|
33052
|
+
if (!ctx) return;
|
|
33053
|
+
try {
|
|
33054
|
+
await runAutoSync({
|
|
33055
|
+
caveatHome: ctx.caveatHome,
|
|
33056
|
+
ownDir: ctx.paths.knowledgeRepo,
|
|
33057
|
+
paths: ctx.paths,
|
|
33058
|
+
logger: silentLogger
|
|
33059
|
+
});
|
|
33060
|
+
} catch (err) {
|
|
33061
|
+
process.stderr.write(`[caveat:hook] auto sync error: ${errorMessage4(err)}
|
|
33062
|
+
`);
|
|
33063
|
+
}
|
|
33064
|
+
}
|
|
32102
33065
|
function buildToolErrorReminder(job, hits) {
|
|
32103
33066
|
const base = toolErrorReminderText(hits);
|
|
32104
33067
|
const mode = hookCodexSidecarMode();
|
|
32105
33068
|
if (mode === "off") return base;
|
|
32106
33069
|
const projectRoot = process.cwd();
|
|
32107
|
-
const hasSidecarConfig =
|
|
33070
|
+
const hasSidecarConfig = existsSync10(join12(projectRoot, ".codex-sidecar.yml"));
|
|
32108
33071
|
if (mode === "auto" && !hasSidecarConfig) return base;
|
|
32109
33072
|
const advisory = runCodexSidecarAdvisory({
|
|
32110
33073
|
searchText: job.searchText,
|
|
@@ -32114,21 +33077,20 @@ function buildToolErrorReminder(job, hits) {
|
|
|
32114
33077
|
"A Claude Code tool just returned an error.",
|
|
32115
33078
|
"Use the provided Caveat context to give concise next-step advice.",
|
|
32116
33079
|
"Do not tell Claude to search Caveat again unless the context is insufficient."
|
|
32117
|
-
].join(" ")
|
|
33080
|
+
].join(" "),
|
|
33081
|
+
additionalContext: job.additionalContext
|
|
32118
33082
|
});
|
|
32119
33083
|
if (advisory.status === "ok") {
|
|
32120
33084
|
return [
|
|
32121
33085
|
base,
|
|
32122
33086
|
"",
|
|
32123
|
-
|
|
32124
|
-
advisory.summary,
|
|
32125
|
-
advisory.rawEventLogRef ? `rawEventLogRef: ${advisory.rawEventLogRef}` : ""
|
|
33087
|
+
formatCodexSidecarAdvisory(advisory)
|
|
32126
33088
|
].filter(Boolean).join("\n");
|
|
32127
33089
|
}
|
|
32128
33090
|
return [
|
|
32129
33091
|
base,
|
|
32130
33092
|
"",
|
|
32131
|
-
|
|
33093
|
+
formatCodexSidecarAdvisory(advisory)
|
|
32132
33094
|
].join("\n");
|
|
32133
33095
|
}
|
|
32134
33096
|
function hookCodexSidecarMode() {
|
|
@@ -32141,7 +33103,7 @@ function buildStopReminder(signals, related) {
|
|
|
32141
33103
|
const mode = hookCodexSidecarMode();
|
|
32142
33104
|
if (mode === "off") return base;
|
|
32143
33105
|
const projectRoot = process.cwd();
|
|
32144
|
-
const hasSidecarConfig =
|
|
33106
|
+
const hasSidecarConfig = existsSync10(join12(projectRoot, ".codex-sidecar.yml"));
|
|
32145
33107
|
if (mode === "auto" && !hasSidecarConfig) return base;
|
|
32146
33108
|
const advisory = runCodexSidecarAdvisory({
|
|
32147
33109
|
searchText: struggleSearchText(signals),
|
|
@@ -32149,115 +33111,44 @@ function buildStopReminder(signals, related) {
|
|
|
32149
33111
|
projectRoot,
|
|
32150
33112
|
prompt: [
|
|
32151
33113
|
"A Claude Code session is ending after objective struggle signals.",
|
|
32152
|
-
"Use the provided Caveat context and
|
|
33114
|
+
"Use the provided Caveat context and structured hook signal context to advise whether Claude should update an existing caveat or record a new one.",
|
|
32153
33115
|
"Be concise and preserve Caveat visibility rules."
|
|
32154
|
-
].join(" ")
|
|
33116
|
+
].join(" "),
|
|
33117
|
+
additionalContext: buildHookSignalSidecarContextBlock({
|
|
33118
|
+
type: "stop",
|
|
33119
|
+
toolFailureCount: signals.toolFailureCount,
|
|
33120
|
+
reeditedFileCount: signals.fileEditCounts.length,
|
|
33121
|
+
webSearchCount: signals.webSearchCount,
|
|
33122
|
+
webFetchCount: signals.webFetchCount,
|
|
33123
|
+
bashRetryCount: signals.bashRetryCount,
|
|
33124
|
+
durationMinutes: signals.durationMinutes
|
|
33125
|
+
})
|
|
32155
33126
|
});
|
|
32156
33127
|
if (advisory.status === "ok") {
|
|
32157
33128
|
return [
|
|
32158
33129
|
base,
|
|
32159
33130
|
"",
|
|
32160
|
-
|
|
32161
|
-
advisory.summary,
|
|
32162
|
-
advisory.rawEventLogRef ? `rawEventLogRef: ${advisory.rawEventLogRef}` : ""
|
|
33131
|
+
formatCodexSidecarAdvisory(advisory)
|
|
32163
33132
|
].filter(Boolean).join("\n");
|
|
32164
33133
|
}
|
|
32165
33134
|
return [
|
|
32166
33135
|
base,
|
|
32167
33136
|
"",
|
|
32168
|
-
|
|
33137
|
+
formatCodexSidecarAdvisory(advisory)
|
|
32169
33138
|
].join("\n");
|
|
32170
33139
|
}
|
|
32171
|
-
function
|
|
32172
|
-
const singleLine = message.replace(/\s+/g, " ").trim();
|
|
32173
|
-
if (!singleLine) return "unknown error";
|
|
32174
|
-
return singleLine.length > 220 ? `${singleLine.slice(0, 220)}...` : singleLine;
|
|
32175
|
-
}
|
|
32176
|
-
function sidecarFailureDetail(output) {
|
|
32177
|
-
const protocol = output.match(/PROTOCOL_ERROR:[^"\r\n]+/);
|
|
32178
|
-
if (protocol) return compactFailureMessage(protocol[0]);
|
|
32179
|
-
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
32180
|
-
const diagnostic = lines.find(
|
|
32181
|
-
(line) => !line.startsWith("[caveat] [codex-sidecar] codex-sidecar ")
|
|
32182
|
-
);
|
|
32183
|
-
return compactFailureMessage(diagnostic ?? output);
|
|
32184
|
-
}
|
|
32185
|
-
function runCodexSidecarAdvisory(input) {
|
|
32186
|
-
const cliScript = process.argv[1];
|
|
32187
|
-
if (!cliScript) {
|
|
32188
|
-
return { status: "failed", message: "current Caveat CLI script path is unavailable" };
|
|
32189
|
-
}
|
|
32190
|
-
const args = [
|
|
32191
|
-
...process.execArgv,
|
|
32192
|
-
"--disable-warning=ExperimentalWarning",
|
|
32193
|
-
cliScript,
|
|
32194
|
-
"codex-sidecar",
|
|
32195
|
-
"run",
|
|
32196
|
-
"explore",
|
|
32197
|
-
input.prompt,
|
|
32198
|
-
"--preset",
|
|
32199
|
-
"advisory",
|
|
32200
|
-
"--query",
|
|
32201
|
-
input.searchText,
|
|
32202
|
-
"--limit",
|
|
32203
|
-
String(Math.max(1, input.limit)),
|
|
32204
|
-
"--host-agent",
|
|
32205
|
-
"claude",
|
|
32206
|
-
"--availability",
|
|
32207
|
-
"operational"
|
|
32208
|
-
];
|
|
32209
|
-
const resultDir = mkdtempSync(join9(tmpdir(), "caveat-codex-advisory-"));
|
|
32210
|
-
const resultFile = join9(resultDir, "result.json");
|
|
32211
|
-
args.push("--save-result", resultFile);
|
|
32212
|
-
const nodeCli = process.env.CAVEAT_CODEX_SIDECAR_NODE_CLI;
|
|
32213
|
-
if (nodeCli) {
|
|
32214
|
-
args.push("--node-cli", nodeCli);
|
|
32215
|
-
} else {
|
|
32216
|
-
const command = process.env.CAVEAT_CODEX_SIDECAR_COMMAND;
|
|
32217
|
-
if (command) args.push("--command", command);
|
|
32218
|
-
}
|
|
33140
|
+
async function runHook(name, arg) {
|
|
32219
33141
|
try {
|
|
32220
|
-
|
|
32221
|
-
|
|
32222
|
-
|
|
32223
|
-
timeout: Number(process.env.CAVEAT_HOOK_CODEX_SIDECAR_TIMEOUT_MS ?? 12e4),
|
|
32224
|
-
maxBuffer: 10 * 1024 * 1024
|
|
32225
|
-
});
|
|
32226
|
-
if (result.error) {
|
|
32227
|
-
return { status: "failed", message: compactFailureMessage(result.error.message) };
|
|
32228
|
-
}
|
|
32229
|
-
if (result.status !== 0) {
|
|
32230
|
-
const detail = (result.stderr || result.stdout || `exit ${result.status}`).trim();
|
|
32231
|
-
return {
|
|
32232
|
-
status: "failed",
|
|
32233
|
-
message: `sidecar command failed: ${sidecarFailureDetail(detail)}`
|
|
32234
|
-
};
|
|
32235
|
-
}
|
|
32236
|
-
const parsed = JSON.parse(readFileSync3(resultFile, "utf-8"));
|
|
32237
|
-
if (parsed.status !== "ok" || typeof parsed.summary !== "string") {
|
|
32238
|
-
return {
|
|
32239
|
-
status: "failed",
|
|
32240
|
-
message: compactFailureMessage(`unexpected SidecarResult status: ${String(parsed.status)}`)
|
|
32241
|
-
};
|
|
32242
|
-
}
|
|
32243
|
-
return {
|
|
32244
|
-
status: "ok",
|
|
32245
|
-
summary: parsed.summary,
|
|
32246
|
-
...typeof parsed.rawEventLogRef === "string" ? { rawEventLogRef: parsed.rawEventLogRef } : {}
|
|
32247
|
-
};
|
|
32248
|
-
} catch (err) {
|
|
32249
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
32250
|
-
return {
|
|
32251
|
-
status: "failed",
|
|
32252
|
-
message: compactFailureMessage(`invalid SidecarResult JSON: ${msg}`)
|
|
32253
|
-
};
|
|
32254
|
-
} finally {
|
|
32255
|
-
rmSync(resultDir, { recursive: true, force: true });
|
|
33142
|
+
sweepStaleWorkerDirs();
|
|
33143
|
+
} catch {
|
|
33144
|
+
process.stderr.write("[caveat:hook] worker stale cleanup failed\n");
|
|
32256
33145
|
}
|
|
32257
|
-
}
|
|
32258
|
-
async function runHook(name, arg) {
|
|
32259
33146
|
if (name === "reindex") {
|
|
32260
|
-
runReindexWorker();
|
|
33147
|
+
await runReindexWorker();
|
|
33148
|
+
process.exit(0);
|
|
33149
|
+
}
|
|
33150
|
+
if (name === "autosync") {
|
|
33151
|
+
await runAutoSyncWorker();
|
|
32261
33152
|
process.exit(0);
|
|
32262
33153
|
}
|
|
32263
33154
|
if (name === "worker") {
|
|
@@ -32279,7 +33170,7 @@ async function runHook(name, arg) {
|
|
|
32279
33170
|
const contexts = name === "stop" ? [] : drainForSession(sessionId);
|
|
32280
33171
|
if (name === "user-prompt-submit") {
|
|
32281
33172
|
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
32282
|
-
const hits = searchCaveatsFromTextSafely(prompt);
|
|
33173
|
+
const hits = searchCaveatsFromTextSafely(prompt, "user_prompt");
|
|
32283
33174
|
if (hits.length > 0) {
|
|
32284
33175
|
contexts.push(userPromptSubmitReminderText(hits));
|
|
32285
33176
|
}
|
|
@@ -32301,7 +33192,13 @@ async function runHook(name, arg) {
|
|
|
32301
33192
|
payload.tool_response ?? payload.toolResponse ?? payload.error ?? payload
|
|
32302
33193
|
);
|
|
32303
33194
|
if (errText) {
|
|
32304
|
-
|
|
33195
|
+
const failureKind = payload.hook_event_name === "PostToolUseFailure" ? "post-tool-use-failure" : "error-bearing-post-tool-use";
|
|
33196
|
+
const additionalContext = buildHookSignalSidecarContextBlock({
|
|
33197
|
+
type: "tool-error",
|
|
33198
|
+
toolName: payload.tool_name ?? payload.toolName,
|
|
33199
|
+
failureKind
|
|
33200
|
+
});
|
|
33201
|
+
spawnWorker({ sessionId, searchText: errText, ...additionalContext ? { additionalContext } : {} });
|
|
32305
33202
|
}
|
|
32306
33203
|
process.exit(0);
|
|
32307
33204
|
}
|
|
@@ -32320,6 +33217,13 @@ async function runHook(name, arg) {
|
|
|
32320
33217
|
} catch (err) {
|
|
32321
33218
|
const msg = err instanceof Error ? err.message : String(err);
|
|
32322
33219
|
process.stderr.write(`[caveat:hook] auto reindex trigger error: ${msg}
|
|
33220
|
+
`);
|
|
33221
|
+
}
|
|
33222
|
+
try {
|
|
33223
|
+
maybeTriggerAutoSync(ctx);
|
|
33224
|
+
} catch (err) {
|
|
33225
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33226
|
+
process.stderr.write(`[caveat:hook] auto sync trigger error: ${msg}
|
|
32323
33227
|
`);
|
|
32324
33228
|
}
|
|
32325
33229
|
}
|
|
@@ -32327,7 +33231,7 @@ async function runHook(name, arg) {
|
|
|
32327
33231
|
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
|
|
32328
33232
|
const signals = transcriptPath ? loadSignalsSafely(transcriptPath) : null;
|
|
32329
33233
|
if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
|
|
32330
|
-
const related = searchCaveatsFromTextSafely(struggleSearchText(signals));
|
|
33234
|
+
const related = searchCaveatsFromTextSafely(struggleSearchText(signals), "stop");
|
|
32331
33235
|
queueStopForSession(sessionId, signals, related);
|
|
32332
33236
|
process.exit(0);
|
|
32333
33237
|
}
|
|
@@ -32337,236 +33241,11 @@ async function runHook(name, arg) {
|
|
|
32337
33241
|
}
|
|
32338
33242
|
|
|
32339
33243
|
// src/commands/codexHookCmd.ts
|
|
32340
|
-
import { spawn as
|
|
32341
|
-
import { existsSync as
|
|
32342
|
-
import { homedir as homedir3, tmpdir as
|
|
32343
|
-
import { join as
|
|
33244
|
+
import { spawn as spawn4, spawnSync as spawnSync5 } from "node:child_process";
|
|
33245
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
33246
|
+
import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
|
|
33247
|
+
import { join as join13 } from "node:path";
|
|
32344
33248
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
|
|
32345
|
-
|
|
32346
|
-
// src/codexHookInstall.ts
|
|
32347
|
-
import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
32348
|
-
import { dirname as dirname5, join as join10 } from "node:path";
|
|
32349
|
-
function quote2(p) {
|
|
32350
|
-
return p.includes(" ") ? `"${p}"` : p;
|
|
32351
|
-
}
|
|
32352
|
-
function hookCommand2(nodePath, cliScriptPath, event) {
|
|
32353
|
-
return `${quote2(nodePath)} ${quote2(cliScriptPath)} codex-hook ${event}`;
|
|
32354
|
-
}
|
|
32355
|
-
function eventCommandFragment(event) {
|
|
32356
|
-
return `codex-hook ${event}`;
|
|
32357
|
-
}
|
|
32358
|
-
function isSameHookCommand2(actual, expected) {
|
|
32359
|
-
return actual === expected || actual.endsWith(` ${expected}`);
|
|
32360
|
-
}
|
|
32361
|
-
function isCaveatCodexHookCommand(actual, event) {
|
|
32362
|
-
const lower = actual.toLowerCase();
|
|
32363
|
-
return lower.includes("caveat") && actual.includes(eventCommandFragment(event));
|
|
32364
|
-
}
|
|
32365
|
-
function hasCaveatHook(hooksJson, event, fragment) {
|
|
32366
|
-
return hooksJson.hooks?.[event]?.some(
|
|
32367
|
-
(entry) => entry.hooks?.some((h) => h.command.includes(fragment))
|
|
32368
|
-
) ?? false;
|
|
32369
|
-
}
|
|
32370
|
-
function readHooks(path) {
|
|
32371
|
-
if (!existsSync8(path)) return {};
|
|
32372
|
-
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
32373
|
-
}
|
|
32374
|
-
function writeJsonWithBackup(path, value) {
|
|
32375
|
-
const dir = dirname5(path);
|
|
32376
|
-
if (!existsSync8(dir)) mkdirSync5(dir, { recursive: true });
|
|
32377
|
-
let backupPath = "";
|
|
32378
|
-
if (existsSync8(path)) {
|
|
32379
|
-
backupPath = `${path}.caveat-backup-${Date.now()}`;
|
|
32380
|
-
copyFileSync2(path, backupPath);
|
|
32381
|
-
}
|
|
32382
|
-
writeFileSync4(path, `${JSON.stringify(value, null, 2)}
|
|
32383
|
-
`, "utf-8");
|
|
32384
|
-
return backupPath;
|
|
32385
|
-
}
|
|
32386
|
-
function upsertHook2(hooksJson, event, command, subcommand) {
|
|
32387
|
-
hooksJson.hooks ??= {};
|
|
32388
|
-
const list = hooksJson.hooks[event] ??= [];
|
|
32389
|
-
for (const entry of list) {
|
|
32390
|
-
for (const hook of entry.hooks ?? []) {
|
|
32391
|
-
if (isSameHookCommand2(hook.command, command)) return "unchanged";
|
|
32392
|
-
if (isCaveatCodexHookCommand(hook.command, subcommand)) {
|
|
32393
|
-
hook.command = command;
|
|
32394
|
-
hook.timeoutSec = 5;
|
|
32395
|
-
hook.async = false;
|
|
32396
|
-
hook.statusMessage = null;
|
|
32397
|
-
return "added";
|
|
32398
|
-
}
|
|
32399
|
-
}
|
|
32400
|
-
}
|
|
32401
|
-
list.push({
|
|
32402
|
-
hooks: [
|
|
32403
|
-
{
|
|
32404
|
-
type: "command",
|
|
32405
|
-
command,
|
|
32406
|
-
timeoutSec: 5,
|
|
32407
|
-
async: false,
|
|
32408
|
-
statusMessage: null
|
|
32409
|
-
}
|
|
32410
|
-
]
|
|
32411
|
-
});
|
|
32412
|
-
return "added";
|
|
32413
|
-
}
|
|
32414
|
-
function removeHook2(hooksJson, event, command, subcommand) {
|
|
32415
|
-
const list = hooksJson.hooks?.[event];
|
|
32416
|
-
if (!list) return false;
|
|
32417
|
-
const before = list.length;
|
|
32418
|
-
const filtered = list.filter(
|
|
32419
|
-
(entry) => !entry.hooks?.some(
|
|
32420
|
-
(h) => isSameHookCommand2(h.command, command) || isCaveatCodexHookCommand(h.command, subcommand)
|
|
32421
|
-
)
|
|
32422
|
-
);
|
|
32423
|
-
if (filtered.length === before) return false;
|
|
32424
|
-
hooksJson.hooks[event] = filtered;
|
|
32425
|
-
return true;
|
|
32426
|
-
}
|
|
32427
|
-
function enableCodexHooksFeature(raw) {
|
|
32428
|
-
if (/^\s*codex_hooks\s*=\s*true\s*$/m.test(raw)) return { text: raw, changed: false };
|
|
32429
|
-
const lines = raw.split(/\r?\n/);
|
|
32430
|
-
const featuresStart = lines.findIndex((line) => /^\s*\[features]\s*$/.test(line));
|
|
32431
|
-
if (featuresStart === -1) {
|
|
32432
|
-
const prefix = raw.trimEnd();
|
|
32433
|
-
const text = `${prefix}${prefix ? "\n\n" : ""}[features]
|
|
32434
|
-
codex_hooks = true
|
|
32435
|
-
`;
|
|
32436
|
-
return { text, changed: true };
|
|
32437
|
-
}
|
|
32438
|
-
let insertAt = featuresStart + 1;
|
|
32439
|
-
for (let i = featuresStart + 1; i < lines.length; i += 1) {
|
|
32440
|
-
if (/^\s*\[.+]\s*$/.test(lines[i])) {
|
|
32441
|
-
break;
|
|
32442
|
-
}
|
|
32443
|
-
if (/^\s*codex_hooks\s*=/.test(lines[i])) {
|
|
32444
|
-
lines[i] = "codex_hooks = true";
|
|
32445
|
-
return { text: `${lines.join("\n").trimEnd()}
|
|
32446
|
-
`, changed: true };
|
|
32447
|
-
}
|
|
32448
|
-
}
|
|
32449
|
-
lines.splice(insertAt, 0, "codex_hooks = true");
|
|
32450
|
-
return { text: `${lines.join("\n").trimEnd()}
|
|
32451
|
-
`, changed: true };
|
|
32452
|
-
}
|
|
32453
|
-
function writeConfigWithBackup(path, text) {
|
|
32454
|
-
const dir = dirname5(path);
|
|
32455
|
-
if (!existsSync8(dir)) mkdirSync5(dir, { recursive: true });
|
|
32456
|
-
let backupPath = "";
|
|
32457
|
-
if (existsSync8(path)) {
|
|
32458
|
-
backupPath = `${path}.caveat-backup-${Date.now()}`;
|
|
32459
|
-
copyFileSync2(path, backupPath);
|
|
32460
|
-
}
|
|
32461
|
-
writeFileSync4(path, text, "utf-8");
|
|
32462
|
-
return backupPath;
|
|
32463
|
-
}
|
|
32464
|
-
function installCodexHooks(opts) {
|
|
32465
|
-
const hooksPath = join10(opts.codexHome, "hooks.json");
|
|
32466
|
-
const configPath = join10(opts.codexHome, "config.toml");
|
|
32467
|
-
const hooksJson = readHooks(hooksPath);
|
|
32468
|
-
const userPromptSubmit = upsertHook2(
|
|
32469
|
-
hooksJson,
|
|
32470
|
-
"UserPromptSubmit",
|
|
32471
|
-
hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
|
|
32472
|
-
"user-prompt-submit"
|
|
32473
|
-
);
|
|
32474
|
-
const postToolUse = upsertHook2(
|
|
32475
|
-
hooksJson,
|
|
32476
|
-
"PostToolUse",
|
|
32477
|
-
hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
|
|
32478
|
-
"post-tool-use"
|
|
32479
|
-
);
|
|
32480
|
-
const stop = upsertHook2(
|
|
32481
|
-
hooksJson,
|
|
32482
|
-
"Stop",
|
|
32483
|
-
hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
|
|
32484
|
-
"stop"
|
|
32485
|
-
);
|
|
32486
|
-
const rawConfig = existsSync8(configPath) ? readFileSync4(configPath, "utf-8") : "";
|
|
32487
|
-
const enabled = enableCodexHooksFeature(rawConfig);
|
|
32488
|
-
const anyHookAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
|
|
32489
|
-
let backupPath;
|
|
32490
|
-
let configBackupPath;
|
|
32491
|
-
if (opts.dryRun) {
|
|
32492
|
-
opts.logger.info(`[dry-run] would update ${hooksPath}`);
|
|
32493
|
-
opts.logger.info(`[dry-run] would ensure [features].codex_hooks = true in ${configPath}`);
|
|
32494
|
-
} else {
|
|
32495
|
-
if (anyHookAdded) {
|
|
32496
|
-
const backup = writeJsonWithBackup(hooksPath, hooksJson);
|
|
32497
|
-
if (backup) backupPath = backup;
|
|
32498
|
-
}
|
|
32499
|
-
if (enabled.changed) {
|
|
32500
|
-
const backup = writeConfigWithBackup(configPath, enabled.text);
|
|
32501
|
-
if (backup) configBackupPath = backup;
|
|
32502
|
-
}
|
|
32503
|
-
}
|
|
32504
|
-
return {
|
|
32505
|
-
hooks: { userPromptSubmit, postToolUse, stop },
|
|
32506
|
-
feature: enabled.changed ? "enabled" : "unchanged",
|
|
32507
|
-
backupPath,
|
|
32508
|
-
configBackupPath
|
|
32509
|
-
};
|
|
32510
|
-
}
|
|
32511
|
-
function uninstallCodexHooks(opts) {
|
|
32512
|
-
const hooksPath = join10(opts.codexHome, "hooks.json");
|
|
32513
|
-
const hooksJson = readHooks(hooksPath);
|
|
32514
|
-
const userPromptSubmitRemoved = removeHook2(
|
|
32515
|
-
hooksJson,
|
|
32516
|
-
"UserPromptSubmit",
|
|
32517
|
-
hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
|
|
32518
|
-
"user-prompt-submit"
|
|
32519
|
-
);
|
|
32520
|
-
const postToolUseRemoved = removeHook2(
|
|
32521
|
-
hooksJson,
|
|
32522
|
-
"PostToolUse",
|
|
32523
|
-
hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
|
|
32524
|
-
"post-tool-use"
|
|
32525
|
-
);
|
|
32526
|
-
const stopRemoved = removeHook2(
|
|
32527
|
-
hooksJson,
|
|
32528
|
-
"Stop",
|
|
32529
|
-
hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
|
|
32530
|
-
"stop"
|
|
32531
|
-
);
|
|
32532
|
-
let backupPath;
|
|
32533
|
-
if (opts.dryRun) {
|
|
32534
|
-
opts.logger.info(`[dry-run] would remove Caveat Codex hooks from ${hooksPath}`);
|
|
32535
|
-
} else if (userPromptSubmitRemoved || postToolUseRemoved || stopRemoved) {
|
|
32536
|
-
const backup = writeJsonWithBackup(hooksPath, hooksJson);
|
|
32537
|
-
if (backup) backupPath = backup;
|
|
32538
|
-
}
|
|
32539
|
-
return {
|
|
32540
|
-
hooks: {
|
|
32541
|
-
userPromptSubmit: userPromptSubmitRemoved ? "added" : "unchanged",
|
|
32542
|
-
postToolUse: postToolUseRemoved ? "added" : "unchanged",
|
|
32543
|
-
stop: stopRemoved ? "added" : "unchanged"
|
|
32544
|
-
},
|
|
32545
|
-
feature: "unchanged",
|
|
32546
|
-
backupPath
|
|
32547
|
-
};
|
|
32548
|
-
}
|
|
32549
|
-
function detectCodexHookInstallation(codexHome) {
|
|
32550
|
-
const hooksPath = join10(codexHome, "hooks.json");
|
|
32551
|
-
const hooksJson = readHooks(hooksPath);
|
|
32552
|
-
const hooks = {
|
|
32553
|
-
userPromptSubmit: hasCaveatHook(
|
|
32554
|
-
hooksJson,
|
|
32555
|
-
"UserPromptSubmit",
|
|
32556
|
-
eventCommandFragment("user-prompt-submit")
|
|
32557
|
-
),
|
|
32558
|
-
postToolUse: hasCaveatHook(hooksJson, "PostToolUse", eventCommandFragment("post-tool-use")),
|
|
32559
|
-
stop: hasCaveatHook(hooksJson, "Stop", eventCommandFragment("stop"))
|
|
32560
|
-
};
|
|
32561
|
-
const count = Object.values(hooks).filter(Boolean).length;
|
|
32562
|
-
return {
|
|
32563
|
-
installation: count === 0 ? "not-installed" : count === 3 ? "installed" : "partial",
|
|
32564
|
-
hooksPath,
|
|
32565
|
-
hooks
|
|
32566
|
-
};
|
|
32567
|
-
}
|
|
32568
|
-
|
|
32569
|
-
// src/commands/codexHookCmd.ts
|
|
32570
33249
|
var silentLogger2 = {
|
|
32571
33250
|
info: () => {
|
|
32572
33251
|
},
|
|
@@ -32606,31 +33285,44 @@ function buildContextSafely2() {
|
|
|
32606
33285
|
return null;
|
|
32607
33286
|
}
|
|
32608
33287
|
}
|
|
32609
|
-
function searchCaveatsFromTextSafely2(text) {
|
|
33288
|
+
function searchCaveatsFromTextSafely2(text, surface) {
|
|
32610
33289
|
if (!text) return [];
|
|
32611
33290
|
let db;
|
|
33291
|
+
let caveatHome;
|
|
33292
|
+
let hits;
|
|
32612
33293
|
try {
|
|
32613
33294
|
const ctx = buildContextSafely2();
|
|
32614
|
-
if (!ctx || !
|
|
33295
|
+
if (!ctx || !existsSync11(ctx.paths.dbPath)) return [];
|
|
33296
|
+
caveatHome = ctx.caveatHome;
|
|
32615
33297
|
db = openDb({ path: ctx.paths.dbPath });
|
|
32616
|
-
|
|
33298
|
+
hits = findCaveatsForPrompt(db, text, {
|
|
32617
33299
|
selfIdentity: defaultSelfIdentityTokens()
|
|
32618
33300
|
});
|
|
32619
|
-
if (hits.length > 0) {
|
|
32620
|
-
try {
|
|
32621
|
-
markHit(db, hits);
|
|
32622
|
-
} catch (err) {
|
|
32623
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
32624
|
-
process.stderr.write(`[caveat:codex-hook] markHit error: ${msg}
|
|
32625
|
-
`);
|
|
32626
|
-
}
|
|
32627
|
-
}
|
|
32628
|
-
return hits;
|
|
32629
33301
|
} catch (err) {
|
|
32630
33302
|
const msg = err instanceof Error ? err.message : String(err);
|
|
32631
33303
|
process.stderr.write(`[caveat:codex-hook] search error: ${msg}
|
|
32632
33304
|
`);
|
|
32633
33305
|
return [];
|
|
33306
|
+
}
|
|
33307
|
+
if (hits.length > 0) {
|
|
33308
|
+
try {
|
|
33309
|
+
markHit(db, hits);
|
|
33310
|
+
} catch (err) {
|
|
33311
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33312
|
+
process.stderr.write(`[caveat:codex-hook] markHit error: ${msg}
|
|
33313
|
+
`);
|
|
33314
|
+
}
|
|
33315
|
+
} else {
|
|
33316
|
+
try {
|
|
33317
|
+
logHookQueryMiss({ caveatHome, agent: "codex", surface, query: text });
|
|
33318
|
+
} catch (err) {
|
|
33319
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33320
|
+
process.stderr.write(`[caveat:codex-hook] query log error: ${msg}
|
|
33321
|
+
`);
|
|
33322
|
+
}
|
|
33323
|
+
}
|
|
33324
|
+
try {
|
|
33325
|
+
return hits;
|
|
32634
33326
|
} finally {
|
|
32635
33327
|
db?.close();
|
|
32636
33328
|
}
|
|
@@ -32680,10 +33372,10 @@ function processExitCodeFromText(text) {
|
|
|
32680
33372
|
return m ? Number(m[1]) : null;
|
|
32681
33373
|
}
|
|
32682
33374
|
function transcriptToolOutput(transcriptPath, toolUseId) {
|
|
32683
|
-
if (!transcriptPath || !toolUseId || !
|
|
33375
|
+
if (!transcriptPath || !toolUseId || !existsSync11(transcriptPath)) return null;
|
|
32684
33376
|
let raw = "";
|
|
32685
33377
|
try {
|
|
32686
|
-
raw =
|
|
33378
|
+
raw = readFileSync6(transcriptPath, "utf-8");
|
|
32687
33379
|
} catch {
|
|
32688
33380
|
return null;
|
|
32689
33381
|
}
|
|
@@ -32779,7 +33471,10 @@ function codexContextOutput(text, eventName = "UserPromptSubmit") {
|
|
|
32779
33471
|
function drainForSession2(sessionId) {
|
|
32780
33472
|
const ctx = buildContextSafely2();
|
|
32781
33473
|
if (!ctx) return [];
|
|
32782
|
-
return
|
|
33474
|
+
return [
|
|
33475
|
+
...drainPendingReminders(ctx.caveatHome, sessionId),
|
|
33476
|
+
...drainGlobalPendingReminders(ctx.caveatHome)
|
|
33477
|
+
];
|
|
32783
33478
|
}
|
|
32784
33479
|
function codexContextDedupeKey(text) {
|
|
32785
33480
|
if (text.startsWith(CODEX_STOP_REMINDER_PREFIX)) return "codex-stop-reminder";
|
|
@@ -32821,20 +33516,20 @@ function stopSignalKey2(signals, related) {
|
|
|
32821
33516
|
return createHash2("sha256").update(body).digest("hex");
|
|
32822
33517
|
}
|
|
32823
33518
|
function stopStatePath2(caveatHome, sessionId) {
|
|
32824
|
-
return
|
|
33519
|
+
return join13(caveatHome, CODEX_STOP_STATE_DIR, `${sanitizeCodexStateId(sessionId)}.txt`);
|
|
32825
33520
|
}
|
|
32826
33521
|
function wasStopReminderQueued2(caveatHome, sessionId, key) {
|
|
32827
33522
|
const path = stopStatePath2(caveatHome, sessionId);
|
|
32828
33523
|
try {
|
|
32829
|
-
return
|
|
33524
|
+
return readFileSync6(path, "utf-8") === key;
|
|
32830
33525
|
} catch {
|
|
32831
33526
|
return false;
|
|
32832
33527
|
}
|
|
32833
33528
|
}
|
|
32834
33529
|
function markStopReminderQueued2(caveatHome, sessionId, key) {
|
|
32835
33530
|
const path = stopStatePath2(caveatHome, sessionId);
|
|
32836
|
-
mkdirSync6(
|
|
32837
|
-
|
|
33531
|
+
mkdirSync6(join13(caveatHome, CODEX_STOP_STATE_DIR), { recursive: true });
|
|
33532
|
+
writeFileSync6(path, key, "utf-8");
|
|
32838
33533
|
}
|
|
32839
33534
|
function queueStopForSession2(sessionId, signals, related) {
|
|
32840
33535
|
const ctx = buildContextSafely2();
|
|
@@ -32875,7 +33570,7 @@ async function processCodexWorkerJob(job, opts = { waitForTranscript: true }) {
|
|
|
32875
33570
|
}
|
|
32876
33571
|
}
|
|
32877
33572
|
if (!knownError && job.allowSymptomOnly !== true) return;
|
|
32878
|
-
const hits = searchCaveatsFromTextSafely2(searchText);
|
|
33573
|
+
const hits = searchCaveatsFromTextSafely2(searchText, "tool_error");
|
|
32879
33574
|
if (hits.length === 0) return;
|
|
32880
33575
|
const ctx = buildContextSafely2();
|
|
32881
33576
|
if (!ctx) return;
|
|
@@ -32887,12 +33582,12 @@ async function processCodexWorkerJob(job, opts = { waitForTranscript: true }) {
|
|
|
32887
33582
|
async function runCodexWorker(workFile) {
|
|
32888
33583
|
let raw;
|
|
32889
33584
|
try {
|
|
32890
|
-
raw =
|
|
33585
|
+
raw = readFileSync6(workFile, "utf-8");
|
|
32891
33586
|
} catch {
|
|
32892
33587
|
process.exit(0);
|
|
32893
33588
|
}
|
|
32894
33589
|
try {
|
|
32895
|
-
|
|
33590
|
+
unlinkSync(workFile);
|
|
32896
33591
|
} catch {
|
|
32897
33592
|
}
|
|
32898
33593
|
let job;
|
|
@@ -32904,13 +33599,14 @@ async function runCodexWorker(workFile) {
|
|
|
32904
33599
|
await processCodexWorkerJob(job);
|
|
32905
33600
|
process.exit(0);
|
|
32906
33601
|
}
|
|
32907
|
-
function runDiagnostics(codexHome = process.env.CODEX_HOME ??
|
|
32908
|
-
const features =
|
|
33602
|
+
function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join13(homedir3(), ".codex")) {
|
|
33603
|
+
const features = spawnSync5("codex", ["features", "list"], {
|
|
32909
33604
|
encoding: "utf-8",
|
|
32910
|
-
maxBuffer: 1024 * 1024
|
|
33605
|
+
maxBuffer: 1024 * 1024,
|
|
33606
|
+
env: codexFeatureListEnv(codexHome)
|
|
32911
33607
|
});
|
|
32912
33608
|
const featureOutput = [features.stdout, features.stderr].filter(Boolean).join("\n");
|
|
32913
|
-
const hasHooks = /^
|
|
33609
|
+
const hasHooks = /^hooks\s+\S+\s+true\b/m.test(featureOutput);
|
|
32914
33610
|
const installation = detectCodexHookInstallation(codexHome);
|
|
32915
33611
|
const result = {
|
|
32916
33612
|
availability: features.error || features.status !== 0 || !hasHooks ? "unavailable" : "available",
|
|
@@ -32920,11 +33616,14 @@ function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join11(homedir3(),
|
|
|
32920
33616
|
codexHome,
|
|
32921
33617
|
hooksPath: installation.hooksPath,
|
|
32922
33618
|
installedHooks: installation.hooks,
|
|
32923
|
-
evidence: featureOutput.split("\n").find((line) => line.trim().startsWith("
|
|
33619
|
+
evidence: featureOutput.split("\n").find((line) => line.trim().startsWith("hooks")) ?? null
|
|
32924
33620
|
};
|
|
32925
33621
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
32926
33622
|
`);
|
|
32927
33623
|
}
|
|
33624
|
+
function codexFeatureListEnv(codexHome, inherited = process.env) {
|
|
33625
|
+
return { ...inherited, CODEX_HOME: codexHome };
|
|
33626
|
+
}
|
|
32928
33627
|
async function runCodexHook(name, arg) {
|
|
32929
33628
|
if (name === "diagnostics") {
|
|
32930
33629
|
runDiagnostics(arg);
|
|
@@ -32950,7 +33649,7 @@ async function runCodexHook(name, arg) {
|
|
|
32950
33649
|
if (name === "user-prompt-submit") {
|
|
32951
33650
|
const contexts = sessionId ? drainForSession2(sessionId) : [];
|
|
32952
33651
|
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
32953
|
-
const hits = searchCaveatsFromTextSafely2(prompt);
|
|
33652
|
+
const hits = searchCaveatsFromTextSafely2(prompt, "user_prompt");
|
|
32954
33653
|
if (hits.length > 0) {
|
|
32955
33654
|
contexts.push(userPromptSubmitReminderText(hits));
|
|
32956
33655
|
}
|
|
@@ -32969,7 +33668,23 @@ async function runCodexHook(name, arg) {
|
|
|
32969
33668
|
if (name === "stop") {
|
|
32970
33669
|
try {
|
|
32971
33670
|
const ctx = buildContextSafely2();
|
|
32972
|
-
if (ctx)
|
|
33671
|
+
if (ctx) {
|
|
33672
|
+
try {
|
|
33673
|
+
maybeSweepPendingDirs(ctx.caveatHome);
|
|
33674
|
+
} catch (err) {
|
|
33675
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33676
|
+
process.stderr.write(`[caveat:codex-hook] pending sweep error: ${msg}
|
|
33677
|
+
`);
|
|
33678
|
+
}
|
|
33679
|
+
maybeTriggerAutoReindex(ctx);
|
|
33680
|
+
try {
|
|
33681
|
+
maybeTriggerAutoSync(ctx);
|
|
33682
|
+
} catch (err) {
|
|
33683
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33684
|
+
process.stderr.write(`[caveat:codex-hook] auto sync trigger error: ${msg}
|
|
33685
|
+
`);
|
|
33686
|
+
}
|
|
33687
|
+
}
|
|
32973
33688
|
} catch (err) {
|
|
32974
33689
|
const msg = err instanceof Error ? err.message : String(err);
|
|
32975
33690
|
process.stderr.write(`[caveat:codex-hook] auto reindex trigger error: ${msg}
|
|
@@ -32979,7 +33694,7 @@ async function runCodexHook(name, arg) {
|
|
|
32979
33694
|
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
|
|
32980
33695
|
const signals = transcriptPath ? loadSignalsSafely2(transcriptPath) : null;
|
|
32981
33696
|
if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
|
|
32982
|
-
const related = searchCaveatsFromTextSafely2(struggleSearchText(signals));
|
|
33697
|
+
const related = searchCaveatsFromTextSafely2(struggleSearchText(signals), "stop");
|
|
32983
33698
|
if (sessionId) queueStopForSession2(sessionId, signals, related);
|
|
32984
33699
|
process.exit(0);
|
|
32985
33700
|
}
|
|
@@ -32989,9 +33704,9 @@ async function runCodexHook(name, arg) {
|
|
|
32989
33704
|
}
|
|
32990
33705
|
|
|
32991
33706
|
// src/commands/pull.ts
|
|
32992
|
-
import { existsSync as
|
|
33707
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
32993
33708
|
async function runPull(ctx) {
|
|
32994
|
-
const hasCommunityDir =
|
|
33709
|
+
const hasCommunityDir = existsSync12(ctx.paths.communityDir);
|
|
32995
33710
|
if (!hasCommunityDir) {
|
|
32996
33711
|
ctx.logger.info(
|
|
32997
33712
|
"no community repos yet \u2014 add one with `caveat community add <github-url>`."
|
|
@@ -33007,8 +33722,12 @@ async function runPull(ctx) {
|
|
|
33007
33722
|
}
|
|
33008
33723
|
const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
|
|
33009
33724
|
try {
|
|
33010
|
-
|
|
33011
|
-
const
|
|
33725
|
+
const keyProvider = createKeyserverKeyProvider({ caveatHome: ctx.caveatHome });
|
|
33726
|
+
const failures = await prewarmSealedKeys({ paths: ctx.paths, keyProvider });
|
|
33727
|
+
for (const failure of failures) {
|
|
33728
|
+
ctx.logger.warn(`${failure.source}: sealed key prewarm failed: ${errorMessage5(failure.error)}`);
|
|
33729
|
+
}
|
|
33730
|
+
const result = reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger, keyProvider });
|
|
33012
33731
|
for (const [source, scan] of Object.entries(result.perSource)) {
|
|
33013
33732
|
ctx.logger.info(`${source}: +${scan.added}`);
|
|
33014
33733
|
}
|
|
@@ -33017,51 +33736,19 @@ async function runPull(ctx) {
|
|
|
33017
33736
|
db.close();
|
|
33018
33737
|
}
|
|
33019
33738
|
}
|
|
33020
|
-
|
|
33021
|
-
|
|
33022
|
-
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
33023
|
-
import { readSync } from "node:fs";
|
|
33024
|
-
function runGh(args) {
|
|
33025
|
-
const result = spawnSync4("gh", args, { encoding: "utf-8" });
|
|
33026
|
-
return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "", stderr: typeof result.stderr === "string" ? result.stderr : "" };
|
|
33027
|
-
}
|
|
33028
|
-
function commandError(result, fallback) {
|
|
33029
|
-
return new Error((result.stderr || result.stdout || fallback).trim());
|
|
33030
|
-
}
|
|
33031
|
-
function askOnce(question) {
|
|
33032
|
-
process.stdout.write(`${question} `);
|
|
33033
|
-
const buf = Buffer.alloc(256);
|
|
33034
|
-
const bytes = readSync(0, buf, 0, buf.length, null);
|
|
33035
|
-
return /^(?:y|yes)$/i.test(buf.toString("utf-8", 0, bytes).trim());
|
|
33739
|
+
function errorMessage5(err) {
|
|
33740
|
+
return err instanceof Error ? err.message : String(err);
|
|
33036
33741
|
}
|
|
33037
33742
|
|
|
33038
33743
|
// src/commands/sync.ts
|
|
33039
33744
|
async function defaultPrivateRepoUrl(opts, deps) {
|
|
33040
|
-
|
|
33041
|
-
|
|
33042
|
-
|
|
33043
|
-
|
|
33044
|
-
|
|
33045
|
-
|
|
33046
|
-
|
|
33047
|
-
if (user.status !== 0 || !user.stdout.trim()) {
|
|
33048
|
-
throw new Error("gh is not authenticated; run `gh auth login`, then retry `caveat sync --init`");
|
|
33049
|
-
}
|
|
33050
|
-
const login = user.stdout.trim();
|
|
33051
|
-
const name = `${login}/Caveat-Private`;
|
|
33052
|
-
const view = deps.ghRunner(["repo", "view", name]);
|
|
33053
|
-
if (view.status !== 0) {
|
|
33054
|
-
const question = `create private repo github.com/${name}? [y/N]`;
|
|
33055
|
-
if (!opts.yes) {
|
|
33056
|
-
if (!deps.isTty()) {
|
|
33057
|
-
throw new Error(`${question}; rerun with --yes to approve non-interactively`);
|
|
33058
|
-
}
|
|
33059
|
-
if (!deps.confirm(question)) throw new Error("private repository creation cancelled");
|
|
33060
|
-
}
|
|
33061
|
-
const create = deps.ghRunner(["repo", "create", "Caveat-Private", "--private"]);
|
|
33062
|
-
if (create.status !== 0) throw commandError(create, "gh repo create failed");
|
|
33063
|
-
}
|
|
33064
|
-
return `https://github.com/${name}.git`;
|
|
33745
|
+
return defaultGitHubRepoUrl({
|
|
33746
|
+
repositoryName: "Caveat-Private",
|
|
33747
|
+
visibility: "private",
|
|
33748
|
+
yes: opts.yes,
|
|
33749
|
+
retryCommand: "caveat sync --init <url>",
|
|
33750
|
+
...deps
|
|
33751
|
+
});
|
|
33065
33752
|
}
|
|
33066
33753
|
async function runSync(ctx, opts, dependencies = {}) {
|
|
33067
33754
|
try {
|
|
@@ -33100,6 +33787,10 @@ async function runSync(ctx, opts, dependencies = {}) {
|
|
|
33100
33787
|
} else {
|
|
33101
33788
|
const rebased = result.pulled ? ", rebased onto remote" : "";
|
|
33102
33789
|
ctx.logger.info(`synced ${result.branch} \u2192 origin (${result.changedFiles} local change(s)${rebased})`);
|
|
33790
|
+
try {
|
|
33791
|
+
resetAutoSyncFailureState(ctx.caveatHome);
|
|
33792
|
+
} catch {
|
|
33793
|
+
}
|
|
33103
33794
|
}
|
|
33104
33795
|
} catch (err) {
|
|
33105
33796
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -33108,60 +33799,11 @@ async function runSync(ctx, opts, dependencies = {}) {
|
|
|
33108
33799
|
}
|
|
33109
33800
|
}
|
|
33110
33801
|
|
|
33111
|
-
// src/commands/publish.ts
|
|
33112
|
-
var HTTPS_TARGET_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+?(?:\.git)?\/?$/;
|
|
33113
|
-
var SCP_TARGET_RE = /^git@github\.com:[^/]+\/[^/]+?(?:\.git)?$/;
|
|
33114
|
-
function validatePublishTarget(url2) {
|
|
33115
|
-
if (!HTTPS_TARGET_RE.test(url2) && !SCP_TARGET_RE.test(url2)) throw new Error("invalid publish target: use https://github.com/<org>/<repo>(.git) or git@github.com:<org>/<repo>(.git)");
|
|
33116
|
-
return url2;
|
|
33117
|
-
}
|
|
33118
|
-
async function defaultPublicRepoUrl(opts, deps) {
|
|
33119
|
-
const version2 = deps.ghRunner(["--version"]);
|
|
33120
|
-
if (version2.status !== 0) throw new Error("gh is not available. create a public repository named Caveat-Public on GitHub, then run:\n caveat publish --init <url>\n # or install gh and run caveat publish");
|
|
33121
|
-
const user = deps.ghRunner(["api", "user", "-q", ".login"]);
|
|
33122
|
-
if (user.status !== 0 || !user.stdout.trim()) throw new Error("gh is not authenticated; run `gh auth login`, then retry `caveat publish`");
|
|
33123
|
-
const login = user.stdout.trim();
|
|
33124
|
-
const name = `${login}/Caveat-Public`;
|
|
33125
|
-
const view = deps.ghRunner(["repo", "view", name]);
|
|
33126
|
-
if (view.status !== 0) {
|
|
33127
|
-
const question = `create public repo github.com/${name}? [y/N]`;
|
|
33128
|
-
if (!opts.yes) {
|
|
33129
|
-
if (!deps.isTty()) throw new Error(`${question}; rerun with --yes to approve non-interactively`);
|
|
33130
|
-
if (!deps.confirm(question)) throw new Error("public repository creation cancelled");
|
|
33131
|
-
}
|
|
33132
|
-
const create = deps.ghRunner(["repo", "create", "Caveat-Public", "--public"]);
|
|
33133
|
-
if (create.status !== 0) throw commandError(create, "gh repo create failed");
|
|
33134
|
-
}
|
|
33135
|
-
return `https://github.com/${name}.git`;
|
|
33136
|
-
}
|
|
33137
|
-
async function runPublish(ctx, opts, dependencies = {}) {
|
|
33138
|
-
try {
|
|
33139
|
-
const ghRunner = dependencies.ghRunner ?? runGh;
|
|
33140
|
-
const isTty = dependencies.isTty ?? (() => Boolean(process.stdin.isTTY));
|
|
33141
|
-
const confirm = dependencies.confirm ?? askOnce;
|
|
33142
|
-
let target = ctx.config.publishTarget;
|
|
33143
|
-
if (opts.init !== void 0) {
|
|
33144
|
-
target = validatePublishTarget(opts.init);
|
|
33145
|
-
writeUserConfigPatch(ctx.userConfigPath, { publishTarget: target });
|
|
33146
|
-
}
|
|
33147
|
-
if (!target) {
|
|
33148
|
-
target = await defaultPublicRepoUrl(opts, { ghRunner, isTty, confirm });
|
|
33149
|
-
writeUserConfigPatch(ctx.userConfigPath, { publishTarget: target });
|
|
33150
|
-
}
|
|
33151
|
-
const result = await publishOwn({ paths: ctx.paths, config: { ...ctx.config, publishTarget: target }, logger: ctx.logger, confirmImpl: confirm, isTty, dryRun: opts.dryRun, yes: opts.yes });
|
|
33152
|
-
if (result.dryRun) ctx.logger.info(`[dry-run] ${result.fileCount} public entry(ies) ready to publish`);
|
|
33153
|
-
else if (result.changed) ctx.logger.info(`published ${result.fileCount} public entry(ies)`);
|
|
33154
|
-
} catch (err) {
|
|
33155
|
-
ctx.logger.error(err instanceof Error ? err.message : String(err));
|
|
33156
|
-
process.exitCode = 1;
|
|
33157
|
-
}
|
|
33158
|
-
}
|
|
33159
|
-
|
|
33160
33802
|
// src/commands/codexSidecar.ts
|
|
33161
|
-
import { spawnSync as
|
|
33162
|
-
import { mkdirSync as mkdirSync7, mkdtempSync as
|
|
33163
|
-
import { tmpdir as
|
|
33164
|
-
import { dirname as
|
|
33803
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
33804
|
+
import { closeSync, constants, fstatSync, lstatSync as lstatSync2, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, openSync, readSync as readSync2, realpathSync as realpathSync3, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
33805
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
33806
|
+
import { basename as basename2, dirname as dirname7, join as join14 } from "node:path";
|
|
33165
33807
|
import { cwd, exit } from "node:process";
|
|
33166
33808
|
function runCodexSidecarDiagnostics(logger, opts) {
|
|
33167
33809
|
const plan = buildCodexSidecarDiagnosticsCommand({
|
|
@@ -33193,8 +33835,8 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
|
|
|
33193
33835
|
process.stdout.write(JSON.stringify({ status: "skipped", decision }, null, 2) + "\n");
|
|
33194
33836
|
exit(0);
|
|
33195
33837
|
}
|
|
33196
|
-
const contextDir =
|
|
33197
|
-
const contextFile =
|
|
33838
|
+
const contextDir = mkdtempSync3(join14(tmpdir4(), "caveat-sidecar-context-"));
|
|
33839
|
+
const contextFile = join14(contextDir, "context.json");
|
|
33198
33840
|
let status = 1;
|
|
33199
33841
|
try {
|
|
33200
33842
|
const blocks = collectCaveatContextBlocks(ctx, {
|
|
@@ -33203,7 +33845,8 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
|
|
|
33203
33845
|
source: opts.source,
|
|
33204
33846
|
visibility: opts.visibility
|
|
33205
33847
|
});
|
|
33206
|
-
|
|
33848
|
+
const additionalBlocks = opts.additionalContextFile ? readHookSignalAdditionalContextFile(opts.additionalContextFile) : [];
|
|
33849
|
+
writeFileSync7(contextFile, JSON.stringify({ context: [...blocks, ...additionalBlocks] }, null, 2) + "\n", "utf-8");
|
|
33207
33850
|
const plan = buildCodexSidecarRunCommand({
|
|
33208
33851
|
workflow,
|
|
33209
33852
|
projectRoot: opts.project ?? cwd(),
|
|
@@ -33214,10 +33857,94 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
|
|
|
33214
33857
|
});
|
|
33215
33858
|
status = executePlan(ctx.logger, plan.command, plan.args, { saveResult: opts.saveResult });
|
|
33216
33859
|
} finally {
|
|
33217
|
-
|
|
33860
|
+
rmSync3(contextDir, { recursive: true, force: true });
|
|
33218
33861
|
}
|
|
33219
33862
|
exit(status);
|
|
33220
33863
|
}
|
|
33864
|
+
var MAX_ADDITIONAL_CONTEXT_BYTES = 4096;
|
|
33865
|
+
function readHookSignalAdditionalContextFile(path, testProbe) {
|
|
33866
|
+
const platform = testProbe?.platform ?? process.platform;
|
|
33867
|
+
assertWindowsPrivateTempContainer(path, platform);
|
|
33868
|
+
const before = lstatSync2(path);
|
|
33869
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
33870
|
+
assertPrivateRegular(before, uid, platform);
|
|
33871
|
+
testProbe?.afterLstat?.();
|
|
33872
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0));
|
|
33873
|
+
let raw;
|
|
33874
|
+
try {
|
|
33875
|
+
const opened = fstatSync(fd);
|
|
33876
|
+
assertPrivateRegular(opened, uid, platform);
|
|
33877
|
+
if (opened.dev !== before.dev || opened.ino !== before.ino) throw new Error("additional context file changed during open");
|
|
33878
|
+
const bytes = Buffer.alloc(MAX_ADDITIONAL_CONTEXT_BYTES + 1);
|
|
33879
|
+
let count = 0;
|
|
33880
|
+
while (count < bytes.length) {
|
|
33881
|
+
const read = readSync2(fd, bytes, count, bytes.length - count, count);
|
|
33882
|
+
if (read === 0) break;
|
|
33883
|
+
count += read;
|
|
33884
|
+
}
|
|
33885
|
+
if (count > MAX_ADDITIONAL_CONTEXT_BYTES) throw new Error(`additional context file exceeds ${MAX_ADDITIONAL_CONTEXT_BYTES} bytes`);
|
|
33886
|
+
const after = fstatSync(fd);
|
|
33887
|
+
assertPrivateRegular(after, uid, platform);
|
|
33888
|
+
if (after.dev !== opened.dev || after.ino !== opened.ino || after.dev !== before.dev || after.ino !== before.ino) throw new Error("additional context file changed during read");
|
|
33889
|
+
raw = bytes.subarray(0, count).toString("utf-8");
|
|
33890
|
+
} finally {
|
|
33891
|
+
closeSync(fd);
|
|
33892
|
+
}
|
|
33893
|
+
const parsed = JSON.parse(raw);
|
|
33894
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.context) || parsed.context.length !== 1) {
|
|
33895
|
+
throw new Error("additional context file must contain exactly one context block");
|
|
33896
|
+
}
|
|
33897
|
+
const block = parsed.context[0];
|
|
33898
|
+
if (!isHookSignalBlock(block)) {
|
|
33899
|
+
throw new Error("additional context file contains an invalid hook signal block");
|
|
33900
|
+
}
|
|
33901
|
+
return [block];
|
|
33902
|
+
}
|
|
33903
|
+
function assertPrivateRegular(stat, uid, platform) {
|
|
33904
|
+
const privateOwner = platform === "win32" || (stat.mode & 63) === 0 && (uid === void 0 || stat.uid === uid);
|
|
33905
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_ADDITIONAL_CONTEXT_BYTES || !privateOwner) throw new Error("additional context file must be a private owner-only regular file within 4096 bytes");
|
|
33906
|
+
}
|
|
33907
|
+
function assertWindowsPrivateTempContainer(path, platform) {
|
|
33908
|
+
if (platform !== "win32") return;
|
|
33909
|
+
const parent = dirname7(path);
|
|
33910
|
+
const parentStat = lstatSync2(parent);
|
|
33911
|
+
const resolvedParent = realpathSync3(parent);
|
|
33912
|
+
const resolvedTemp = realpathSync3(tmpdir4());
|
|
33913
|
+
if (parentStat.isSymbolicLink() || !parentStat.isDirectory() || dirname7(resolvedParent) !== resolvedTemp || !basename2(resolvedParent).startsWith("caveat-")) {
|
|
33914
|
+
throw new Error("additional context file must be inside a reserved per-user Caveat temporary directory");
|
|
33915
|
+
}
|
|
33916
|
+
}
|
|
33917
|
+
function isRecord(value) {
|
|
33918
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
33919
|
+
}
|
|
33920
|
+
function isHookSignalBlock(value) {
|
|
33921
|
+
if (!isRecord(value) || !hasExactKeys(value, ["kind", "source", "trust", "summary", "data"]) || value.kind !== "manual_note" || value.source !== "caveat-hook-signal" || value.trust !== "local" || typeof value.summary !== "string" || !isRecord(value.data)) return false;
|
|
33922
|
+
const data = value.data;
|
|
33923
|
+
if (data.type === "tool-error") {
|
|
33924
|
+
if (!hasExactKeys(data, ["type", "tool", "failure_kind"]) || typeof data.tool !== "string" || data.failure_kind !== "post-tool-use-failure" && data.failure_kind !== "error-bearing-post-tool-use") return false;
|
|
33925
|
+
const canonical2 = buildHookSignalFromData(data);
|
|
33926
|
+
return canonical2 !== null && JSON.stringify(value) === JSON.stringify(canonical2);
|
|
33927
|
+
}
|
|
33928
|
+
if (!hasExactKeys(data, ["type", "tool_failure_count", "reedited_file_count", "web_search_count", "web_fetch_count", "bash_retry_count", "duration_minutes"])) return false;
|
|
33929
|
+
const canonical = buildHookSignalFromData(data);
|
|
33930
|
+
return canonical !== null && JSON.stringify(value) === JSON.stringify(canonical);
|
|
33931
|
+
}
|
|
33932
|
+
function hasExactKeys(value, keys) {
|
|
33933
|
+
const actual = Object.keys(value).sort();
|
|
33934
|
+
return actual.length === keys.length && actual.every((key, index) => key === keys.slice().sort()[index]);
|
|
33935
|
+
}
|
|
33936
|
+
function buildHookSignalFromData(data) {
|
|
33937
|
+
if (data.type === "tool-error") {
|
|
33938
|
+
const validTools = ["bash", "edit", "write", "read", "glob", "grep", "web-search", "web-fetch", "notebook-edit", "other"];
|
|
33939
|
+
if (!validTools.includes(String(data.tool))) return null;
|
|
33940
|
+
const toolNames = { bash: "Bash", edit: "Edit", write: "Write", read: "Read", glob: "Glob", grep: "Grep", "web-search": "WebSearch", "web-fetch": "WebFetch", "notebook-edit": "NotebookEdit", other: "other" };
|
|
33941
|
+
return buildHookSignalSidecarContextBlock({ type: "tool-error", toolName: toolNames[String(data.tool)], failureKind: data.failure_kind });
|
|
33942
|
+
}
|
|
33943
|
+
if (data.type !== "stop") return null;
|
|
33944
|
+
const keys = ["tool_failure_count", "reedited_file_count", "web_search_count", "web_fetch_count", "bash_retry_count", "duration_minutes"];
|
|
33945
|
+
if (!keys.every((key) => typeof data[key] === "number" && Number.isInteger(data[key]) && data[key] >= 0 && data[key] <= 1e4)) return null;
|
|
33946
|
+
return buildHookSignalSidecarContextBlock({ type: "stop", toolFailureCount: data.tool_failure_count, reeditedFileCount: data.reedited_file_count, webSearchCount: data.web_search_count, webFetchCount: data.web_fetch_count, bashRetryCount: data.bash_retry_count, durationMinutes: data.duration_minutes });
|
|
33947
|
+
}
|
|
33221
33948
|
function runCodexSidecarWorkSmoke(ctx, prompt, opts) {
|
|
33222
33949
|
const plan = buildCodexSidecarRunCommand({
|
|
33223
33950
|
workflow: "work",
|
|
@@ -33258,7 +33985,7 @@ function defaultPreset(workflow) {
|
|
|
33258
33985
|
}
|
|
33259
33986
|
function executePlan(logger, command, args, options = {}) {
|
|
33260
33987
|
logger.info(`[codex-sidecar] ${command} ${args.map(shellDisplayQuote).join(" ")}`);
|
|
33261
|
-
const result =
|
|
33988
|
+
const result = spawnSync6(command, args, { encoding: "utf-8", stdio: "pipe" });
|
|
33262
33989
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
33263
33990
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
33264
33991
|
if (options.saveResult && result.stdout) {
|
|
@@ -33272,8 +33999,8 @@ function executePlan(logger, command, args, options = {}) {
|
|
|
33272
33999
|
}
|
|
33273
34000
|
function saveStructuredResult(path, stdout) {
|
|
33274
34001
|
const parsed = JSON.parse(stdout);
|
|
33275
|
-
mkdirSync7(
|
|
33276
|
-
|
|
34002
|
+
mkdirSync7(dirname7(path), { recursive: true });
|
|
34003
|
+
writeFileSync7(path, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
33277
34004
|
}
|
|
33278
34005
|
function shellDisplayQuote(value) {
|
|
33279
34006
|
return /[\s"'$`]/.test(value) ? JSON.stringify(value) : value;
|
|
@@ -33361,10 +34088,13 @@ function runCommunityRemove(ctx, handle, opts) {
|
|
|
33361
34088
|
|
|
33362
34089
|
// src/index.ts
|
|
33363
34090
|
var program = new Command();
|
|
34091
|
+
function collectRepeatable(value, previous) {
|
|
34092
|
+
return [...previous, value];
|
|
34093
|
+
}
|
|
33364
34094
|
program.name("caveat").description("External spec gotcha knowledge base CLI").version(CAVEAT_VERSION);
|
|
33365
34095
|
program.command("init").description(
|
|
33366
34096
|
"Initialize ~/.caveatrc.json, ~/.caveat/, and register Claude Code integration. Add knowledge sources later with `caveat community add <github-url>`."
|
|
33367
|
-
).option("--skip-claude", "skip Claude Code MCP + hook registration", false).option("--dry-run", "show planned changes without writing", false).option(
|
|
34097
|
+
).option("--skip-claude", "skip Claude Code MCP + hook registration", false).option("--sync [url]", "initialize private sync (optionally with its URL)").option("--publish-target [url]", "configure the sealed public mirror target").option("--yes", "approve creating conventional GitHub repositories", false).option("--skip-codex-hook", "skip Codex hook installation", false).option("--dry-run", "show planned changes without writing", false).option(
|
|
33368
34098
|
"--pending-stale-days <days>",
|
|
33369
34099
|
"sweep `<caveatHome>/pending/<sessionId>/` whose newest entry is older than this (default 7)",
|
|
33370
34100
|
(raw) => {
|
|
@@ -33380,7 +34110,11 @@ program.command("init").description(
|
|
|
33380
34110
|
await runInit(ctx, {
|
|
33381
34111
|
skipClaude: opts.skipClaude,
|
|
33382
34112
|
dryRun: opts.dryRun,
|
|
33383
|
-
pendingStaleDays: opts.pendingStaleDays
|
|
34113
|
+
pendingStaleDays: opts.pendingStaleDays,
|
|
34114
|
+
sync: opts.sync,
|
|
34115
|
+
publishTarget: opts.publishTarget,
|
|
34116
|
+
yes: opts.yes,
|
|
34117
|
+
skipCodexHook: opts.skipCodexHook
|
|
33384
34118
|
});
|
|
33385
34119
|
}
|
|
33386
34120
|
);
|
|
@@ -33388,9 +34122,9 @@ program.command("uninstall").description("Remove Claude Code MCP server and hook
|
|
|
33388
34122
|
const ctx = buildContext(stdoutLogger);
|
|
33389
34123
|
runUninstall(ctx, { dryRun: opts.dryRun });
|
|
33390
34124
|
});
|
|
33391
|
-
program.command("index").description("Index knowledge repo entries into SQLite").option("--full", "full rebuild (DELETE all then rescan)", false).action((opts) => {
|
|
34125
|
+
program.command("index").description("Index knowledge repo entries into SQLite").option("--full", "full rebuild (DELETE all then rescan)", false).action(async (opts) => {
|
|
33392
34126
|
const ctx = buildContext(stdoutLogger);
|
|
33393
|
-
runIndex(ctx, { full: opts.full });
|
|
34127
|
+
await runIndex(ctx, { full: opts.full });
|
|
33394
34128
|
});
|
|
33395
34129
|
program.command("search").description("Search caveats via FTS").argument("<query>", "FTS query (3+ chars for trigram)").option("--source <source>", "own | community | all").option("--tag <tag...>", "filter by tag (repeatable)").option("--confidence <confidence...>", "filter by confidence (repeatable)").action(
|
|
33396
34130
|
(query, opts) => {
|
|
@@ -33432,7 +34166,7 @@ program.command("sync").description("Sync own Caveat entries to a private git re
|
|
|
33432
34166
|
const ctx = buildContext(stdoutLogger);
|
|
33433
34167
|
await runSync(ctx, opts);
|
|
33434
34168
|
});
|
|
33435
|
-
program.command("publish").description("Mirror own public Caveat entries to a public git repository").option("--init <url>", "set the public mirror target").option("--dry-run", "show public mirror changes without committing or pushing", false).option("--yes", "approve creating a default repository or publishing changes", false).action(async (opts) => {
|
|
34169
|
+
program.command("publish").description("Mirror own public Caveat entries to a public git repository").option("--init <url>", "set the public mirror target").option("--dry-run", "show public mirror changes without committing or pushing", false).option("--yes", "approve creating a default repository or publishing changes", false).option("--allow <digest>", "allow a scan finding by its sha256 digest (repeatable)", collectRepeatable, []).option("--save", "persist --allow digests to .caveat-publish-allow.json in the own repo", false).action(async (opts) => {
|
|
33436
34170
|
const ctx = buildContext(stdoutLogger);
|
|
33437
34171
|
await runPublish(ctx, opts);
|
|
33438
34172
|
});
|
|
@@ -33443,12 +34177,12 @@ program.command("mcp-server").description("Run the MCP stdio server (registered
|
|
|
33443
34177
|
await runMcpServer();
|
|
33444
34178
|
});
|
|
33445
34179
|
program.command("hook <name> [arg]").description(
|
|
33446
|
-
"Run a Claude Code hook. name: user-prompt-submit | post-tool-use | stop | worker | reindex"
|
|
34180
|
+
"Run a Claude Code hook. name: user-prompt-submit | post-tool-use | stop | worker | reindex | autosync"
|
|
33447
34181
|
).action(async (name, arg) => {
|
|
33448
34182
|
await runHook(name, arg);
|
|
33449
34183
|
});
|
|
33450
34184
|
var codexHook = program.command("codex-hook").description("Install or run Codex hooks for Caveat");
|
|
33451
|
-
codexHook.command("install").description("Install Caveat hooks into ~/.codex/hooks.json and enable
|
|
34185
|
+
codexHook.command("install").description("Install Caveat hooks into ~/.codex/hooks.json and enable hooks").option("--dry-run", "show planned changes without writing", false).option("--codex-home <path>", "Codex home directory", process.env.CODEX_HOME ?? `${process.env.HOME}/.codex`).action((opts) => {
|
|
33452
34186
|
const cliScriptPath = process.argv[1];
|
|
33453
34187
|
if (!cliScriptPath) {
|
|
33454
34188
|
process.stderr.write("[caveat:error] cannot determine CLI script path\n");
|
|
@@ -33464,7 +34198,12 @@ codexHook.command("install").description("Install Caveat hooks into ~/.codex/hoo
|
|
|
33464
34198
|
stdoutLogger.info(`UserPromptSubmit hook: ${result.hooks.userPromptSubmit}`);
|
|
33465
34199
|
stdoutLogger.info(`PostToolUse hook: ${result.hooks.postToolUse}`);
|
|
33466
34200
|
stdoutLogger.info(`Stop hook: ${result.hooks.stop}`);
|
|
33467
|
-
stdoutLogger.info(`
|
|
34201
|
+
stdoutLogger.info(`hooks feature: ${result.feature}`);
|
|
34202
|
+
if (result.feature === "blocked") {
|
|
34203
|
+
stdoutLogger.warn(
|
|
34204
|
+
`${result.blockedReason}; preserving explicit consent. Set \`hooks = true\` in ${opts.codexHome}/config.toml, then rerun \`caveat codex-hook install\`.`
|
|
34205
|
+
);
|
|
34206
|
+
}
|
|
33468
34207
|
if (result.backupPath) stdoutLogger.info(`hooks.json backed up: ${result.backupPath}`);
|
|
33469
34208
|
if (result.configBackupPath) {
|
|
33470
34209
|
stdoutLogger.info(`config.toml backed up: ${result.configBackupPath}`);
|
|
@@ -33510,7 +34249,7 @@ codexSidecar.command("diagnostics").description("Run codex-sidecar diagnostics f
|
|
|
33510
34249
|
codexSidecar.command("smoke").description("Run a read-only codex_explore smoke through codex-sidecar").option("--project <path>", "repository root to check").option("--preset <preset>", "codex-sidecar read-only preset", "explore").option("--command <command>", "codex-sidecar executable", "codex-sidecar").option("--node-cli <path>", "development path to codex-sidecar CLI JS").option("--save-result <path>", "write structured SidecarResult JSON to this path").action((opts) => {
|
|
33511
34250
|
runCodexSidecarSmoke(stdoutLogger, opts);
|
|
33512
34251
|
});
|
|
33513
|
-
codexSidecar.command("run <workflow> [prompt]").description("Run read-only codex-sidecar with relevant Caveat entries as context").option("--project <path>", "repository root to check").option("--preset <preset>", "codex-sidecar preset").option("--query <query>", "Caveat search query; defaults to prompt text").option("--limit <n>", "maximum caveat entries to pass", (v) => Number(v), 5).option("--source <source>", "own | community | all", "all").option("--visibility <visibility>", "public | private | all", "all").option("--host-agent <agent>", "claude | codex | automation | unknown", "unknown").option("--availability <level>", "disabled | unavailable | configured | operational | work-capable", "operational").option("--sidecar-agent <agent>", "codex | disabled | auto").option("--requires-isolation", "mark the sidecar call as isolation-bounded", false).option("--structured-result-required", "mark structured SidecarResult as the delegation boundary", false).option("--explicit-second-pass", "mark the sidecar call as an explicit second-pass review", false).option("--command <command>", "codex-sidecar executable", "codex-sidecar").option("--node-cli <path>", "development path to codex-sidecar CLI JS").option("--save-result <path>", "write structured SidecarResult JSON to this path").action(
|
|
34252
|
+
codexSidecar.command("run <workflow> [prompt]").description("Run read-only codex-sidecar with relevant Caveat entries as context").option("--project <path>", "repository root to check").option("--preset <preset>", "codex-sidecar preset").option("--query <query>", "Caveat search query; defaults to prompt text").option("--limit <n>", "maximum caveat entries to pass", (v) => Number(v), 5).option("--source <source>", "own | community | all", "all").option("--visibility <visibility>", "public | private | all", "all").option("--host-agent <agent>", "claude | codex | automation | unknown", "unknown").option("--availability <level>", "disabled | unavailable | configured | operational | work-capable", "operational").option("--sidecar-agent <agent>", "codex | disabled | auto").option("--requires-isolation", "mark the sidecar call as isolation-bounded", false).option("--structured-result-required", "mark structured SidecarResult as the delegation boundary", false).option("--explicit-second-pass", "mark the sidecar call as an explicit second-pass review", false).option("--command <command>", "codex-sidecar executable", "codex-sidecar").option("--node-cli <path>", "development path to codex-sidecar CLI JS").option("--save-result <path>", "write structured SidecarResult JSON to this path").option("--additional-context-file <path>", "strictly validated bounded hook-signal context file").action(
|
|
33514
34253
|
(workflow, prompt, opts) => {
|
|
33515
34254
|
const normalized = workflow === "risk" ? "risk-check" : workflow;
|
|
33516
34255
|
if (normalized !== "review" && normalized !== "explore" && normalized !== "opinion" && normalized !== "risk-check") {
|
|
@@ -33527,7 +34266,7 @@ codexSidecar.command("work-smoke [prompt]").description("Run codex_work in an is
|
|
|
33527
34266
|
runCodexSidecarWorkSmoke(ctx, prompt, opts);
|
|
33528
34267
|
}
|
|
33529
34268
|
);
|
|
33530
|
-
var community = program.command("community").description("Manage community caveat repos (shallow clones under <
|
|
34269
|
+
var community = program.command("community").description("Manage community caveat repos (shallow clones under <caveatHome>/community/)");
|
|
33531
34270
|
community.command("add <url>").description("Shallow-clone a GitHub caveat repo into community/<handle>/").action(async (url2) => {
|
|
33532
34271
|
const ctx = buildContext(stdoutLogger);
|
|
33533
34272
|
await runCommunityAdd(ctx, url2);
|