@primitive.ai/prim 0.1.0-alpha.16 → 0.1.0-alpha.17
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-7YRBACIE.js +9 -0
- package/dist/{chunk-PTLXSXIY.js → chunk-LCC66K45.js} +6 -2
- package/dist/{chunk-S47B4VGC.js → chunk-TPQ3X244.js} +32 -3
- package/dist/daemon/server.js +33 -0
- package/dist/hooks/post-tool-use.js +9 -3
- package/dist/hooks/pre-commit.js +2 -1
- package/dist/hooks/pre-tool-use.js +47 -4
- package/dist/hooks/prim-hook.js +5 -2
- package/dist/hooks/session-start.js +27 -2
- package/dist/index.js +183 -21
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { platform } from "os";
|
|
|
6
6
|
var ENVELOPE_VERSION = 1;
|
|
7
7
|
|
|
8
8
|
// src/hooks/prim-hook-core.ts
|
|
9
|
-
function toMove(parsed, cliVersion) {
|
|
9
|
+
function toMove(parsed, cliVersion, agent = "claude_code") {
|
|
10
10
|
return {
|
|
11
11
|
moveId: randomUUID(),
|
|
12
12
|
capturedAt: Date.now(),
|
|
@@ -18,7 +18,11 @@ function toMove(parsed, cliVersion) {
|
|
|
18
18
|
cliVersion,
|
|
19
19
|
osPlatform: platform()
|
|
20
20
|
},
|
|
21
|
-
envelopeVersion: ENVELOPE_VERSION
|
|
21
|
+
envelopeVersion: ENVELOPE_VERSION,
|
|
22
|
+
// Stamp the producer only for Codex; Claude Code moves omit it (the
|
|
23
|
+
// backend defaults an absent value to "claude_code"), keeping the
|
|
24
|
+
// Claude wire shape byte-identical.
|
|
25
|
+
...agent === "codex" ? { producer: "codex" } : {}
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
function shouldFlushAfter(eventType) {
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient
|
|
3
3
|
} from "./chunk-6SIEWWUL.js";
|
|
4
|
+
import {
|
|
5
|
+
daemonRequest
|
|
6
|
+
} from "./chunk-UTKQTZHL.js";
|
|
7
|
+
|
|
8
|
+
// src/daemon/proxy.ts
|
|
9
|
+
var DAEMON_HTTP_TIMEOUT_MS = 1e4;
|
|
10
|
+
var DAEMON_PROBE_TIMEOUT_MS = 250;
|
|
11
|
+
async function daemonOrDirect(method, params, direct) {
|
|
12
|
+
const fromDaemon = await daemonRequest(method, params, {
|
|
13
|
+
timeoutMs: DAEMON_PROBE_TIMEOUT_MS
|
|
14
|
+
});
|
|
15
|
+
if (fromDaemon !== null) {
|
|
16
|
+
return fromDaemon;
|
|
17
|
+
}
|
|
18
|
+
return await direct();
|
|
19
|
+
}
|
|
20
|
+
async function daemonOrDirectGet(method, path, client, timeoutMs = DAEMON_HTTP_TIMEOUT_MS) {
|
|
21
|
+
return await daemonOrDirect(
|
|
22
|
+
method,
|
|
23
|
+
{ path },
|
|
24
|
+
async () => await client.get(path, {
|
|
25
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
}
|
|
4
29
|
|
|
5
30
|
// src/hooks/decisions-check.ts
|
|
6
31
|
var DECISIONS_CHECK_TIMEOUT_MS = 1e4;
|
|
@@ -19,9 +44,12 @@ async function fetchAffecting(client, batch) {
|
|
|
19
44
|
params.append("files", file);
|
|
20
45
|
}
|
|
21
46
|
try {
|
|
22
|
-
return await
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
return await daemonOrDirectGet(
|
|
48
|
+
"decisions_affecting",
|
|
49
|
+
`/api/cli/decisions/affecting?${params.toString()}`,
|
|
50
|
+
client,
|
|
51
|
+
DECISIONS_CHECK_TIMEOUT_MS
|
|
52
|
+
);
|
|
25
53
|
} catch (err) {
|
|
26
54
|
const detail = err instanceof Error ? err.message : String(err);
|
|
27
55
|
return { decisions: [], truncated: false, unavailable: `decision check failed: ${detail}` };
|
|
@@ -116,6 +144,7 @@ function getGitContext() {
|
|
|
116
144
|
}
|
|
117
145
|
|
|
118
146
|
export {
|
|
147
|
+
daemonOrDirectGet,
|
|
119
148
|
checkAffectedDecisions,
|
|
120
149
|
formatDecisionsWarning,
|
|
121
150
|
getGitContext
|
package/dist/daemon/server.js
CHANGED
|
@@ -16,6 +16,7 @@ var PID_PATH = join(CONFIG_DIR, "daemon.pid");
|
|
|
16
16
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
17
17
|
var TOKEN_CHECK_INTERVAL_MS = 6e4;
|
|
18
18
|
var TOKEN_REFRESH_THRESHOLD_MS = 9e4;
|
|
19
|
+
var HTTP_PROXY_TIMEOUT_MS = 1e4;
|
|
19
20
|
var PRESENCE_FRESH_WINDOW_MS = 9e4;
|
|
20
21
|
var SOCKET_DIR_MODE = 448;
|
|
21
22
|
var PID_FILE_MODE = 384;
|
|
@@ -98,6 +99,22 @@ async function handleConflictCheck(params) {
|
|
|
98
99
|
}
|
|
99
100
|
return await client.post("/api/cli/decisions/conflict-check", { file: params.file });
|
|
100
101
|
}
|
|
102
|
+
function pathParam(params) {
|
|
103
|
+
if (typeof params.path !== "string" || !params.path.startsWith("/api/cli/")) {
|
|
104
|
+
throw new Error("proxy request requires `path: string` under /api/cli/");
|
|
105
|
+
}
|
|
106
|
+
return params.path;
|
|
107
|
+
}
|
|
108
|
+
function assertEndpointPath(path, endpoint) {
|
|
109
|
+
if (path !== endpoint && !path.startsWith(`${endpoint}?`)) {
|
|
110
|
+
throw new Error(`proxy path must be ${endpoint} or ${endpoint}?...`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function proxyGet(params, allowedPrefix) {
|
|
114
|
+
const path = pathParam(params);
|
|
115
|
+
assertEndpointPath(path, allowedPrefix);
|
|
116
|
+
return await client.get(path, { signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS) });
|
|
117
|
+
}
|
|
101
118
|
function handleStatusSnapshot() {
|
|
102
119
|
const presenceFresh = lastOkAtLocal !== void 0 && Date.now() - lastOkAtLocal < PRESENCE_FRESH_WINDOW_MS;
|
|
103
120
|
const presenceStale = lastOkAtLocal !== void 0 && !presenceFresh;
|
|
@@ -120,6 +137,22 @@ async function dispatchRequest(req) {
|
|
|
120
137
|
const result = await handleConflictCheck(req.params ?? {});
|
|
121
138
|
return { id, ok: true, result };
|
|
122
139
|
}
|
|
140
|
+
case "decisions_recent": {
|
|
141
|
+
const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/recent");
|
|
142
|
+
return { id, ok: true, result };
|
|
143
|
+
}
|
|
144
|
+
case "decisions_show": {
|
|
145
|
+
const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/show");
|
|
146
|
+
return { id, ok: true, result };
|
|
147
|
+
}
|
|
148
|
+
case "decisions_cascade": {
|
|
149
|
+
const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/cascade");
|
|
150
|
+
return { id, ok: true, result };
|
|
151
|
+
}
|
|
152
|
+
case "decisions_affecting": {
|
|
153
|
+
const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/affecting");
|
|
154
|
+
return { id, ok: true, result };
|
|
155
|
+
}
|
|
123
156
|
case "session_start": {
|
|
124
157
|
const sid = req.params?.sessionId;
|
|
125
158
|
if (typeof sid === "string" && sid.length > 0) {
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
scrubFromCwd,
|
|
11
11
|
toMove
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-LCC66K45.js";
|
|
13
|
+
import {
|
|
14
|
+
parseAgent
|
|
15
|
+
} from "../chunk-7YRBACIE.js";
|
|
13
16
|
|
|
14
17
|
// src/hooks/post-tool-use.ts
|
|
15
18
|
import { readFileSync } from "fs";
|
|
@@ -36,6 +39,7 @@ function isVerdictFooterContext(value) {
|
|
|
36
39
|
var STDIN_TIMEOUT_MS = 1e3;
|
|
37
40
|
var INGEST_TIMEOUT_MS = 4e3;
|
|
38
41
|
var EDITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
42
|
+
var CODEX_EDITING_TOOLS = /* @__PURE__ */ new Set(["apply_patch"]);
|
|
39
43
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
40
44
|
function resolveCliVersion() {
|
|
41
45
|
try {
|
|
@@ -100,7 +104,9 @@ async function main() {
|
|
|
100
104
|
return;
|
|
101
105
|
}
|
|
102
106
|
const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
|
|
103
|
-
|
|
107
|
+
const agent = parseAgent(process.argv);
|
|
108
|
+
const editingTools = agent === "codex" ? CODEX_EDITING_TOOLS : EDITING_TOOLS;
|
|
109
|
+
if (!editingTools.has(toolName)) {
|
|
104
110
|
emit();
|
|
105
111
|
return;
|
|
106
112
|
}
|
|
@@ -109,7 +115,7 @@ async function main() {
|
|
|
109
115
|
return;
|
|
110
116
|
}
|
|
111
117
|
const cwd = parsed.cwd ?? process.cwd();
|
|
112
|
-
const base = toMove(parsed, resolveCliVersion());
|
|
118
|
+
const base = toMove(parsed, resolveCliVersion(), agent);
|
|
113
119
|
const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
|
|
114
120
|
try {
|
|
115
121
|
const result = await ingestMove(move);
|
package/dist/hooks/pre-commit.js
CHANGED
|
@@ -3,10 +3,11 @@ import {
|
|
|
3
3
|
checkAffectedDecisions,
|
|
4
4
|
formatDecisionsWarning,
|
|
5
5
|
getGitContext
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-TPQ3X244.js";
|
|
7
7
|
import {
|
|
8
8
|
getClient
|
|
9
9
|
} from "../chunk-6SIEWWUL.js";
|
|
10
|
+
import "../chunk-UTKQTZHL.js";
|
|
10
11
|
|
|
11
12
|
// src/hooks/pre-commit.ts
|
|
12
13
|
import { execSync } from "child_process";
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
getClient
|
|
4
4
|
} from "../chunk-6SIEWWUL.js";
|
|
5
|
+
import {
|
|
6
|
+
parseAgent
|
|
7
|
+
} from "../chunk-7YRBACIE.js";
|
|
5
8
|
import {
|
|
6
9
|
daemonRequest
|
|
7
10
|
} from "../chunk-UTKQTZHL.js";
|
|
@@ -43,7 +46,7 @@ function unverifiedNote(results) {
|
|
|
43
46
|
}
|
|
44
47
|
return causes.map((c) => `[primitive] ${c}`).join("\n");
|
|
45
48
|
}
|
|
46
|
-
function buildHookOutput(aggregate, results) {
|
|
49
|
+
function buildHookOutput(aggregate, results, agent = "claude_code") {
|
|
47
50
|
if (aggregate === "deny") {
|
|
48
51
|
const reason = results.filter((r) => r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
|
|
49
52
|
return {
|
|
@@ -54,6 +57,18 @@ function buildHookOutput(aggregate, results) {
|
|
|
54
57
|
}
|
|
55
58
|
};
|
|
56
59
|
}
|
|
60
|
+
if (agent === "codex" && aggregate === "ask") {
|
|
61
|
+
const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
|
|
62
|
+
const context = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
|
|
63
|
+
const merged = [reason, context].filter((s) => s.length > 0).join("\n\n");
|
|
64
|
+
const out = {
|
|
65
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" }
|
|
66
|
+
};
|
|
67
|
+
if (merged.length > 0) {
|
|
68
|
+
out.hookSpecificOutput.additionalContext = merged;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
57
72
|
if (aggregate === "ask") {
|
|
58
73
|
const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] please confirm this edit";
|
|
59
74
|
const additionalContext = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
|
|
@@ -98,7 +113,32 @@ function failOpenOutput() {
|
|
|
98
113
|
};
|
|
99
114
|
}
|
|
100
115
|
var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
101
|
-
|
|
116
|
+
var APPLY_PATCH_FILE_RE = /^\*\*\* (?:Update|Add|Delete) File: (?<path>.+)$/;
|
|
117
|
+
var LINE_SPLIT_RE = /\r?\n/;
|
|
118
|
+
function parseApplyPatchPaths(command) {
|
|
119
|
+
const paths = /* @__PURE__ */ new Set();
|
|
120
|
+
for (const line of command.split(LINE_SPLIT_RE)) {
|
|
121
|
+
const path = APPLY_PATCH_FILE_RE.exec(line)?.groups?.path?.trim();
|
|
122
|
+
if (path) {
|
|
123
|
+
paths.add(path);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return Array.from(paths);
|
|
127
|
+
}
|
|
128
|
+
function extractCodexFilePaths(toolName, toolInput) {
|
|
129
|
+
if (toolName !== "apply_patch") {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const command = toolInput.command;
|
|
136
|
+
return typeof command === "string" ? parseApplyPatchPaths(command) : [];
|
|
137
|
+
}
|
|
138
|
+
function extractFilePaths(toolName, toolInput, agent = "claude_code") {
|
|
139
|
+
if (agent === "codex") {
|
|
140
|
+
return extractCodexFilePaths(toolName, toolInput);
|
|
141
|
+
}
|
|
102
142
|
if (!SUPPORTED_TOOLS.has(toolName)) {
|
|
103
143
|
return [];
|
|
104
144
|
}
|
|
@@ -199,7 +239,10 @@ async function main() {
|
|
|
199
239
|
}
|
|
200
240
|
const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
|
|
201
241
|
const cwd = typeof envelope.cwd === "string" && envelope.cwd.length > 0 ? envelope.cwd : process.cwd();
|
|
202
|
-
const
|
|
242
|
+
const agent = parseAgent(process.argv);
|
|
243
|
+
const files = extractFilePaths(toolName, envelope.tool_input, agent).map(
|
|
244
|
+
(f) => toRepoRelative(f, cwd)
|
|
245
|
+
);
|
|
203
246
|
if (files.length === 0) {
|
|
204
247
|
emit(failOpenOutput());
|
|
205
248
|
return;
|
|
@@ -213,7 +256,7 @@ async function main() {
|
|
|
213
256
|
}
|
|
214
257
|
const rawAggregate = aggregateCheckResults(results);
|
|
215
258
|
const aggregate = demoteForMode(rawAggregate, mode);
|
|
216
|
-
emit(buildHookOutput(aggregate, results));
|
|
259
|
+
emit(buildHookOutput(aggregate, results, agent));
|
|
217
260
|
}
|
|
218
261
|
main().catch(() => {
|
|
219
262
|
emit(failOpenOutput());
|
package/dist/hooks/prim-hook.js
CHANGED
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
scrubFromCwd,
|
|
8
8
|
shouldFlushAfter,
|
|
9
9
|
toMove
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-LCC66K45.js";
|
|
11
|
+
import {
|
|
12
|
+
parseAgent
|
|
13
|
+
} from "../chunk-7YRBACIE.js";
|
|
11
14
|
|
|
12
15
|
// src/hooks/prim-hook.ts
|
|
13
16
|
import { spawn } from "child_process";
|
|
@@ -34,7 +37,7 @@ try {
|
|
|
34
37
|
const raw = readFileSync(0, "utf-8");
|
|
35
38
|
const parsed = JSON.parse(raw);
|
|
36
39
|
const cwd = parsed.cwd ?? process.cwd();
|
|
37
|
-
const base = toMove(parsed, resolveCliVersion());
|
|
40
|
+
const base = toMove(parsed, resolveCliVersion(), parseAgent(process.argv));
|
|
38
41
|
const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
|
|
39
42
|
const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
|
|
40
43
|
appendMove(move, orgId);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseAgent
|
|
4
|
+
} from "../chunk-7YRBACIE.js";
|
|
2
5
|
import {
|
|
3
6
|
daemonRequest
|
|
4
7
|
} from "../chunk-UTKQTZHL.js";
|
|
@@ -23,8 +26,19 @@ function readStdin() {
|
|
|
23
26
|
});
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
|
-
function emit() {
|
|
27
|
-
|
|
29
|
+
function emit(additionalContext) {
|
|
30
|
+
if (!additionalContext) {
|
|
31
|
+
process.stdout.write("{}\n");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const out = {
|
|
35
|
+
hookSpecificOutput: {
|
|
36
|
+
hookEventName: "SessionStart",
|
|
37
|
+
additionalContext
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
process.stdout.write(`${JSON.stringify(out)}
|
|
41
|
+
`);
|
|
28
42
|
}
|
|
29
43
|
async function main() {
|
|
30
44
|
let raw;
|
|
@@ -54,6 +68,17 @@ async function main() {
|
|
|
54
68
|
{ sessionId: envelope.session_id },
|
|
55
69
|
{ timeoutMs: DAEMON_TIMEOUT_MS }
|
|
56
70
|
);
|
|
71
|
+
if (parseAgent(process.argv) === "codex") {
|
|
72
|
+
const snapshot = await daemonRequest(
|
|
73
|
+
"status_snapshot",
|
|
74
|
+
{},
|
|
75
|
+
{ timeoutMs: DAEMON_TIMEOUT_MS }
|
|
76
|
+
);
|
|
77
|
+
if (snapshot && !snapshot.presenceStale && typeof snapshot.onlineCount === "number") {
|
|
78
|
+
emit(`[prim] team: ${snapshot.onlineCount} online`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
57
82
|
emit();
|
|
58
83
|
}
|
|
59
84
|
main().catch(() => {
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
} from "./chunk-BEEGFDGU.js";
|
|
7
7
|
import {
|
|
8
8
|
checkAffectedDecisions,
|
|
9
|
+
daemonOrDirectGet,
|
|
9
10
|
formatDecisionsWarning,
|
|
10
11
|
getGitContext
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-TPQ3X244.js";
|
|
12
13
|
import {
|
|
13
14
|
HttpError,
|
|
14
15
|
REFRESH_TOKEN_PATH,
|
|
@@ -532,6 +533,154 @@ ${line("project", result.project)}`);
|
|
|
532
533
|
});
|
|
533
534
|
}
|
|
534
535
|
|
|
536
|
+
// src/commands/codex-install.ts
|
|
537
|
+
import { homedir as homedir2 } from "os";
|
|
538
|
+
import { join as join2 } from "path";
|
|
539
|
+
var CAPTURE_COMMAND2 = "prim-hook --agent codex";
|
|
540
|
+
var GATE_COMMAND2 = "prim-pre-tool-use --agent codex";
|
|
541
|
+
var POST_TOOL_USE_COMMAND2 = "prim-post-tool-use --agent codex";
|
|
542
|
+
var SESSION_START_COMMAND2 = "prim-session-start --agent codex";
|
|
543
|
+
var JSON_INDENT2 = 2;
|
|
544
|
+
var CODEX_CAPTURE_EVENTS = [
|
|
545
|
+
"SessionStart",
|
|
546
|
+
"UserPromptSubmit",
|
|
547
|
+
"PreToolUse",
|
|
548
|
+
"PostToolUse",
|
|
549
|
+
"Stop",
|
|
550
|
+
"SubagentStop"
|
|
551
|
+
];
|
|
552
|
+
var PRIM_COMMANDS2 = /* @__PURE__ */ new Set([
|
|
553
|
+
CAPTURE_COMMAND2,
|
|
554
|
+
GATE_COMMAND2,
|
|
555
|
+
POST_TOOL_USE_COMMAND2,
|
|
556
|
+
SESSION_START_COMMAND2
|
|
557
|
+
]);
|
|
558
|
+
var CODEX_REGISTRATIONS = [
|
|
559
|
+
...CODEX_CAPTURE_EVENTS.map((event) => ({ event, matcher: "*", command: CAPTURE_COMMAND2 })),
|
|
560
|
+
{ event: "PreToolUse", matcher: "apply_patch", command: GATE_COMMAND2 },
|
|
561
|
+
{ event: "PostToolUse", matcher: "apply_patch", command: POST_TOOL_USE_COMMAND2 },
|
|
562
|
+
{ event: "SessionStart", matcher: "*", command: SESSION_START_COMMAND2 }
|
|
563
|
+
];
|
|
564
|
+
var USER_SCOPE_PATH2 = join2(homedir2(), ".codex", "hooks.json");
|
|
565
|
+
var PROJECT_SCOPE_PATH2 = join2(process.cwd(), ".codex", "hooks.json");
|
|
566
|
+
function settingsPathFor2(scope) {
|
|
567
|
+
return scope === "user" ? USER_SCOPE_PATH2 : PROJECT_SCOPE_PATH2;
|
|
568
|
+
}
|
|
569
|
+
function applyInstall2(settings, options = {}) {
|
|
570
|
+
const hooks = { ...settings.hooks ?? {} };
|
|
571
|
+
for (const reg of CODEX_REGISTRATIONS) {
|
|
572
|
+
hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
|
|
573
|
+
}
|
|
574
|
+
return { ...settings, hooks };
|
|
575
|
+
}
|
|
576
|
+
function applyUninstall2(settings) {
|
|
577
|
+
const source = settings.hooks ?? {};
|
|
578
|
+
const hooks = {};
|
|
579
|
+
for (const event of Object.keys(source)) {
|
|
580
|
+
let list = source[event] ?? [];
|
|
581
|
+
for (const command of PRIM_COMMANDS2) {
|
|
582
|
+
list = stripCommand(list, command);
|
|
583
|
+
}
|
|
584
|
+
if (list.length > 0) {
|
|
585
|
+
hooks[event] = list;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return { ...settings, hooks };
|
|
589
|
+
}
|
|
590
|
+
function captureInstalled2(settings) {
|
|
591
|
+
return CODEX_CAPTURE_EVENTS.some(
|
|
592
|
+
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND2))
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
function isGateInstalled2(settings) {
|
|
596
|
+
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND2));
|
|
597
|
+
}
|
|
598
|
+
function resultFor(scope, path, after, changed) {
|
|
599
|
+
return {
|
|
600
|
+
scope,
|
|
601
|
+
path,
|
|
602
|
+
gate: isGateInstalled2(after),
|
|
603
|
+
capture: captureInstalled2(after),
|
|
604
|
+
changed
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function performInstall2(scope, force) {
|
|
608
|
+
const path = settingsPathFor2(scope);
|
|
609
|
+
const before = readSettings(path);
|
|
610
|
+
const after = applyInstall2(before, { force });
|
|
611
|
+
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
612
|
+
if (changed) {
|
|
613
|
+
atomicWrite(path, after);
|
|
614
|
+
}
|
|
615
|
+
return resultFor(scope, path, after, changed);
|
|
616
|
+
}
|
|
617
|
+
function performUninstall2(scope) {
|
|
618
|
+
const path = settingsPathFor2(scope);
|
|
619
|
+
const before = readSettings(path);
|
|
620
|
+
const after = applyUninstall2(before);
|
|
621
|
+
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
622
|
+
if (changed) {
|
|
623
|
+
atomicWrite(path, after);
|
|
624
|
+
}
|
|
625
|
+
return resultFor(scope, path, after, changed);
|
|
626
|
+
}
|
|
627
|
+
function performStatus2() {
|
|
628
|
+
const statusFor = (path) => {
|
|
629
|
+
const settings = readSettings(path);
|
|
630
|
+
return { path, gate: isGateInstalled2(settings), capture: captureInstalled2(settings) };
|
|
631
|
+
};
|
|
632
|
+
return { user: statusFor(USER_SCOPE_PATH2), project: statusFor(PROJECT_SCOPE_PATH2) };
|
|
633
|
+
}
|
|
634
|
+
function resolveScope2(input) {
|
|
635
|
+
if (input === void 0 || input === "user") {
|
|
636
|
+
return "user";
|
|
637
|
+
}
|
|
638
|
+
if (input === "project") {
|
|
639
|
+
return "project";
|
|
640
|
+
}
|
|
641
|
+
console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
var TRUST_NOTICE = "[prim] Codex requires hook trust: run `/hooks` in Codex to review and trust these hooks (or start Codex with --dangerously-bypass-hook-trust). Until trusted, the hooks will not fire.";
|
|
645
|
+
function registerCodexCommands(program2) {
|
|
646
|
+
const codex = program2.command("codex").description("Manage the prim Codex integration (capture, gate, ingest, presence)");
|
|
647
|
+
codex.command("install").description("Register the prim hooks in Codex's ~/.codex/hooks.json").option(
|
|
648
|
+
"--scope <scope>",
|
|
649
|
+
"user (default, ~/.codex/hooks.json) or project (./.codex/hooks.json)"
|
|
650
|
+
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
651
|
+
const scope = resolveScope2(opts.scope);
|
|
652
|
+
const result = performInstall2(scope, opts.force ?? false);
|
|
653
|
+
if (result.changed) {
|
|
654
|
+
console.error(`[prim] Codex integration installed (${scope} scope) at ${result.path}`);
|
|
655
|
+
} else {
|
|
656
|
+
console.error(`[prim] Codex integration already present at ${result.path} (no changes)`);
|
|
657
|
+
}
|
|
658
|
+
console.error(TRUST_NOTICE);
|
|
659
|
+
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
660
|
+
});
|
|
661
|
+
codex.command("uninstall").description("Remove all prim hooks from ~/.codex/hooks.json").option(
|
|
662
|
+
"--scope <scope>",
|
|
663
|
+
"user (default, ~/.codex/hooks.json) or project (./.codex/hooks.json)"
|
|
664
|
+
).action((opts) => {
|
|
665
|
+
const scope = resolveScope2(opts.scope);
|
|
666
|
+
const result = performUninstall2(scope);
|
|
667
|
+
if (result.changed) {
|
|
668
|
+
console.error(`[prim] prim hooks removed from ${result.path}`);
|
|
669
|
+
} else {
|
|
670
|
+
console.error(`[prim] no prim hooks to remove at ${result.path} (nothing changed)`);
|
|
671
|
+
}
|
|
672
|
+
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
673
|
+
});
|
|
674
|
+
codex.command("status").description("Report whether each prim surface (gate, capture) is installed per scope").action(() => {
|
|
675
|
+
const result = performStatus2();
|
|
676
|
+
const mark = (b) => b ? "\u2713" : "\u2717";
|
|
677
|
+
const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} (${s.path})`;
|
|
678
|
+
console.error(`${line("user", result.user)}
|
|
679
|
+
${line("project", result.project)}`);
|
|
680
|
+
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
535
684
|
// src/commands/context.ts
|
|
536
685
|
import { readFileSync as readFileSync3 } from "fs";
|
|
537
686
|
function registerContextCommands(program2) {
|
|
@@ -652,11 +801,11 @@ ${contexts.length} context(s)`);
|
|
|
652
801
|
// src/commands/daemon.ts
|
|
653
802
|
import { spawn } from "child_process";
|
|
654
803
|
import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
655
|
-
import { homedir as
|
|
656
|
-
import { join as
|
|
804
|
+
import { homedir as homedir3 } from "os";
|
|
805
|
+
import { join as join3 } from "path";
|
|
657
806
|
var DAEMON_BIN = "prim-daemon-server";
|
|
658
|
-
var PID_PATH =
|
|
659
|
-
var SOCK_PATH =
|
|
807
|
+
var PID_PATH = join3(homedir3(), ".config", "prim", "daemon.pid");
|
|
808
|
+
var SOCK_PATH = join3(homedir3(), ".config", "prim", "sock");
|
|
660
809
|
var STOP_TIMEOUT_MS = 5e3;
|
|
661
810
|
var STOP_POLL_MS = 100;
|
|
662
811
|
var STATUS_PROBE_TIMEOUT_MS = 500;
|
|
@@ -1036,9 +1185,12 @@ async function fetchCascade(idOrShortId, deps = defaultDeps) {
|
|
|
1036
1185
|
const params = new URLSearchParams({ id: idOrShortId });
|
|
1037
1186
|
const client = deps.getClient();
|
|
1038
1187
|
try {
|
|
1039
|
-
return await
|
|
1040
|
-
|
|
1041
|
-
|
|
1188
|
+
return await daemonOrDirectGet(
|
|
1189
|
+
"decisions_cascade",
|
|
1190
|
+
`/api/cli/decisions/cascade?${params.toString()}`,
|
|
1191
|
+
client,
|
|
1192
|
+
CASCADE_TIMEOUT_MS
|
|
1193
|
+
);
|
|
1042
1194
|
} catch (err) {
|
|
1043
1195
|
if (err instanceof Error && NOT_FOUND_RE.test(err.message)) {
|
|
1044
1196
|
throw new CascadeNotFoundError(idOrShortId);
|
|
@@ -1063,9 +1215,12 @@ async function fetchRecent(args, deps = defaultDeps2) {
|
|
|
1063
1215
|
}
|
|
1064
1216
|
const client = deps.getClient();
|
|
1065
1217
|
try {
|
|
1066
|
-
const res = await
|
|
1067
|
-
|
|
1068
|
-
|
|
1218
|
+
const res = await daemonOrDirectGet(
|
|
1219
|
+
"decisions_recent",
|
|
1220
|
+
`/api/cli/decisions/recent?${params.toString()}`,
|
|
1221
|
+
client,
|
|
1222
|
+
RECENT_TIMEOUT_MS
|
|
1223
|
+
);
|
|
1069
1224
|
const result = { decisions: res.decisions };
|
|
1070
1225
|
if (res.unavailable !== void 0) {
|
|
1071
1226
|
result.unavailable = res.unavailable;
|
|
@@ -1092,6 +1247,8 @@ function authorLabel(row) {
|
|
|
1092
1247
|
switch (row.producerKind) {
|
|
1093
1248
|
case "claude_code":
|
|
1094
1249
|
return "Your Claude Code";
|
|
1250
|
+
case "codex":
|
|
1251
|
+
return "Your Codex";
|
|
1095
1252
|
case "chat":
|
|
1096
1253
|
return "Your chat";
|
|
1097
1254
|
case "spec_edit":
|
|
@@ -1232,9 +1389,12 @@ async function fetchShow(idOrShortId, deps = defaultDeps4) {
|
|
|
1232
1389
|
const params = new URLSearchParams({ id: idOrShortId });
|
|
1233
1390
|
const client = deps.getClient();
|
|
1234
1391
|
try {
|
|
1235
|
-
return await
|
|
1236
|
-
|
|
1237
|
-
|
|
1392
|
+
return await daemonOrDirectGet(
|
|
1393
|
+
"decisions_show",
|
|
1394
|
+
`/api/cli/decisions/show?${params.toString()}`,
|
|
1395
|
+
client,
|
|
1396
|
+
SHOW_TIMEOUT_MS
|
|
1397
|
+
);
|
|
1238
1398
|
} catch (err) {
|
|
1239
1399
|
if (err instanceof Error && NOT_FOUND_RE3.test(err.message)) {
|
|
1240
1400
|
throw new DecisionNotFoundError(idOrShortId);
|
|
@@ -1572,7 +1732,7 @@ function registerHooksCommands(program2) {
|
|
|
1572
1732
|
|
|
1573
1733
|
// src/commands/moves.ts
|
|
1574
1734
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1575
|
-
import { join as
|
|
1735
|
+
import { join as join4 } from "path";
|
|
1576
1736
|
|
|
1577
1737
|
// src/flusher.ts
|
|
1578
1738
|
import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
@@ -1681,18 +1841,18 @@ function registerMovesCommands(program2) {
|
|
|
1681
1841
|
}
|
|
1682
1842
|
});
|
|
1683
1843
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
1684
|
-
const dir =
|
|
1844
|
+
const dir = join4(process.cwd(), ".prim");
|
|
1685
1845
|
if (!existsSync5(dir)) {
|
|
1686
1846
|
mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
|
|
1687
1847
|
}
|
|
1688
|
-
const file =
|
|
1848
|
+
const file = join4(process.cwd(), WORKSPACE_FILE);
|
|
1689
1849
|
writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
1690
1850
|
mode: FILE_MODE2
|
|
1691
1851
|
});
|
|
1692
1852
|
console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
|
|
1693
1853
|
});
|
|
1694
1854
|
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
1695
|
-
const file =
|
|
1855
|
+
const file = join4(process.cwd(), WORKSPACE_FILE);
|
|
1696
1856
|
if (!existsSync5(file)) {
|
|
1697
1857
|
console.log("[prim] no workspace binding in cwd");
|
|
1698
1858
|
return;
|
|
@@ -1815,7 +1975,7 @@ import {
|
|
|
1815
1975
|
unlinkSync as unlinkSync5,
|
|
1816
1976
|
writeFileSync as writeFileSync5
|
|
1817
1977
|
} from "fs";
|
|
1818
|
-
import { join as
|
|
1978
|
+
import { join as join5 } from "path";
|
|
1819
1979
|
var DIR_MODE2 = 448;
|
|
1820
1980
|
var FILE_MODE3 = 384;
|
|
1821
1981
|
function ensureDir() {
|
|
@@ -1824,7 +1984,7 @@ function ensureDir() {
|
|
|
1824
1984
|
}
|
|
1825
1985
|
}
|
|
1826
1986
|
function markerPath(sessionId) {
|
|
1827
|
-
return
|
|
1987
|
+
return join5(SESSIONS_DIR, `${sessionId}.json`);
|
|
1828
1988
|
}
|
|
1829
1989
|
function registerSessionCommands(program2) {
|
|
1830
1990
|
const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
|
|
@@ -1852,7 +2012,7 @@ function registerSessionCommands(program2) {
|
|
|
1852
2012
|
for (const f of files) {
|
|
1853
2013
|
const sessionId = f.replace(/\.json$/, "");
|
|
1854
2014
|
try {
|
|
1855
|
-
const m = JSON.parse(readFileSync6(
|
|
2015
|
+
const m = JSON.parse(readFileSync6(join5(SESSIONS_DIR, f), "utf-8"));
|
|
1856
2016
|
console.log(`${sessionId} org=${m.orgId}`);
|
|
1857
2017
|
} catch {
|
|
1858
2018
|
}
|
|
@@ -1887,6 +2047,7 @@ var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
|
|
|
1887
2047
|
var SKILL_END = "<!-- END PRIM SKILL v1 -->";
|
|
1888
2048
|
var TARGET_CANDIDATES = [
|
|
1889
2049
|
"CLAUDE.md",
|
|
2050
|
+
"AGENTS.md",
|
|
1890
2051
|
".cursor/rules",
|
|
1891
2052
|
".windsurfrules",
|
|
1892
2053
|
".github/instructions/primitive.md"
|
|
@@ -2366,6 +2527,7 @@ registerMovesCommands(program);
|
|
|
2366
2527
|
registerSessionCommands(program);
|
|
2367
2528
|
registerDecisionsCommands(program);
|
|
2368
2529
|
registerClaudeCommands(program);
|
|
2530
|
+
registerCodexCommands(program);
|
|
2369
2531
|
registerDaemonCommands(program);
|
|
2370
2532
|
registerReconcileCommands(program);
|
|
2371
2533
|
registerStatuslineCommands(program);
|