@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.
@@ -0,0 +1,9 @@
1
+ // src/hooks/agent.ts
2
+ function parseAgent(argv) {
3
+ const i = argv.indexOf("--agent");
4
+ return i !== -1 && argv[i + 1] === "codex" ? "codex" : "claude_code";
5
+ }
6
+
7
+ export {
8
+ parseAgent
9
+ };
@@ -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 client.get(`/api/cli/decisions/affecting?${params.toString()}`, {
23
- signal: AbortSignal.timeout(DECISIONS_CHECK_TIMEOUT_MS)
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
@@ -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-PTLXSXIY.js";
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
- if (!EDITING_TOOLS.has(toolName)) {
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);
@@ -3,10 +3,11 @@ import {
3
3
  checkAffectedDecisions,
4
4
  formatDecisionsWarning,
5
5
  getGitContext
6
- } from "../chunk-S47B4VGC.js";
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
- function extractFilePaths(toolName, toolInput) {
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 files = extractFilePaths(toolName, envelope.tool_input).map((f) => toRepoRelative(f, cwd));
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());
@@ -7,7 +7,10 @@ import {
7
7
  scrubFromCwd,
8
8
  shouldFlushAfter,
9
9
  toMove
10
- } from "../chunk-PTLXSXIY.js";
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
- process.stdout.write("{}\n");
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-S47B4VGC.js";
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 homedir2 } from "os";
656
- import { join as join2 } from "path";
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 = join2(homedir2(), ".config", "prim", "daemon.pid");
659
- var SOCK_PATH = join2(homedir2(), ".config", "prim", "sock");
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 client.get(`/api/cli/decisions/cascade?${params.toString()}`, {
1040
- signal: AbortSignal.timeout(CASCADE_TIMEOUT_MS)
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 client.get(`/api/cli/decisions/recent?${params.toString()}`, {
1067
- signal: AbortSignal.timeout(RECENT_TIMEOUT_MS)
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 client.get(`/api/cli/decisions/show?${params.toString()}`, {
1236
- signal: AbortSignal.timeout(SHOW_TIMEOUT_MS)
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 join3 } from "path";
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 = join3(process.cwd(), ".prim");
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 = join3(process.cwd(), WORKSPACE_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 = join3(process.cwd(), WORKSPACE_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 join4 } from "path";
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 join4(SESSIONS_DIR, `${sessionId}.json`);
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(join4(SESSIONS_DIR, f), "utf-8"));
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.17",
4
4
  "description": "CLI for managing Primitive specs, contexts, and git hooks",
5
5
  "type": "module",
6
6
  "license": "MIT",