@primitive.ai/prim 0.1.0-alpha.18 → 0.1.0-alpha.19

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.
Files changed (2) hide show
  1. package/dist/index.js +263 -169
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -34,9 +34,9 @@ import {
34
34
  } from "./chunk-UTKQTZHL.js";
35
35
 
36
36
  // src/index.ts
37
- import { readFileSync as readFileSync10 } from "fs";
38
- import { dirname as dirname5, resolve as resolve4 } from "path";
39
- import { fileURLToPath as fileURLToPath3 } from "url";
37
+ import { readFileSync as readFileSync11 } from "fs";
38
+ import { dirname as dirname6, resolve as resolve4 } from "path";
39
+ import { fileURLToPath as fileURLToPath4 } from "url";
40
40
  import { Command } from "commander";
41
41
  import updateNotifier from "update-notifier";
42
42
 
@@ -125,7 +125,8 @@ function registerAuthCommands(program2) {
125
125
  res.end("<h1>Authentication successful!</h1><p>You can close this tab.</p>");
126
126
  exchangeCode(siteUrl, code, verifier, `http://${LOCALHOST}:${port}/callback`).then((token) => {
127
127
  saveToken(token);
128
- console.log(`Authenticated! Token saved to ${TOKEN_FILE_PATH}`);
128
+ console.error(`Authenticated! Token saved to ${TOKEN_FILE_PATH}`);
129
+ console.log(JSON.stringify({ authenticated: true, tokenFile: TOKEN_FILE_PATH }));
129
130
  server.close();
130
131
  process.exit(0);
131
132
  }).catch((err) => {
@@ -151,12 +152,12 @@ function registerAuthCommands(program2) {
151
152
  authUrl.searchParams.set("state", state);
152
153
  authUrl.searchParams.set("code_challenge", challenge);
153
154
  authUrl.searchParams.set("code_challenge_method", "S256");
154
- console.log("Opening browser for authentication...");
155
+ console.error("Opening browser for authentication...");
155
156
  openBrowser(authUrl.toString());
156
- console.log(`If the browser doesn't open, visit:
157
+ console.error(`If the browser doesn't open, visit:
157
158
  ${authUrl.toString()}
158
159
  `);
159
- console.log("Waiting for callback...");
160
+ console.error("Waiting for callback...");
160
161
  setTimeout(() => {
161
162
  console.error("Authentication timed out.");
162
163
  server.close();
@@ -281,33 +282,94 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
281
282
  // src/commands/claude-install.ts
282
283
  import {
283
284
  closeSync,
284
- existsSync as existsSync2,
285
+ existsSync as existsSync3,
285
286
  fsyncSync,
286
287
  mkdirSync as mkdirSync2,
287
288
  openSync,
288
- readFileSync as readFileSync2,
289
+ readFileSync as readFileSync3,
289
290
  renameSync,
290
291
  writeFileSync as writeFileSync2
291
292
  } from "fs";
292
293
  import { homedir } from "os";
293
- import { dirname as dirname2, join } from "path";
294
- var CAPTURE_COMMAND = "prim-hook";
295
- var GATE_COMMAND = "prim-pre-tool-use";
296
- var POST_TOOL_USE_COMMAND = "prim-post-tool-use";
297
- var SESSION_START_COMMAND = "prim-session-start";
298
- var SESSION_END_COMMAND = "prim-session-end";
299
- var STATUSLINE_COMMAND = "prim statusline";
294
+ import { dirname as dirname3, join as join2 } from "path";
295
+
296
+ // src/lib/bin-path.ts
297
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
298
+ import { dirname as dirname2, isAbsolute, join } from "path";
299
+ import { fileURLToPath } from "url";
300
+ var PKG_NAME = "@primitive.ai/prim";
301
+ var ROOT_WALK_LIMIT = 6;
302
+ var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
303
+ var resolvedRoot;
304
+ function locateRoot() {
305
+ if (resolvedRoot !== void 0) {
306
+ return resolvedRoot;
307
+ }
308
+ let dir = dirname2(fileURLToPath(import.meta.url));
309
+ for (let depth = 0; depth < ROOT_WALK_LIMIT; depth++) {
310
+ const manifestPath = join(dir, "package.json");
311
+ if (existsSync2(manifestPath)) {
312
+ try {
313
+ const manifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
314
+ if (manifest.name === PKG_NAME && manifest.bin) {
315
+ resolvedRoot = { dir, bin: manifest.bin };
316
+ return resolvedRoot;
317
+ }
318
+ } catch {
319
+ }
320
+ }
321
+ const parent = dirname2(dir);
322
+ if (parent === dir) {
323
+ break;
324
+ }
325
+ dir = parent;
326
+ }
327
+ resolvedRoot = null;
328
+ return resolvedRoot;
329
+ }
330
+ function binFile(bin) {
331
+ const root = locateRoot();
332
+ const rel = root?.bin[bin];
333
+ if (!root || !rel) {
334
+ return null;
335
+ }
336
+ return isAbsolute(rel) ? rel : join(root.dir, rel);
337
+ }
338
+ function hookShimCommand(bin, args = "") {
339
+ const invoke = (cmd) => args ? `${cmd} ${args}` : cmd;
340
+ return `if command -v ${bin} >/dev/null 2>&1; then ${invoke(bin)}; elif [ -f "./node_modules/.bin/${bin}" ]; then ${invoke(`./node_modules/.bin/${bin}`)}; else ${invoke(`${NPX_FALLBACK} ${bin}`)}; fi`;
341
+ }
342
+ function commandMatchesBin(command, bin) {
343
+ if (!command) {
344
+ return false;
345
+ }
346
+ const c = command.trim();
347
+ if (c === bin || c.startsWith(`${bin} `)) {
348
+ return true;
349
+ }
350
+ return c.includes(`command -v ${bin} `);
351
+ }
352
+
353
+ // src/commands/claude-install.ts
354
+ var CAPTURE_BIN = "prim-hook";
355
+ var GATE_BIN = "prim-pre-tool-use";
356
+ var POST_TOOL_USE_BIN = "prim-post-tool-use";
357
+ var SESSION_START_BIN = "prim-session-start";
358
+ var SESSION_END_BIN = "prim-session-end";
359
+ var STATUSLINE_BIN = "prim";
360
+ var STATUSLINE_ARGS = "statusline";
361
+ var STATUSLINE_COMMAND = hookShimCommand(STATUSLINE_BIN, STATUSLINE_ARGS);
300
362
  var STATUSLINE_REFRESH_SECONDS = 5;
301
- var PRIM_COMMANDS = /* @__PURE__ */ new Set([
302
- CAPTURE_COMMAND,
303
- GATE_COMMAND,
304
- POST_TOOL_USE_COMMAND,
305
- SESSION_START_COMMAND,
306
- SESSION_END_COMMAND
307
- ]);
363
+ var PRIM_BINS = [
364
+ CAPTURE_BIN,
365
+ GATE_BIN,
366
+ POST_TOOL_USE_BIN,
367
+ SESSION_START_BIN,
368
+ SESSION_END_BIN
369
+ ];
308
370
  var JSON_INDENT = 2;
309
- var USER_SCOPE_PATH = join(homedir(), ".claude", "settings.json");
310
- var PROJECT_SCOPE_PATH = join(process.cwd(), ".claude", "settings.json");
371
+ var USER_SCOPE_PATH = join2(homedir(), ".claude", "settings.json");
372
+ var PROJECT_SCOPE_PATH = join2(process.cwd(), ".claude", "settings.json");
311
373
  var CAPTURE_EVENTS = [
312
374
  "SessionStart",
313
375
  "UserPromptSubmit",
@@ -317,21 +379,24 @@ var CAPTURE_EVENTS = [
317
379
  "SessionEnd",
318
380
  "SubagentStop"
319
381
  ];
382
+ function makeRegistration(event, matcher, bin, args = "") {
383
+ return { event, matcher, bin, command: hookShimCommand(bin, args) };
384
+ }
320
385
  var REGISTRATIONS = [
321
- ...CAPTURE_EVENTS.map((event) => ({ event, matcher: "*", command: CAPTURE_COMMAND })),
322
- { event: "PreToolUse", matcher: "Edit|Write|MultiEdit", command: GATE_COMMAND },
323
- { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", command: POST_TOOL_USE_COMMAND },
324
- { event: "SessionStart", matcher: "*", command: SESSION_START_COMMAND },
325
- { event: "SessionEnd", matcher: "*", command: SESSION_END_COMMAND }
386
+ ...CAPTURE_EVENTS.map((event) => makeRegistration(event, "*", CAPTURE_BIN)),
387
+ makeRegistration("PreToolUse", "Edit|Write|MultiEdit", GATE_BIN),
388
+ makeRegistration("PostToolUse", "Edit|Write|MultiEdit", POST_TOOL_USE_BIN),
389
+ makeRegistration("SessionStart", "*", SESSION_START_BIN),
390
+ makeRegistration("SessionEnd", "*", SESSION_END_BIN)
326
391
  ];
327
392
  function settingsPathFor(scope) {
328
393
  return scope === "user" ? USER_SCOPE_PATH : PROJECT_SCOPE_PATH;
329
394
  }
330
395
  function readSettings(path) {
331
- if (!existsSync2(path)) {
396
+ if (!existsSync3(path)) {
332
397
  return {};
333
398
  }
334
- const raw = readFileSync2(path, "utf-8");
399
+ const raw = readFileSync3(path, "utf-8");
335
400
  try {
336
401
  return JSON.parse(raw);
337
402
  } catch (err) {
@@ -339,16 +404,16 @@ function readSettings(path) {
339
404
  throw new Error(`${path} is not valid JSON: ${detail}`);
340
405
  }
341
406
  }
342
- function entryHasCommand(entry, command) {
343
- return entry.hooks?.some((h) => h.command === command) ?? false;
407
+ function entryHasCommand(entry, bin) {
408
+ return entry.hooks?.some((h) => commandMatchesBin(h.command, bin)) ?? false;
344
409
  }
345
410
  function canonicalEntry(reg) {
346
411
  return { matcher: reg.matcher, hooks: [{ type: "command", command: reg.command }] };
347
412
  }
348
- function stripCommand(list, command) {
413
+ function stripCommand(list, bin) {
349
414
  const out = [];
350
415
  for (const e of list) {
351
- const hooks = (e.hooks ?? []).filter((h) => h.command !== command);
416
+ const hooks = (e.hooks ?? []).filter((h) => !commandMatchesBin(h.command, bin));
352
417
  if (hooks.length > 0) {
353
418
  out.push({ ...e, hooks });
354
419
  }
@@ -362,11 +427,16 @@ function ensureRegistration(list, reg, force) {
362
427
  if (hasCanonical && !force) {
363
428
  return list;
364
429
  }
365
- return [...stripCommand(list, reg.command), canonicalEntry(reg)];
430
+ return [...stripCommand(list, reg.bin), canonicalEntry(reg)];
366
431
  }
432
+ var LEGACY_STATUSLINE_COMMAND = "prim statusline";
367
433
  function isPrimStatusLine(settings) {
368
434
  const s = settings.statusLine;
369
- return s?.type === "command" && s?.command === STATUSLINE_COMMAND;
435
+ if (s?.type !== "command") {
436
+ return false;
437
+ }
438
+ const c = (s.command ?? "").trim();
439
+ return c === LEGACY_STATUSLINE_COMMAND || c.includes("@primitive.ai/prim") && c.includes("statusline");
370
440
  }
371
441
  function applyStatusLine(settings) {
372
442
  if (settings.statusLine && !isPrimStatusLine(settings)) {
@@ -393,8 +463,8 @@ function applyUninstall(settings) {
393
463
  const hooks = {};
394
464
  for (const event of Object.keys(source)) {
395
465
  let list = source[event] ?? [];
396
- for (const command of PRIM_COMMANDS) {
397
- list = stripCommand(list, command);
466
+ for (const bin of PRIM_BINS) {
467
+ list = stripCommand(list, bin);
398
468
  }
399
469
  if (list.length > 0) {
400
470
  hooks[event] = list;
@@ -408,18 +478,18 @@ function applyUninstall(settings) {
408
478
  }
409
479
  function captureInstalled(settings) {
410
480
  return CAPTURE_EVENTS.some(
411
- (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND))
481
+ (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN))
412
482
  );
413
483
  }
414
484
  function statuslineInstalled(settings) {
415
485
  return isPrimStatusLine(settings);
416
486
  }
417
487
  function isGateInstalled(settings) {
418
- return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND));
488
+ return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN));
419
489
  }
420
490
  function atomicWrite(path, content) {
421
- const dir = dirname2(path);
422
- if (!existsSync2(dir)) {
491
+ const dir = dirname3(path);
492
+ if (!existsSync3(dir)) {
423
493
  mkdirSync2(dir, { recursive: true });
424
494
  }
425
495
  const tmp = `${path}.tmp.${String(Date.now())}`;
@@ -535,11 +605,12 @@ ${line("project", result.project)}`);
535
605
 
536
606
  // src/commands/codex-install.ts
537
607
  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";
608
+ import { join as join3 } from "path";
609
+ var CAPTURE_BIN2 = "prim-hook";
610
+ var GATE_BIN2 = "prim-pre-tool-use";
611
+ var POST_TOOL_USE_BIN2 = "prim-post-tool-use";
612
+ var SESSION_START_BIN2 = "prim-session-start";
613
+ var CODEX_ARGS = "--agent codex";
543
614
  var JSON_INDENT2 = 2;
544
615
  var CODEX_CAPTURE_EVENTS = [
545
616
  "SessionStart",
@@ -549,20 +620,15 @@ var CODEX_CAPTURE_EVENTS = [
549
620
  "Stop",
550
621
  "SubagentStop"
551
622
  ];
552
- var PRIM_COMMANDS2 = /* @__PURE__ */ new Set([
553
- CAPTURE_COMMAND2,
554
- GATE_COMMAND2,
555
- POST_TOOL_USE_COMMAND2,
556
- SESSION_START_COMMAND2
557
- ]);
623
+ var PRIM_BINS2 = [CAPTURE_BIN2, GATE_BIN2, POST_TOOL_USE_BIN2, SESSION_START_BIN2];
558
624
  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 }
625
+ ...CODEX_CAPTURE_EVENTS.map((event) => makeRegistration(event, "*", CAPTURE_BIN2, CODEX_ARGS)),
626
+ makeRegistration("PreToolUse", "apply_patch", GATE_BIN2, CODEX_ARGS),
627
+ makeRegistration("PostToolUse", "apply_patch", POST_TOOL_USE_BIN2, CODEX_ARGS),
628
+ makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS)
563
629
  ];
564
- var USER_SCOPE_PATH2 = join2(homedir2(), ".codex", "hooks.json");
565
- var PROJECT_SCOPE_PATH2 = join2(process.cwd(), ".codex", "hooks.json");
630
+ var USER_SCOPE_PATH2 = join3(homedir2(), ".codex", "hooks.json");
631
+ var PROJECT_SCOPE_PATH2 = join3(process.cwd(), ".codex", "hooks.json");
566
632
  function settingsPathFor2(scope) {
567
633
  return scope === "user" ? USER_SCOPE_PATH2 : PROJECT_SCOPE_PATH2;
568
634
  }
@@ -578,8 +644,8 @@ function applyUninstall2(settings) {
578
644
  const hooks = {};
579
645
  for (const event of Object.keys(source)) {
580
646
  let list = source[event] ?? [];
581
- for (const command of PRIM_COMMANDS2) {
582
- list = stripCommand(list, command);
647
+ for (const bin of PRIM_BINS2) {
648
+ list = stripCommand(list, bin);
583
649
  }
584
650
  if (list.length > 0) {
585
651
  hooks[event] = list;
@@ -589,11 +655,11 @@ function applyUninstall2(settings) {
589
655
  }
590
656
  function captureInstalled2(settings) {
591
657
  return CODEX_CAPTURE_EVENTS.some(
592
- (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND2))
658
+ (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN2))
593
659
  );
594
660
  }
595
661
  function isGateInstalled2(settings) {
596
- return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND2));
662
+ return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN2));
597
663
  }
598
664
  function resultFor(scope, path, after, changed) {
599
665
  return {
@@ -682,7 +748,7 @@ ${line("project", result.project)}`);
682
748
  }
683
749
 
684
750
  // src/commands/context.ts
685
- import { readFileSync as readFileSync3 } from "fs";
751
+ import { readFileSync as readFileSync4 } from "fs";
686
752
  function registerContextCommands(program2) {
687
753
  const context = program2.command("context").description("Manage contexts");
688
754
  context.command("list").description("List contexts").option("-s, --scope <scope>", "Filter by scope: project, global, external").option("-t, --project-id <projectId>", "List contexts linked to a specific project").option("--json", "Output as JSON").action(async (opts) => {
@@ -711,7 +777,7 @@ function registerContextCommands(program2) {
711
777
  const client = getClient();
712
778
  let text = opts.text;
713
779
  if (opts.file) {
714
- text = readFileSync3(opts.file, "utf-8");
780
+ text = readFileSync4(opts.file, "utf-8");
715
781
  }
716
782
  const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
717
783
  const result = await client.post("/api/cli/contexts", {
@@ -734,7 +800,7 @@ function registerContextCommands(program2) {
734
800
  const client = getClient();
735
801
  let text = opts.text;
736
802
  if (opts.file) {
737
- text = readFileSync3(opts.file, "utf-8");
803
+ text = readFileSync4(opts.file, "utf-8");
738
804
  }
739
805
  await client.patch(`/api/cli/contexts/${contextId}`, {
740
806
  name: opts.name,
@@ -800,22 +866,26 @@ ${contexts.length} context(s)`);
800
866
 
801
867
  // src/commands/daemon.ts
802
868
  import { spawn } from "child_process";
803
- import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
869
+ import { existsSync as existsSync4, readFileSync as readFileSync5, unlinkSync } from "fs";
804
870
  import { homedir as homedir3 } from "os";
805
- import { join as join3 } from "path";
871
+ import { join as join4 } from "path";
806
872
  var DAEMON_BIN = "prim-daemon-server";
807
- var PID_PATH = join3(homedir3(), ".config", "prim", "daemon.pid");
808
- var SOCK_PATH = join3(homedir3(), ".config", "prim", "sock");
873
+ var PID_PATH = join4(homedir3(), ".config", "prim", "daemon.pid");
874
+ var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
809
875
  var STOP_TIMEOUT_MS = 5e3;
810
876
  var STOP_POLL_MS = 100;
811
877
  var STATUS_PROBE_TIMEOUT_MS = 500;
812
- var POST_START_WAIT_MS = 400;
878
+ var READY_TIMEOUT_MS = 5e3;
879
+ var READY_POLL_MS = 100;
880
+ var READY_PROBE_TIMEOUT_MS = 250;
881
+ var EXIT_OK = 0;
813
882
  var EXIT_NOT_RUNNING = 2;
883
+ var EXIT_BOOTING = 3;
814
884
  function readPidfile() {
815
- if (!existsSync3(PID_PATH)) {
885
+ if (!existsSync4(PID_PATH)) {
816
886
  return null;
817
887
  }
818
- const raw = readFileSync4(PID_PATH, "utf-8").trim();
888
+ const raw = readFileSync5(PID_PATH, "utf-8").trim();
819
889
  const pid = Number(raw);
820
890
  if (!Number.isInteger(pid) || pid <= 0) {
821
891
  return null;
@@ -846,6 +916,20 @@ function sleep(ms) {
846
916
  timer.unref();
847
917
  });
848
918
  }
919
+ function spawnDaemon(options) {
920
+ const file = binFile(DAEMON_BIN);
921
+ return file ? spawn(process.execPath, [file], options) : spawn(DAEMON_BIN, [], options);
922
+ }
923
+ async function waitForReady() {
924
+ const deadline = Date.now() + READY_TIMEOUT_MS;
925
+ while (Date.now() < deadline) {
926
+ if (await daemonIsLive(READY_PROBE_TIMEOUT_MS)) {
927
+ return true;
928
+ }
929
+ await sleep(READY_POLL_MS);
930
+ }
931
+ return daemonIsLive(READY_PROBE_TIMEOUT_MS);
932
+ }
849
933
  async function daemonStart(opts) {
850
934
  const existing = readPidfile();
851
935
  if (existing?.alive) {
@@ -858,29 +942,32 @@ async function daemonStart(opts) {
858
942
  clearStaleArtifacts();
859
943
  }
860
944
  if (opts.foreground) {
861
- const child2 = spawn(DAEMON_BIN, [], { stdio: "inherit" });
945
+ const child2 = spawnDaemon({ stdio: "inherit" });
862
946
  child2.on("exit", (code) => {
863
947
  process.exit(code ?? 0);
864
948
  });
865
949
  return;
866
950
  }
867
- const child = spawn(DAEMON_BIN, [], {
868
- detached: true,
869
- stdio: ["ignore", "ignore", "ignore"]
870
- });
951
+ const child = spawnDaemon({ detached: true, stdio: ["ignore", "ignore", "ignore"] });
871
952
  child.unref();
872
- await sleep(POST_START_WAIT_MS);
873
- const after = readPidfile();
874
- if (after?.alive) {
875
- process.stderr.write(`[prim] daemon started (pid=${after.pid}, socket=${SOCK_PATH})
876
- `);
877
- console.log(JSON.stringify({ started: true, pid: after.pid }, null, 2));
953
+ const live = await waitForReady();
954
+ if (live) {
955
+ const after = readPidfile();
956
+ process.stderr.write(
957
+ `[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})
958
+ `
959
+ );
960
+ console.log(JSON.stringify({ started: true, pid: after?.pid }, null, 2));
878
961
  return;
879
962
  }
880
963
  process.stderr.write(
881
- "[prim] daemon start: bin spawned but no pidfile observed (check that `prim-daemon-server` is on PATH)\n"
964
+ `[prim] \u2717 daemon start: spawned but the socket did not respond within ${READY_TIMEOUT_MS}ms (check that \`${DAEMON_BIN}\` resolves, and see its log)
965
+ `
882
966
  );
883
967
  console.log(JSON.stringify({ started: false }, null, 2));
968
+ if (!process.exitCode) {
969
+ process.exitCode = EXIT_NOT_RUNNING;
970
+ }
884
971
  }
885
972
  async function daemonStop() {
886
973
  const existing = readPidfile();
@@ -922,41 +1009,48 @@ async function daemonStop() {
922
1009
  );
923
1010
  console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
924
1011
  }
925
- async function daemonStatus() {
926
- const pid = readPidfile();
927
- if (!pid?.alive) {
928
- process.stderr.write("[prim] \u2717 daemon down\n");
929
- console.log(JSON.stringify({ running: false }, null, 2));
930
- if (!process.exitCode) {
931
- process.exitCode = EXIT_NOT_RUNNING;
932
- }
933
- return;
1012
+ function classifyStatus(pidAlive, responding, snapshot, pid) {
1013
+ if (!pidAlive) {
1014
+ return { json: { running: false }, exitCode: EXIT_NOT_RUNNING };
934
1015
  }
935
- const live = await daemonIsLive(STATUS_PROBE_TIMEOUT_MS);
936
- if (!live) {
937
- process.stderr.write(`[prim] \u2717 daemon pid=${pid.pid} alive but socket not responding
938
- `);
939
- console.log(JSON.stringify({ running: true, responding: false, pid: pid.pid }, null, 2));
940
- if (!process.exitCode) {
941
- process.exitCode = EXIT_NOT_RUNNING;
942
- }
943
- return;
1016
+ if (!responding) {
1017
+ return {
1018
+ json: { running: true, responding: false, state: "starting", pid },
1019
+ exitCode: EXIT_BOOTING
1020
+ };
944
1021
  }
945
- const snapshot = await daemonRequest(
1022
+ if (!snapshot) {
1023
+ return { json: { running: true, responding: true }, exitCode: EXIT_OK };
1024
+ }
1025
+ return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK };
1026
+ }
1027
+ async function daemonStatus() {
1028
+ const pid = readPidfile();
1029
+ const pidAlive = pid?.alive ?? false;
1030
+ const responding = pidAlive ? await daemonIsLive(STATUS_PROBE_TIMEOUT_MS) : false;
1031
+ const snapshot = responding ? await daemonRequest(
946
1032
  "status_snapshot",
947
1033
  {},
948
1034
  { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
949
- );
950
- if (!snapshot) {
1035
+ ) : null;
1036
+ const { json, exitCode } = classifyStatus(pidAlive, responding, snapshot, pid?.pid);
1037
+ if (!pidAlive) {
1038
+ process.stderr.write("[prim] \u2717 daemon down\n");
1039
+ } else if (!responding) {
1040
+ process.stderr.write(`[prim] \u25CC daemon pid=${pid?.pid} starting (socket not responding yet)
1041
+ `);
1042
+ } else if (!snapshot) {
951
1043
  process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
952
- console.log(JSON.stringify({ running: true, responding: true }, null, 2));
953
- return;
954
- }
955
- process.stderr.write(
956
- `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
1044
+ } else {
1045
+ process.stderr.write(
1046
+ `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
957
1047
  `
958
- );
959
- console.log(JSON.stringify({ running: true, responding: true, ...snapshot }, null, 2));
1048
+ );
1049
+ }
1050
+ console.log(JSON.stringify(json, null, 2));
1051
+ if (exitCode !== EXIT_OK && !process.exitCode) {
1052
+ process.exitCode = exitCode;
1053
+ }
960
1054
  }
961
1055
  async function daemonRestart(opts) {
962
1056
  await daemonStop();
@@ -1574,7 +1668,7 @@ function registerDecisionsCommands(program2) {
1574
1668
 
1575
1669
  // src/commands/hooks.ts
1576
1670
  import { execSync } from "child_process";
1577
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1671
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1578
1672
  import { resolve } from "path";
1579
1673
  import { Option } from "commander";
1580
1674
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
@@ -1617,13 +1711,13 @@ function getGitRoot() {
1617
1711
  }
1618
1712
  function detectHusky(gitRoot) {
1619
1713
  const huskyDir = resolve(gitRoot, ".husky");
1620
- if (!existsSync4(huskyDir)) return false;
1621
- if (existsSync4(resolve(huskyDir, "_"))) return true;
1622
- if (existsSync4(resolve(huskyDir, "pre-commit"))) return true;
1714
+ if (!existsSync5(huskyDir)) return false;
1715
+ if (existsSync5(resolve(huskyDir, "_"))) return true;
1716
+ if (existsSync5(resolve(huskyDir, "pre-commit"))) return true;
1623
1717
  const pkgPath = resolve(gitRoot, "package.json");
1624
- if (existsSync4(pkgPath)) {
1718
+ if (existsSync5(pkgPath)) {
1625
1719
  try {
1626
- const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
1720
+ const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
1627
1721
  const scripts = pkg2.scripts ?? {};
1628
1722
  if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
1629
1723
  return true;
@@ -1650,8 +1744,8 @@ async function askConfirmation(question) {
1650
1744
  }
1651
1745
  function installToHusky(gitRoot, spec = PRE_COMMIT) {
1652
1746
  const hookPath = resolve(gitRoot, ".husky", spec.hookName);
1653
- if (existsSync4(hookPath)) {
1654
- const existing = readFileSync5(hookPath, "utf-8");
1747
+ if (existsSync5(hookPath)) {
1748
+ const existing = readFileSync6(hookPath, "utf-8");
1655
1749
  if (containsPrimHook(existing, spec.binName)) {
1656
1750
  console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
1657
1751
  return;
@@ -1675,11 +1769,11 @@ ${huskyBlock(spec)}
1675
1769
  function installToDotGit(gitRoot, spec = PRE_COMMIT) {
1676
1770
  const hooksDir = resolve(gitRoot, ".git", "hooks");
1677
1771
  const hookPath = resolve(hooksDir, spec.hookName);
1678
- if (!existsSync4(hooksDir)) {
1772
+ if (!existsSync5(hooksDir)) {
1679
1773
  mkdirSync3(hooksDir, { recursive: true });
1680
1774
  }
1681
- if (existsSync4(hookPath)) {
1682
- const existing = readFileSync5(hookPath, "utf-8");
1775
+ if (existsSync5(hookPath)) {
1776
+ const existing = readFileSync6(hookPath, "utf-8");
1683
1777
  if (containsPrimHook(existing, spec.binName)) {
1684
1778
  console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
1685
1779
  return;
@@ -1742,11 +1836,11 @@ function registerHooksCommands(program2) {
1742
1836
  const gitRoot = getGitRoot();
1743
1837
  for (const spec of HOOKS) {
1744
1838
  const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
1745
- if (!existsSync4(hookPath)) {
1839
+ if (!existsSync5(hookPath)) {
1746
1840
  console.log(`No ${spec.hookName} hook found.`);
1747
1841
  continue;
1748
1842
  }
1749
- if (containsPrimHook(readFileSync5(hookPath, "utf-8"), spec.binName)) {
1843
+ if (containsPrimHook(readFileSync6(hookPath, "utf-8"), spec.binName)) {
1750
1844
  unlinkSync2(hookPath);
1751
1845
  console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
1752
1846
  } else {
@@ -1757,8 +1851,8 @@ function registerHooksCommands(program2) {
1757
1851
  }
1758
1852
 
1759
1853
  // src/commands/moves.ts
1760
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
1761
- import { join as join4 } from "path";
1854
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
1855
+ import { join as join5 } from "path";
1762
1856
 
1763
1857
  // src/flusher.ts
1764
1858
  import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
@@ -1867,19 +1961,19 @@ function registerMovesCommands(program2) {
1867
1961
  }
1868
1962
  });
1869
1963
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
1870
- const dir = join4(process.cwd(), ".prim");
1871
- if (!existsSync5(dir)) {
1964
+ const dir = join5(process.cwd(), ".prim");
1965
+ if (!existsSync6(dir)) {
1872
1966
  mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
1873
1967
  }
1874
- const file = join4(process.cwd(), WORKSPACE_FILE);
1968
+ const file = join5(process.cwd(), WORKSPACE_FILE);
1875
1969
  writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
1876
1970
  mode: FILE_MODE2
1877
1971
  });
1878
1972
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
1879
1973
  });
1880
1974
  moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
1881
- const file = join4(process.cwd(), WORKSPACE_FILE);
1882
- if (!existsSync5(file)) {
1975
+ const file = join5(process.cwd(), WORKSPACE_FILE);
1976
+ if (!existsSync6(file)) {
1883
1977
  console.log("[prim] no workspace binding in cwd");
1884
1978
  return;
1885
1979
  }
@@ -1911,7 +2005,7 @@ function registerProjectCommands(program2) {
1911
2005
  }
1912
2006
 
1913
2007
  // src/commands/reconcile.ts
1914
- var EXIT_OK = 0;
2008
+ var EXIT_OK2 = 0;
1915
2009
  var EXIT_USAGE = 2;
1916
2010
  var EXIT_SERVER = 3;
1917
2011
  var HTTP_CLIENT_ERROR_MIN = 400;
@@ -1974,7 +2068,7 @@ async function performReconcile(idOrShortId, opts = {}) {
1974
2068
  `
1975
2069
  );
1976
2070
  console.log(JSON.stringify(response, null, 2));
1977
- process.exitCode = EXIT_OK;
2071
+ process.exitCode = EXIT_OK2;
1978
2072
  return;
1979
2073
  }
1980
2074
  process.stderr.write("[prim] reconcile: malformed server response\n");
@@ -1994,23 +2088,23 @@ function registerReconcileCommands(program2) {
1994
2088
 
1995
2089
  // src/commands/session.ts
1996
2090
  import {
1997
- existsSync as existsSync6,
2091
+ existsSync as existsSync7,
1998
2092
  mkdirSync as mkdirSync5,
1999
- readFileSync as readFileSync6,
2093
+ readFileSync as readFileSync7,
2000
2094
  readdirSync,
2001
2095
  unlinkSync as unlinkSync5,
2002
2096
  writeFileSync as writeFileSync5
2003
2097
  } from "fs";
2004
- import { join as join5 } from "path";
2098
+ import { join as join6 } from "path";
2005
2099
  var DIR_MODE2 = 448;
2006
2100
  var FILE_MODE3 = 384;
2007
2101
  function ensureDir() {
2008
- if (!existsSync6(SESSIONS_DIR)) {
2102
+ if (!existsSync7(SESSIONS_DIR)) {
2009
2103
  mkdirSync5(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
2010
2104
  }
2011
2105
  }
2012
2106
  function markerPath(sessionId) {
2013
- return join5(SESSIONS_DIR, `${sessionId}.json`);
2107
+ return join6(SESSIONS_DIR, `${sessionId}.json`);
2014
2108
  }
2015
2109
  function registerSessionCommands(program2) {
2016
2110
  const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
@@ -2026,7 +2120,7 @@ function registerSessionCommands(program2) {
2026
2120
  console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
2027
2121
  });
2028
2122
  session.command("list").description("List active session markers").action(() => {
2029
- if (!existsSync6(SESSIONS_DIR)) {
2123
+ if (!existsSync7(SESSIONS_DIR)) {
2030
2124
  console.log("[prim] no session markers");
2031
2125
  return;
2032
2126
  }
@@ -2038,7 +2132,7 @@ function registerSessionCommands(program2) {
2038
2132
  for (const f of files) {
2039
2133
  const sessionId = f.replace(/\.json$/, "");
2040
2134
  try {
2041
- const m = JSON.parse(readFileSync6(join5(SESSIONS_DIR, f), "utf-8"));
2135
+ const m = JSON.parse(readFileSync7(join6(SESSIONS_DIR, f), "utf-8"));
2042
2136
  console.log(`${sessionId} org=${m.orgId}`);
2043
2137
  } catch {
2044
2138
  }
@@ -2046,7 +2140,7 @@ function registerSessionCommands(program2) {
2046
2140
  });
2047
2141
  session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
2048
2142
  const p = markerPath(sessionId);
2049
- if (!existsSync6(p)) {
2143
+ if (!existsSync7(p)) {
2050
2144
  console.log(`[prim] no marker for session ${sessionId}`);
2051
2145
  return;
2052
2146
  }
@@ -2058,17 +2152,17 @@ function registerSessionCommands(program2) {
2058
2152
  // src/commands/skill.ts
2059
2153
  import {
2060
2154
  closeSync as closeSync2,
2061
- existsSync as existsSync7,
2155
+ existsSync as existsSync8,
2062
2156
  fsyncSync as fsyncSync2,
2063
2157
  openSync as openSync2,
2064
- readFileSync as readFileSync7,
2158
+ readFileSync as readFileSync8,
2065
2159
  renameSync as renameSync3,
2066
2160
  writeFileSync as writeFileSync6
2067
2161
  } from "fs";
2068
- import { dirname as dirname3, resolve as resolve2 } from "path";
2069
- import { fileURLToPath } from "url";
2162
+ import { dirname as dirname4, resolve as resolve2 } from "path";
2163
+ import { fileURLToPath as fileURLToPath2 } from "url";
2070
2164
  import { createPatch } from "diff";
2071
- var __dirname = dirname3(fileURLToPath(import.meta.url));
2165
+ var __dirname = dirname4(fileURLToPath2(import.meta.url));
2072
2166
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
2073
2167
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
2074
2168
  var TARGET_CANDIDATES = [
@@ -2081,15 +2175,15 @@ var TARGET_CANDIDATES = [
2081
2175
  var DEFAULT_TARGET = "CLAUDE.md";
2082
2176
  function loadSkill() {
2083
2177
  let dir = __dirname;
2084
- while (dir !== dirname3(dir)) {
2178
+ while (dir !== dirname4(dir)) {
2085
2179
  const p = resolve2(dir, "SKILL.md");
2086
- if (existsSync7(p)) return readFileSync7(p, "utf-8");
2087
- dir = dirname3(dir);
2180
+ if (existsSync8(p)) return readFileSync8(p, "utf-8");
2181
+ dir = dirname4(dir);
2088
2182
  }
2089
2183
  throw new Error("SKILL.md not found in package");
2090
2184
  }
2091
2185
  function detectTargets(cwd) {
2092
- return TARGET_CANDIDATES.filter((p) => existsSync7(resolve2(cwd, p)));
2186
+ return TARGET_CANDIDATES.filter((p) => existsSync8(resolve2(cwd, p)));
2093
2187
  }
2094
2188
  function detectNewline(content) {
2095
2189
  return content.includes("\r\n") ? "\r\n" : "\n";
@@ -2138,7 +2232,7 @@ function resolveTarget(cwd, override) {
2138
2232
  function runInstall(cwd, opts) {
2139
2233
  const target = resolveTarget(cwd, opts.target);
2140
2234
  if (target === null) return 1;
2141
- const existing = existsSync7(target) ? readFileSync7(target, "utf-8") : "";
2235
+ const existing = existsSync8(target) ? readFileSync8(target, "utf-8") : "";
2142
2236
  const eol = existing ? detectNewline(existing) : "\n";
2143
2237
  const block = composeBlock(loadSkill(), eol);
2144
2238
  const next = applyBlock(existing, block, eol);
@@ -2157,11 +2251,11 @@ function runInstall(cwd, opts) {
2157
2251
  function runUninstall(cwd, opts) {
2158
2252
  const target = resolveTarget(cwd, opts.target);
2159
2253
  if (target === null) return 1;
2160
- if (!existsSync7(target)) {
2254
+ if (!existsSync8(target)) {
2161
2255
  console.log(`Skill block not present at ${target}`);
2162
2256
  return 0;
2163
2257
  }
2164
- const existing = readFileSync7(target, "utf-8");
2258
+ const existing = readFileSync8(target, "utf-8");
2165
2259
  const next = removeBlock(existing);
2166
2260
  if (next === null) {
2167
2261
  console.log(`Skill block not present at ${target}`);
@@ -2174,10 +2268,10 @@ function runUninstall(cwd, opts) {
2174
2268
  function runStatus(cwd, opts) {
2175
2269
  const target = resolveTarget(cwd, opts.target);
2176
2270
  if (target === null) return 1;
2177
- const fileExists = existsSync7(target);
2271
+ const fileExists = existsSync8(target);
2178
2272
  let installed = false;
2179
2273
  if (fileExists) {
2180
- const content = readFileSync7(target, "utf-8");
2274
+ const content = readFileSync8(target, "utf-8");
2181
2275
  installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
2182
2276
  }
2183
2277
  if (opts.json) {
@@ -2214,7 +2308,7 @@ function registerSkillCommands(program2) {
2214
2308
  }
2215
2309
 
2216
2310
  // src/commands/spec.ts
2217
- import { readFileSync as readFileSync8 } from "fs";
2311
+ import { readFileSync as readFileSync9 } from "fs";
2218
2312
  function registerSpecCommands(program2) {
2219
2313
  const spec = program2.command("spec").description("Manage spec documents");
2220
2314
  spec.command("list").description("List spec documents").option("-t, --project-id <projectId>", "List spec for a specific root project").option("--json", "Output as JSON").action(async (opts) => {
@@ -2268,7 +2362,7 @@ ${contexts.length} spec(s)`);
2268
2362
  const client = getClient();
2269
2363
  let text = opts.text;
2270
2364
  if (opts.file) {
2271
- text = readFileSync8(opts.file, "utf-8");
2365
+ text = readFileSync9(opts.file, "utf-8");
2272
2366
  }
2273
2367
  const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
2274
2368
  let linkedBranch;
@@ -2309,7 +2403,7 @@ ${contexts.length} spec(s)`);
2309
2403
  const client = getClient();
2310
2404
  let text = opts.text;
2311
2405
  if (opts.file) {
2312
- text = readFileSync8(opts.file, "utf-8");
2406
+ text = readFileSync9(opts.file, "utf-8");
2313
2407
  }
2314
2408
  if (!(text || opts.name)) {
2315
2409
  console.error("Provide --text, --file, or --name to update.");
@@ -2480,17 +2574,17 @@ ${preview}`);
2480
2574
  }
2481
2575
 
2482
2576
  // src/commands/statusline.ts
2483
- import { readFileSync as readFileSync9 } from "fs";
2484
- import { dirname as dirname4, resolve as resolve3 } from "path";
2485
- import { fileURLToPath as fileURLToPath2 } from "url";
2577
+ import { readFileSync as readFileSync10 } from "fs";
2578
+ import { dirname as dirname5, resolve as resolve3 } from "path";
2579
+ import { fileURLToPath as fileURLToPath3 } from "url";
2486
2580
  var STATUSLINE_TIMEOUT_MS = 200;
2487
2581
  function readPackageVersion() {
2488
2582
  try {
2489
- const here = dirname4(fileURLToPath2(import.meta.url));
2583
+ const here = dirname5(fileURLToPath3(import.meta.url));
2490
2584
  const candidates = [resolve3(here, "../../package.json"), resolve3(here, "../package.json")];
2491
2585
  for (const path of candidates) {
2492
2586
  try {
2493
- const pkg2 = JSON.parse(readFileSync9(path, "utf-8"));
2587
+ const pkg2 = JSON.parse(readFileSync10(path, "utf-8"));
2494
2588
  if (pkg2.version) {
2495
2589
  return pkg2.version;
2496
2590
  }
@@ -2535,8 +2629,8 @@ function registerStatuslineCommands(program2) {
2535
2629
  }
2536
2630
 
2537
2631
  // src/index.ts
2538
- var __dirname2 = dirname5(fileURLToPath3(import.meta.url));
2539
- var pkg = JSON.parse(readFileSync10(resolve4(__dirname2, "../package.json"), "utf-8"));
2632
+ var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
2633
+ var pkg = JSON.parse(readFileSync11(resolve4(__dirname2, "../package.json"), "utf-8"));
2540
2634
  updateNotifier({ pkg }).notify();
2541
2635
  var program = new Command();
2542
2636
  program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.18",
3
+ "version": "0.1.0-alpha.19",
4
4
  "description": "CLI for managing Primitive specs, contexts, and git hooks",
5
5
  "type": "module",
6
6
  "license": "MIT",