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

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/index.js CHANGED
@@ -7,9 +7,8 @@ import {
7
7
  import {
8
8
  checkAffectedDecisions,
9
9
  daemonOrDirectGet,
10
- formatDecisionsWarning,
11
- getGitContext
12
- } from "./chunk-TPQ3X244.js";
10
+ formatDecisionsWarning
11
+ } from "./chunk-E5UZXMZL.js";
13
12
  import {
14
13
  HttpError,
15
14
  REFRESH_TOKEN_PATH,
@@ -20,7 +19,7 @@ import {
20
19
  getSiteUrl,
21
20
  getTokenExpiresAt,
22
21
  saveTokenExpiry
23
- } from "./chunk-6SIEWWUL.js";
22
+ } from "./chunk-26VA3ADF.js";
24
23
  import {
25
24
  JOURNAL_DIR,
26
25
  SESSIONS_DIR,
@@ -34,9 +33,9 @@ import {
34
33
  } from "./chunk-UTKQTZHL.js";
35
34
 
36
35
  // 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";
36
+ import { readFileSync as readFileSync9 } from "fs";
37
+ import { dirname as dirname6, resolve as resolve4 } from "path";
38
+ import { fileURLToPath as fileURLToPath4 } from "url";
40
39
  import { Command } from "commander";
41
40
  import updateNotifier from "update-notifier";
42
41
 
@@ -125,7 +124,8 @@ function registerAuthCommands(program2) {
125
124
  res.end("<h1>Authentication successful!</h1><p>You can close this tab.</p>");
126
125
  exchangeCode(siteUrl, code, verifier, `http://${LOCALHOST}:${port}/callback`).then((token) => {
127
126
  saveToken(token);
128
- console.log(`Authenticated! Token saved to ${TOKEN_FILE_PATH}`);
127
+ console.error(`Authenticated! Token saved to ${TOKEN_FILE_PATH}`);
128
+ console.log(JSON.stringify({ authenticated: true, tokenFile: TOKEN_FILE_PATH }));
129
129
  server.close();
130
130
  process.exit(0);
131
131
  }).catch((err) => {
@@ -151,12 +151,12 @@ function registerAuthCommands(program2) {
151
151
  authUrl.searchParams.set("state", state);
152
152
  authUrl.searchParams.set("code_challenge", challenge);
153
153
  authUrl.searchParams.set("code_challenge_method", "S256");
154
- console.log("Opening browser for authentication...");
154
+ console.error("Opening browser for authentication...");
155
155
  openBrowser(authUrl.toString());
156
- console.log(`If the browser doesn't open, visit:
156
+ console.error(`If the browser doesn't open, visit:
157
157
  ${authUrl.toString()}
158
158
  `);
159
- console.log("Waiting for callback...");
159
+ console.error("Waiting for callback...");
160
160
  setTimeout(() => {
161
161
  console.error("Authentication timed out.");
162
162
  server.close();
@@ -281,33 +281,94 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
281
281
  // src/commands/claude-install.ts
282
282
  import {
283
283
  closeSync,
284
- existsSync as existsSync2,
284
+ existsSync as existsSync3,
285
285
  fsyncSync,
286
286
  mkdirSync as mkdirSync2,
287
287
  openSync,
288
- readFileSync as readFileSync2,
288
+ readFileSync as readFileSync3,
289
289
  renameSync,
290
290
  writeFileSync as writeFileSync2
291
291
  } from "fs";
292
292
  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";
293
+ import { dirname as dirname3, join as join2 } from "path";
294
+
295
+ // src/lib/bin-path.ts
296
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
297
+ import { dirname as dirname2, isAbsolute, join } from "path";
298
+ import { fileURLToPath } from "url";
299
+ var PKG_NAME = "@primitive.ai/prim";
300
+ var ROOT_WALK_LIMIT = 6;
301
+ var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
302
+ var resolvedRoot;
303
+ function locateRoot() {
304
+ if (resolvedRoot !== void 0) {
305
+ return resolvedRoot;
306
+ }
307
+ let dir = dirname2(fileURLToPath(import.meta.url));
308
+ for (let depth = 0; depth < ROOT_WALK_LIMIT; depth++) {
309
+ const manifestPath = join(dir, "package.json");
310
+ if (existsSync2(manifestPath)) {
311
+ try {
312
+ const manifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
313
+ if (manifest.name === PKG_NAME && manifest.bin) {
314
+ resolvedRoot = { dir, bin: manifest.bin };
315
+ return resolvedRoot;
316
+ }
317
+ } catch {
318
+ }
319
+ }
320
+ const parent = dirname2(dir);
321
+ if (parent === dir) {
322
+ break;
323
+ }
324
+ dir = parent;
325
+ }
326
+ resolvedRoot = null;
327
+ return resolvedRoot;
328
+ }
329
+ function binFile(bin) {
330
+ const root = locateRoot();
331
+ const rel = root?.bin[bin];
332
+ if (!root || !rel) {
333
+ return null;
334
+ }
335
+ return isAbsolute(rel) ? rel : join(root.dir, rel);
336
+ }
337
+ function hookShimCommand(bin, args = "") {
338
+ const invoke = (cmd) => args ? `${cmd} ${args}` : cmd;
339
+ 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`;
340
+ }
341
+ function commandMatchesBin(command, bin) {
342
+ if (!command) {
343
+ return false;
344
+ }
345
+ const c = command.trim();
346
+ if (c === bin || c.startsWith(`${bin} `)) {
347
+ return true;
348
+ }
349
+ return c.includes(`command -v ${bin} `);
350
+ }
351
+
352
+ // src/commands/claude-install.ts
353
+ var CAPTURE_BIN = "prim-hook";
354
+ var GATE_BIN = "prim-pre-tool-use";
355
+ var POST_TOOL_USE_BIN = "prim-post-tool-use";
356
+ var SESSION_START_BIN = "prim-session-start";
357
+ var SESSION_END_BIN = "prim-session-end";
358
+ var STATUSLINE_BIN = "prim";
359
+ var STATUSLINE_ARGS = "statusline";
360
+ var STATUSLINE_COMMAND = hookShimCommand(STATUSLINE_BIN, STATUSLINE_ARGS);
300
361
  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
- ]);
362
+ var PRIM_BINS = [
363
+ CAPTURE_BIN,
364
+ GATE_BIN,
365
+ POST_TOOL_USE_BIN,
366
+ SESSION_START_BIN,
367
+ SESSION_END_BIN
368
+ ];
308
369
  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");
370
+ var USER_SCOPE_PATH = join2(homedir(), ".claude", "settings.json");
371
+ var PROJECT_SCOPE_PATH = join2(process.cwd(), ".claude", "settings.json");
311
372
  var CAPTURE_EVENTS = [
312
373
  "SessionStart",
313
374
  "UserPromptSubmit",
@@ -317,21 +378,24 @@ var CAPTURE_EVENTS = [
317
378
  "SessionEnd",
318
379
  "SubagentStop"
319
380
  ];
381
+ function makeRegistration(event, matcher, bin, args = "") {
382
+ return { event, matcher, bin, command: hookShimCommand(bin, args) };
383
+ }
320
384
  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 }
385
+ ...CAPTURE_EVENTS.map((event) => makeRegistration(event, "*", CAPTURE_BIN)),
386
+ makeRegistration("PreToolUse", "Edit|Write|MultiEdit", GATE_BIN),
387
+ makeRegistration("PostToolUse", "Edit|Write|MultiEdit", POST_TOOL_USE_BIN),
388
+ makeRegistration("SessionStart", "*", SESSION_START_BIN),
389
+ makeRegistration("SessionEnd", "*", SESSION_END_BIN)
326
390
  ];
327
391
  function settingsPathFor(scope) {
328
392
  return scope === "user" ? USER_SCOPE_PATH : PROJECT_SCOPE_PATH;
329
393
  }
330
394
  function readSettings(path) {
331
- if (!existsSync2(path)) {
395
+ if (!existsSync3(path)) {
332
396
  return {};
333
397
  }
334
- const raw = readFileSync2(path, "utf-8");
398
+ const raw = readFileSync3(path, "utf-8");
335
399
  try {
336
400
  return JSON.parse(raw);
337
401
  } catch (err) {
@@ -339,16 +403,16 @@ function readSettings(path) {
339
403
  throw new Error(`${path} is not valid JSON: ${detail}`);
340
404
  }
341
405
  }
342
- function entryHasCommand(entry, command) {
343
- return entry.hooks?.some((h) => h.command === command) ?? false;
406
+ function entryHasCommand(entry, bin) {
407
+ return entry.hooks?.some((h) => commandMatchesBin(h.command, bin)) ?? false;
344
408
  }
345
409
  function canonicalEntry(reg) {
346
410
  return { matcher: reg.matcher, hooks: [{ type: "command", command: reg.command }] };
347
411
  }
348
- function stripCommand(list, command) {
412
+ function stripCommand(list, bin) {
349
413
  const out = [];
350
414
  for (const e of list) {
351
- const hooks = (e.hooks ?? []).filter((h) => h.command !== command);
415
+ const hooks = (e.hooks ?? []).filter((h) => !commandMatchesBin(h.command, bin));
352
416
  if (hooks.length > 0) {
353
417
  out.push({ ...e, hooks });
354
418
  }
@@ -362,11 +426,16 @@ function ensureRegistration(list, reg, force) {
362
426
  if (hasCanonical && !force) {
363
427
  return list;
364
428
  }
365
- return [...stripCommand(list, reg.command), canonicalEntry(reg)];
429
+ return [...stripCommand(list, reg.bin), canonicalEntry(reg)];
366
430
  }
431
+ var LEGACY_STATUSLINE_COMMAND = "prim statusline";
367
432
  function isPrimStatusLine(settings) {
368
433
  const s = settings.statusLine;
369
- return s?.type === "command" && s?.command === STATUSLINE_COMMAND;
434
+ if (s?.type !== "command") {
435
+ return false;
436
+ }
437
+ const c = (s.command ?? "").trim();
438
+ return c === LEGACY_STATUSLINE_COMMAND || c.includes("@primitive.ai/prim") && c.includes("statusline");
370
439
  }
371
440
  function applyStatusLine(settings) {
372
441
  if (settings.statusLine && !isPrimStatusLine(settings)) {
@@ -393,8 +462,8 @@ function applyUninstall(settings) {
393
462
  const hooks = {};
394
463
  for (const event of Object.keys(source)) {
395
464
  let list = source[event] ?? [];
396
- for (const command of PRIM_COMMANDS) {
397
- list = stripCommand(list, command);
465
+ for (const bin of PRIM_BINS) {
466
+ list = stripCommand(list, bin);
398
467
  }
399
468
  if (list.length > 0) {
400
469
  hooks[event] = list;
@@ -408,18 +477,18 @@ function applyUninstall(settings) {
408
477
  }
409
478
  function captureInstalled(settings) {
410
479
  return CAPTURE_EVENTS.some(
411
- (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND))
480
+ (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN))
412
481
  );
413
482
  }
414
483
  function statuslineInstalled(settings) {
415
484
  return isPrimStatusLine(settings);
416
485
  }
417
486
  function isGateInstalled(settings) {
418
- return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND));
487
+ return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN));
419
488
  }
420
489
  function atomicWrite(path, content) {
421
- const dir = dirname2(path);
422
- if (!existsSync2(dir)) {
490
+ const dir = dirname3(path);
491
+ if (!existsSync3(dir)) {
423
492
  mkdirSync2(dir, { recursive: true });
424
493
  }
425
494
  const tmp = `${path}.tmp.${String(Date.now())}`;
@@ -535,11 +604,12 @@ ${line("project", result.project)}`);
535
604
 
536
605
  // src/commands/codex-install.ts
537
606
  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";
607
+ import { join as join3 } from "path";
608
+ var CAPTURE_BIN2 = "prim-hook";
609
+ var GATE_BIN2 = "prim-pre-tool-use";
610
+ var POST_TOOL_USE_BIN2 = "prim-post-tool-use";
611
+ var SESSION_START_BIN2 = "prim-session-start";
612
+ var CODEX_ARGS = "--agent codex";
543
613
  var JSON_INDENT2 = 2;
544
614
  var CODEX_CAPTURE_EVENTS = [
545
615
  "SessionStart",
@@ -549,20 +619,15 @@ var CODEX_CAPTURE_EVENTS = [
549
619
  "Stop",
550
620
  "SubagentStop"
551
621
  ];
552
- var PRIM_COMMANDS2 = /* @__PURE__ */ new Set([
553
- CAPTURE_COMMAND2,
554
- GATE_COMMAND2,
555
- POST_TOOL_USE_COMMAND2,
556
- SESSION_START_COMMAND2
557
- ]);
622
+ var PRIM_BINS2 = [CAPTURE_BIN2, GATE_BIN2, POST_TOOL_USE_BIN2, SESSION_START_BIN2];
558
623
  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 }
624
+ ...CODEX_CAPTURE_EVENTS.map((event) => makeRegistration(event, "*", CAPTURE_BIN2, CODEX_ARGS)),
625
+ makeRegistration("PreToolUse", "apply_patch", GATE_BIN2, CODEX_ARGS),
626
+ makeRegistration("PostToolUse", "apply_patch", POST_TOOL_USE_BIN2, CODEX_ARGS),
627
+ makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS)
563
628
  ];
564
- var USER_SCOPE_PATH2 = join2(homedir2(), ".codex", "hooks.json");
565
- var PROJECT_SCOPE_PATH2 = join2(process.cwd(), ".codex", "hooks.json");
629
+ var USER_SCOPE_PATH2 = join3(homedir2(), ".codex", "hooks.json");
630
+ var PROJECT_SCOPE_PATH2 = join3(process.cwd(), ".codex", "hooks.json");
566
631
  function settingsPathFor2(scope) {
567
632
  return scope === "user" ? USER_SCOPE_PATH2 : PROJECT_SCOPE_PATH2;
568
633
  }
@@ -578,8 +643,8 @@ function applyUninstall2(settings) {
578
643
  const hooks = {};
579
644
  for (const event of Object.keys(source)) {
580
645
  let list = source[event] ?? [];
581
- for (const command of PRIM_COMMANDS2) {
582
- list = stripCommand(list, command);
646
+ for (const bin of PRIM_BINS2) {
647
+ list = stripCommand(list, bin);
583
648
  }
584
649
  if (list.length > 0) {
585
650
  hooks[event] = list;
@@ -589,11 +654,11 @@ function applyUninstall2(settings) {
589
654
  }
590
655
  function captureInstalled2(settings) {
591
656
  return CODEX_CAPTURE_EVENTS.some(
592
- (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND2))
657
+ (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN2))
593
658
  );
594
659
  }
595
660
  function isGateInstalled2(settings) {
596
- return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND2));
661
+ return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN2));
597
662
  }
598
663
  function resultFor(scope, path, after, changed) {
599
664
  return {
@@ -681,138 +746,25 @@ ${line("project", result.project)}`);
681
746
  });
682
747
  }
683
748
 
684
- // src/commands/context.ts
685
- import { readFileSync as readFileSync3 } from "fs";
686
- function registerContextCommands(program2) {
687
- const context = program2.command("context").description("Manage contexts");
688
- 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) => {
689
- const client = getClient();
690
- const params = new URLSearchParams();
691
- if (opts.projectId) {
692
- params.set("taskId", opts.projectId);
693
- }
694
- if (opts.scope) {
695
- params.set("scope", opts.scope === "project" ? "task" : opts.scope);
696
- }
697
- const contexts = await client.get(`/api/cli/contexts?${params.toString()}`);
698
- if (opts.json) {
699
- printJson(contexts);
700
- return;
701
- }
702
- printContextList(contexts);
703
- });
704
- context.command("get <contextId>").description("Get a context by ID").option("--json", "Output as JSON (default behavior; accepted for symmetry)").action(async (contextId) => {
705
- const client = getClient();
706
- const ctx = await client.get(`/api/cli/contexts/${contextId}`);
707
- printJson(ctx);
708
- });
709
- context.command("create").description("Create a new context").requiredOption("-s, --scope <scope>", "Scope: project, global, external").requiredOption("-n, --name <name>", "Context name").option("-t, --text <text>", "Context text content").option("-f, --file <path>", "Read text content from file").option("--project-id <projectId>", "Link to project(s), comma-separated").option("--spec", "Mark as a spec document").option("--json", "Output as JSON").action(
710
- async (opts) => {
711
- const client = getClient();
712
- let text = opts.text;
713
- if (opts.file) {
714
- text = readFileSync3(opts.file, "utf-8");
715
- }
716
- const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
717
- const result = await client.post("/api/cli/contexts", {
718
- scope: opts.scope === "project" ? "task" : opts.scope,
719
- name: opts.name,
720
- text,
721
- taskIds,
722
- isSpecDocument: opts.spec ?? false
723
- });
724
- if (opts.json) {
725
- printJson({ _id: result._id });
726
- return;
727
- }
728
- console.error(`Created context: ${result._id}`);
729
- console.log(result._id);
730
- }
731
- );
732
- context.command("update <contextId>").description("Update a context").option("-n, --name <name>", "New name").option("-t, --text <text>", "New text content").option("-f, --file <path>", "Read text content from file").option("--json", "Output as JSON").action(
733
- async (contextId, opts) => {
734
- const client = getClient();
735
- let text = opts.text;
736
- if (opts.file) {
737
- text = readFileSync3(opts.file, "utf-8");
738
- }
739
- await client.patch(`/api/cli/contexts/${contextId}`, {
740
- name: opts.name,
741
- text
742
- });
743
- if (opts.json) {
744
- printJson({ _id: contextId });
745
- return;
746
- }
747
- console.error(`Updated context: ${contextId}`);
748
- console.log(contextId);
749
- }
750
- );
751
- context.command("delete <contextId>").description("Delete a context").option("--json", "Output as JSON").action(async (contextId, opts) => {
752
- const client = getClient();
753
- await client.delete(`/api/cli/contexts/${contextId}`);
754
- if (opts.json) {
755
- printJson({ _id: contextId });
756
- return;
757
- }
758
- console.error(`Deleted context: ${contextId}`);
759
- console.log(contextId);
760
- });
761
- context.command("link <contextId>").description("Link a context to a project").requiredOption("--project <projectId>", "Project ID to link to").option("--json", "Output as JSON").action(async (contextId, opts) => {
762
- const client = getClient();
763
- await client.post(`/api/cli/contexts/${contextId}/link`, {
764
- taskId: opts.project
765
- });
766
- if (opts.json) {
767
- printJson({ _id: contextId, project: opts.project });
768
- return;
769
- }
770
- console.error(`Linked context ${contextId} to project ${opts.project}`);
771
- console.log(contextId);
772
- });
773
- context.command("unlink <contextId>").description("Unlink a context from a project").requiredOption("--project <projectId>", "Project ID to unlink from").option("--json", "Output as JSON").action(async (contextId, opts) => {
774
- const client = getClient();
775
- await client.post(`/api/cli/contexts/${contextId}/unlink`, {
776
- taskId: opts.project
777
- });
778
- if (opts.json) {
779
- printJson({ _id: contextId, project: opts.project });
780
- return;
781
- }
782
- console.error(`Unlinked context ${contextId} from project ${opts.project}`);
783
- console.log(contextId);
784
- });
785
- }
786
- function printContextList(contexts) {
787
- if (contexts.length === 0) {
788
- console.error("No contexts found.");
789
- return;
790
- }
791
- for (const ctx of contexts) {
792
- const scope = ctx.scope === "task" ? "project" : ctx.scope ?? "project";
793
- const spec = ctx.isSpecDocument ? " [SPEC]" : "";
794
- const name = ctx.name ?? ctx.title ?? "(unnamed)";
795
- console.log(`${ctx._id} ${scope.padEnd(8)} ${name}${spec}`);
796
- }
797
- console.error(`
798
- ${contexts.length} context(s)`);
799
- }
800
-
801
749
  // src/commands/daemon.ts
802
750
  import { spawn } from "child_process";
803
- import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
751
+ import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
804
752
  import { homedir as homedir3 } from "os";
805
- import { join as join3 } from "path";
753
+ import { join as join4 } from "path";
806
754
  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");
755
+ var PID_PATH = join4(homedir3(), ".config", "prim", "daemon.pid");
756
+ var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
809
757
  var STOP_TIMEOUT_MS = 5e3;
810
758
  var STOP_POLL_MS = 100;
811
759
  var STATUS_PROBE_TIMEOUT_MS = 500;
812
- var POST_START_WAIT_MS = 400;
760
+ var READY_TIMEOUT_MS = 5e3;
761
+ var READY_POLL_MS = 100;
762
+ var READY_PROBE_TIMEOUT_MS = 250;
763
+ var EXIT_OK = 0;
813
764
  var EXIT_NOT_RUNNING = 2;
765
+ var EXIT_BOOTING = 3;
814
766
  function readPidfile() {
815
- if (!existsSync3(PID_PATH)) {
767
+ if (!existsSync4(PID_PATH)) {
816
768
  return null;
817
769
  }
818
770
  const raw = readFileSync4(PID_PATH, "utf-8").trim();
@@ -846,6 +798,20 @@ function sleep(ms) {
846
798
  timer.unref();
847
799
  });
848
800
  }
801
+ function spawnDaemon(options) {
802
+ const file = binFile(DAEMON_BIN);
803
+ return file ? spawn(process.execPath, [file], options) : spawn(DAEMON_BIN, [], options);
804
+ }
805
+ async function waitForReady() {
806
+ const deadline = Date.now() + READY_TIMEOUT_MS;
807
+ while (Date.now() < deadline) {
808
+ if (await daemonIsLive(READY_PROBE_TIMEOUT_MS)) {
809
+ return true;
810
+ }
811
+ await sleep(READY_POLL_MS);
812
+ }
813
+ return daemonIsLive(READY_PROBE_TIMEOUT_MS);
814
+ }
849
815
  async function daemonStart(opts) {
850
816
  const existing = readPidfile();
851
817
  if (existing?.alive) {
@@ -858,29 +824,32 @@ async function daemonStart(opts) {
858
824
  clearStaleArtifacts();
859
825
  }
860
826
  if (opts.foreground) {
861
- const child2 = spawn(DAEMON_BIN, [], { stdio: "inherit" });
827
+ const child2 = spawnDaemon({ stdio: "inherit" });
862
828
  child2.on("exit", (code) => {
863
829
  process.exit(code ?? 0);
864
830
  });
865
831
  return;
866
832
  }
867
- const child = spawn(DAEMON_BIN, [], {
868
- detached: true,
869
- stdio: ["ignore", "ignore", "ignore"]
870
- });
833
+ const child = spawnDaemon({ detached: true, stdio: ["ignore", "ignore", "ignore"] });
871
834
  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));
835
+ const live = await waitForReady();
836
+ if (live) {
837
+ const after = readPidfile();
838
+ process.stderr.write(
839
+ `[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})
840
+ `
841
+ );
842
+ console.log(JSON.stringify({ started: true, pid: after?.pid }, null, 2));
878
843
  return;
879
844
  }
880
845
  process.stderr.write(
881
- "[prim] daemon start: bin spawned but no pidfile observed (check that `prim-daemon-server` is on PATH)\n"
846
+ `[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)
847
+ `
882
848
  );
883
849
  console.log(JSON.stringify({ started: false }, null, 2));
850
+ if (!process.exitCode) {
851
+ process.exitCode = EXIT_NOT_RUNNING;
852
+ }
884
853
  }
885
854
  async function daemonStop() {
886
855
  const existing = readPidfile();
@@ -922,41 +891,48 @@ async function daemonStop() {
922
891
  );
923
892
  console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
924
893
  }
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;
894
+ function classifyStatus(pidAlive, responding, snapshot, pid) {
895
+ if (!pidAlive) {
896
+ return { json: { running: false }, exitCode: EXIT_NOT_RUNNING };
934
897
  }
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;
898
+ if (!responding) {
899
+ return {
900
+ json: { running: true, responding: false, state: "starting", pid },
901
+ exitCode: EXIT_BOOTING
902
+ };
944
903
  }
945
- const snapshot = await daemonRequest(
904
+ if (!snapshot) {
905
+ return { json: { running: true, responding: true }, exitCode: EXIT_OK };
906
+ }
907
+ return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK };
908
+ }
909
+ async function daemonStatus() {
910
+ const pid = readPidfile();
911
+ const pidAlive = pid?.alive ?? false;
912
+ const responding = pidAlive ? await daemonIsLive(STATUS_PROBE_TIMEOUT_MS) : false;
913
+ const snapshot = responding ? await daemonRequest(
946
914
  "status_snapshot",
947
915
  {},
948
916
  { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
949
- );
950
- if (!snapshot) {
917
+ ) : null;
918
+ const { json, exitCode } = classifyStatus(pidAlive, responding, snapshot, pid?.pid);
919
+ if (!pidAlive) {
920
+ process.stderr.write("[prim] \u2717 daemon down\n");
921
+ } else if (!responding) {
922
+ process.stderr.write(`[prim] \u25CC daemon pid=${pid?.pid} starting (socket not responding yet)
923
+ `);
924
+ } else if (!snapshot) {
951
925
  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}
926
+ } else {
927
+ process.stderr.write(
928
+ `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
957
929
  `
958
- );
959
- console.log(JSON.stringify({ running: true, responding: true, ...snapshot }, null, 2));
930
+ );
931
+ }
932
+ console.log(JSON.stringify(json, null, 2));
933
+ if (exitCode !== EXIT_OK && !process.exitCode) {
934
+ process.exitCode = exitCode;
935
+ }
960
936
  }
961
937
  async function daemonRestart(opts) {
962
938
  await daemonStop();
@@ -1574,7 +1550,7 @@ function registerDecisionsCommands(program2) {
1574
1550
 
1575
1551
  // src/commands/hooks.ts
1576
1552
  import { execSync } from "child_process";
1577
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1553
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1578
1554
  import { resolve } from "path";
1579
1555
  import { Option } from "commander";
1580
1556
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
@@ -1617,11 +1593,11 @@ function getGitRoot() {
1617
1593
  }
1618
1594
  function detectHusky(gitRoot) {
1619
1595
  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;
1596
+ if (!existsSync5(huskyDir)) return false;
1597
+ if (existsSync5(resolve(huskyDir, "_"))) return true;
1598
+ if (existsSync5(resolve(huskyDir, "pre-commit"))) return true;
1623
1599
  const pkgPath = resolve(gitRoot, "package.json");
1624
- if (existsSync4(pkgPath)) {
1600
+ if (existsSync5(pkgPath)) {
1625
1601
  try {
1626
1602
  const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
1627
1603
  const scripts = pkg2.scripts ?? {};
@@ -1650,7 +1626,7 @@ async function askConfirmation(question) {
1650
1626
  }
1651
1627
  function installToHusky(gitRoot, spec = PRE_COMMIT) {
1652
1628
  const hookPath = resolve(gitRoot, ".husky", spec.hookName);
1653
- if (existsSync4(hookPath)) {
1629
+ if (existsSync5(hookPath)) {
1654
1630
  const existing = readFileSync5(hookPath, "utf-8");
1655
1631
  if (containsPrimHook(existing, spec.binName)) {
1656
1632
  console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
@@ -1675,10 +1651,10 @@ ${huskyBlock(spec)}
1675
1651
  function installToDotGit(gitRoot, spec = PRE_COMMIT) {
1676
1652
  const hooksDir = resolve(gitRoot, ".git", "hooks");
1677
1653
  const hookPath = resolve(hooksDir, spec.hookName);
1678
- if (!existsSync4(hooksDir)) {
1654
+ if (!existsSync5(hooksDir)) {
1679
1655
  mkdirSync3(hooksDir, { recursive: true });
1680
1656
  }
1681
- if (existsSync4(hookPath)) {
1657
+ if (existsSync5(hookPath)) {
1682
1658
  const existing = readFileSync5(hookPath, "utf-8");
1683
1659
  if (containsPrimHook(existing, spec.binName)) {
1684
1660
  console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
@@ -1742,7 +1718,7 @@ function registerHooksCommands(program2) {
1742
1718
  const gitRoot = getGitRoot();
1743
1719
  for (const spec of HOOKS) {
1744
1720
  const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
1745
- if (!existsSync4(hookPath)) {
1721
+ if (!existsSync5(hookPath)) {
1746
1722
  console.log(`No ${spec.hookName} hook found.`);
1747
1723
  continue;
1748
1724
  }
@@ -1757,8 +1733,8 @@ function registerHooksCommands(program2) {
1757
1733
  }
1758
1734
 
1759
1735
  // 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";
1736
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
1737
+ import { join as join5 } from "path";
1762
1738
 
1763
1739
  // src/flusher.ts
1764
1740
  import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
@@ -1867,19 +1843,19 @@ function registerMovesCommands(program2) {
1867
1843
  }
1868
1844
  });
1869
1845
  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)) {
1846
+ const dir = join5(process.cwd(), ".prim");
1847
+ if (!existsSync6(dir)) {
1872
1848
  mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
1873
1849
  }
1874
- const file = join4(process.cwd(), WORKSPACE_FILE);
1850
+ const file = join5(process.cwd(), WORKSPACE_FILE);
1875
1851
  writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
1876
1852
  mode: FILE_MODE2
1877
1853
  });
1878
1854
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
1879
1855
  });
1880
1856
  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)) {
1857
+ const file = join5(process.cwd(), WORKSPACE_FILE);
1858
+ if (!existsSync6(file)) {
1883
1859
  console.log("[prim] no workspace binding in cwd");
1884
1860
  return;
1885
1861
  }
@@ -1888,30 +1864,8 @@ function registerMovesCommands(program2) {
1888
1864
  });
1889
1865
  }
1890
1866
 
1891
- // src/commands/project.ts
1892
- function registerProjectCommands(program2) {
1893
- const project = program2.command("project").description("Manage projects");
1894
- project.command("create").description("Create a new project").requiredOption("-n, --name <name>", "Project name").option("-d, --description <description>", "Project description").option("--spec <contextId>", "Link an existing spec as this project's spec").option("--json", "Output as JSON").action(async (opts) => {
1895
- const client = getClient();
1896
- const result = await client.post("/api/cli/tasks", {
1897
- name: opts.name,
1898
- description: opts.description,
1899
- specContextId: opts.spec
1900
- });
1901
- if (opts.json) {
1902
- printJson(opts.spec ? { _id: result._id, spec: opts.spec } : { _id: result._id });
1903
- return;
1904
- }
1905
- console.error(`Created project: ${result._id}`);
1906
- if (opts.spec) {
1907
- console.error(`Linked spec: ${opts.spec}`);
1908
- }
1909
- console.log(result._id);
1910
- });
1911
- }
1912
-
1913
1867
  // src/commands/reconcile.ts
1914
- var EXIT_OK = 0;
1868
+ var EXIT_OK2 = 0;
1915
1869
  var EXIT_USAGE = 2;
1916
1870
  var EXIT_SERVER = 3;
1917
1871
  var HTTP_CLIENT_ERROR_MIN = 400;
@@ -1974,7 +1928,7 @@ async function performReconcile(idOrShortId, opts = {}) {
1974
1928
  `
1975
1929
  );
1976
1930
  console.log(JSON.stringify(response, null, 2));
1977
- process.exitCode = EXIT_OK;
1931
+ process.exitCode = EXIT_OK2;
1978
1932
  return;
1979
1933
  }
1980
1934
  process.stderr.write("[prim] reconcile: malformed server response\n");
@@ -1994,23 +1948,23 @@ function registerReconcileCommands(program2) {
1994
1948
 
1995
1949
  // src/commands/session.ts
1996
1950
  import {
1997
- existsSync as existsSync6,
1951
+ existsSync as existsSync7,
1998
1952
  mkdirSync as mkdirSync5,
1999
1953
  readFileSync as readFileSync6,
2000
1954
  readdirSync,
2001
1955
  unlinkSync as unlinkSync5,
2002
1956
  writeFileSync as writeFileSync5
2003
1957
  } from "fs";
2004
- import { join as join5 } from "path";
1958
+ import { join as join6 } from "path";
2005
1959
  var DIR_MODE2 = 448;
2006
1960
  var FILE_MODE3 = 384;
2007
1961
  function ensureDir() {
2008
- if (!existsSync6(SESSIONS_DIR)) {
1962
+ if (!existsSync7(SESSIONS_DIR)) {
2009
1963
  mkdirSync5(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
2010
1964
  }
2011
1965
  }
2012
1966
  function markerPath(sessionId) {
2013
- return join5(SESSIONS_DIR, `${sessionId}.json`);
1967
+ return join6(SESSIONS_DIR, `${sessionId}.json`);
2014
1968
  }
2015
1969
  function registerSessionCommands(program2) {
2016
1970
  const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
@@ -2026,7 +1980,7 @@ function registerSessionCommands(program2) {
2026
1980
  console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
2027
1981
  });
2028
1982
  session.command("list").description("List active session markers").action(() => {
2029
- if (!existsSync6(SESSIONS_DIR)) {
1983
+ if (!existsSync7(SESSIONS_DIR)) {
2030
1984
  console.log("[prim] no session markers");
2031
1985
  return;
2032
1986
  }
@@ -2038,7 +1992,7 @@ function registerSessionCommands(program2) {
2038
1992
  for (const f of files) {
2039
1993
  const sessionId = f.replace(/\.json$/, "");
2040
1994
  try {
2041
- const m = JSON.parse(readFileSync6(join5(SESSIONS_DIR, f), "utf-8"));
1995
+ const m = JSON.parse(readFileSync6(join6(SESSIONS_DIR, f), "utf-8"));
2042
1996
  console.log(`${sessionId} org=${m.orgId}`);
2043
1997
  } catch {
2044
1998
  }
@@ -2046,7 +2000,7 @@ function registerSessionCommands(program2) {
2046
2000
  });
2047
2001
  session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
2048
2002
  const p = markerPath(sessionId);
2049
- if (!existsSync6(p)) {
2003
+ if (!existsSync7(p)) {
2050
2004
  console.log(`[prim] no marker for session ${sessionId}`);
2051
2005
  return;
2052
2006
  }
@@ -2058,17 +2012,17 @@ function registerSessionCommands(program2) {
2058
2012
  // src/commands/skill.ts
2059
2013
  import {
2060
2014
  closeSync as closeSync2,
2061
- existsSync as existsSync7,
2015
+ existsSync as existsSync8,
2062
2016
  fsyncSync as fsyncSync2,
2063
2017
  openSync as openSync2,
2064
2018
  readFileSync as readFileSync7,
2065
2019
  renameSync as renameSync3,
2066
2020
  writeFileSync as writeFileSync6
2067
2021
  } from "fs";
2068
- import { dirname as dirname3, resolve as resolve2 } from "path";
2069
- import { fileURLToPath } from "url";
2022
+ import { dirname as dirname4, resolve as resolve2 } from "path";
2023
+ import { fileURLToPath as fileURLToPath2 } from "url";
2070
2024
  import { createPatch } from "diff";
2071
- var __dirname = dirname3(fileURLToPath(import.meta.url));
2025
+ var __dirname = dirname4(fileURLToPath2(import.meta.url));
2072
2026
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
2073
2027
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
2074
2028
  var TARGET_CANDIDATES = [
@@ -2081,15 +2035,15 @@ var TARGET_CANDIDATES = [
2081
2035
  var DEFAULT_TARGET = "CLAUDE.md";
2082
2036
  function loadSkill() {
2083
2037
  let dir = __dirname;
2084
- while (dir !== dirname3(dir)) {
2038
+ while (dir !== dirname4(dir)) {
2085
2039
  const p = resolve2(dir, "SKILL.md");
2086
- if (existsSync7(p)) return readFileSync7(p, "utf-8");
2087
- dir = dirname3(dir);
2040
+ if (existsSync8(p)) return readFileSync7(p, "utf-8");
2041
+ dir = dirname4(dir);
2088
2042
  }
2089
2043
  throw new Error("SKILL.md not found in package");
2090
2044
  }
2091
2045
  function detectTargets(cwd) {
2092
- return TARGET_CANDIDATES.filter((p) => existsSync7(resolve2(cwd, p)));
2046
+ return TARGET_CANDIDATES.filter((p) => existsSync8(resolve2(cwd, p)));
2093
2047
  }
2094
2048
  function detectNewline(content) {
2095
2049
  return content.includes("\r\n") ? "\r\n" : "\n";
@@ -2138,7 +2092,7 @@ function resolveTarget(cwd, override) {
2138
2092
  function runInstall(cwd, opts) {
2139
2093
  const target = resolveTarget(cwd, opts.target);
2140
2094
  if (target === null) return 1;
2141
- const existing = existsSync7(target) ? readFileSync7(target, "utf-8") : "";
2095
+ const existing = existsSync8(target) ? readFileSync7(target, "utf-8") : "";
2142
2096
  const eol = existing ? detectNewline(existing) : "\n";
2143
2097
  const block = composeBlock(loadSkill(), eol);
2144
2098
  const next = applyBlock(existing, block, eol);
@@ -2157,7 +2111,7 @@ function runInstall(cwd, opts) {
2157
2111
  function runUninstall(cwd, opts) {
2158
2112
  const target = resolveTarget(cwd, opts.target);
2159
2113
  if (target === null) return 1;
2160
- if (!existsSync7(target)) {
2114
+ if (!existsSync8(target)) {
2161
2115
  console.log(`Skill block not present at ${target}`);
2162
2116
  return 0;
2163
2117
  }
@@ -2174,7 +2128,7 @@ function runUninstall(cwd, opts) {
2174
2128
  function runStatus(cwd, opts) {
2175
2129
  const target = resolveTarget(cwd, opts.target);
2176
2130
  if (target === null) return 1;
2177
- const fileExists = existsSync7(target);
2131
+ const fileExists = existsSync8(target);
2178
2132
  let installed = false;
2179
2133
  if (fileExists) {
2180
2134
  const content = readFileSync7(target, "utf-8");
@@ -2213,284 +2167,18 @@ function registerSkillCommands(program2) {
2213
2167
  });
2214
2168
  }
2215
2169
 
2216
- // src/commands/spec.ts
2217
- import { readFileSync as readFileSync8 } from "fs";
2218
- function registerSpecCommands(program2) {
2219
- const spec = program2.command("spec").description("Manage spec documents");
2220
- 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) => {
2221
- const client = getClient();
2222
- if (opts.projectId) {
2223
- const specs = await client.get(`/api/cli/specs?rootTaskId=${opts.projectId}`);
2224
- if (opts.json) {
2225
- printJson(specs[0] ?? null);
2226
- return;
2227
- }
2228
- if (specs.length === 0) {
2229
- console.error("No spec document found for this project.");
2230
- return;
2231
- }
2232
- printSpec(specs[0]);
2233
- return;
2234
- }
2235
- const contexts = await client.get("/api/cli/specs");
2236
- if (opts.json) {
2237
- printJson(contexts);
2238
- return;
2239
- }
2240
- if (contexts.length === 0) {
2241
- console.error("No spec documents found.");
2242
- return;
2243
- }
2244
- for (const ctx of contexts) {
2245
- const scope = ctx.scope === "task" ? "project" : ctx.scope ?? "project";
2246
- const review = ctx.specReviewStatus ?? "\u2014";
2247
- const name = ctx.name ?? "(unnamed)";
2248
- console.log(`${ctx._id} ${scope.padEnd(8)} ${String(review).padEnd(10)} ${name}`);
2249
- }
2250
- console.error(`
2251
- ${contexts.length} spec(s)`);
2252
- });
2253
- spec.command("get <contextId>").description("Get a spec document by ID").option("--text-only", "Print only the text content (no metadata)").option("--json", "Output as JSON (overrides --text-only)").action(async (contextId, opts) => {
2254
- const client = getClient();
2255
- const ctx = await client.get(`/api/cli/contexts/${contextId}`);
2256
- if (opts.json) {
2257
- printJson(ctx);
2258
- return;
2259
- }
2260
- if (opts.textOnly) {
2261
- console.log(ctx.text ?? "");
2262
- return;
2263
- }
2264
- printSpec(ctx);
2265
- });
2266
- spec.command("create").description("Create a new spec document").requiredOption("-s, --scope <scope>", "Scope: project, global, external").requiredOption("-n, --name <name>", "Spec name").option("-t, --text <text>", "Spec text content").option("-f, --file <path>", "Read text content from file").option("--project-id <projectId>", "Link to project(s), comma-separated").option("--branch <branch>", "Link spec to this branch on the current repo").option("--pr <prNumber>", "Optional PR number to attach to the link").option("--json", "Output as JSON").action(
2267
- async (opts) => {
2268
- const client = getClient();
2269
- let text = opts.text;
2270
- if (opts.file) {
2271
- text = readFileSync8(opts.file, "utf-8");
2272
- }
2273
- const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
2274
- let linkedBranch;
2275
- if (opts.branch) {
2276
- const { repoFullName } = getGitContext();
2277
- if (!repoFullName) {
2278
- console.warn(
2279
- "[prim] --branch supplied but origin remote is not GitHub; skipping link."
2280
- );
2281
- } else {
2282
- linkedBranch = { repoFullName, branch: opts.branch };
2283
- if (opts.pr) {
2284
- const n = Number.parseInt(opts.pr, 10);
2285
- if (Number.isFinite(n)) linkedBranch.prNumber = n;
2286
- }
2287
- }
2288
- }
2289
- const result = await client.post("/api/cli/contexts", {
2290
- scope: opts.scope === "project" ? "task" : opts.scope,
2291
- name: opts.name,
2292
- text,
2293
- taskIds,
2294
- isSpecDocument: true,
2295
- linkedBranch
2296
- });
2297
- if (opts.json) {
2298
- printJson({ _id: result._id });
2299
- return;
2300
- }
2301
- console.error(
2302
- `Created spec: ${result._id}${linkedBranch ? ` (linked to ${linkedBranch.branch})` : ""}`
2303
- );
2304
- console.log(result._id);
2305
- }
2306
- );
2307
- spec.command("update <contextId>").description("Update a spec document's text content").option("-t, --text <text>", "New text content").option("-f, --file <path>", "Read text content from file").option("-n, --name <name>", "New name").option("--json", "Output as JSON").action(
2308
- async (contextId, opts) => {
2309
- const client = getClient();
2310
- let text = opts.text;
2311
- if (opts.file) {
2312
- text = readFileSync8(opts.file, "utf-8");
2313
- }
2314
- if (!(text || opts.name)) {
2315
- console.error("Provide --text, --file, or --name to update.");
2316
- process.exit(1);
2317
- }
2318
- await client.patch(`/api/cli/contexts/${contextId}`, {
2319
- name: opts.name,
2320
- text,
2321
- skipTiptapLifecycle: !!text
2322
- });
2323
- if (text) {
2324
- await client.post(`/api/cli/contexts/${contextId}/inject`);
2325
- }
2326
- if (opts.json) {
2327
- printJson({ _id: contextId });
2328
- return;
2329
- }
2330
- console.error(`Updated spec: ${contextId}`);
2331
- console.log(contextId);
2332
- }
2333
- );
2334
- spec.command("sync <contextId>").description("Trigger spec \u2194 project DAG synchronization").option("--json", "Output as JSON").action(async (contextId, opts) => {
2335
- const client = getClient();
2336
- const ctx = await client.get(`/api/cli/contexts/${contextId}`);
2337
- if (!ctx.isSpecDocument) {
2338
- console.error("Context is not a spec document. Use `prim context` instead.");
2339
- process.exit(1);
2340
- }
2341
- await client.post(`/api/cli/contexts/${contextId}/sync`);
2342
- if (opts.json) {
2343
- printJson(
2344
- ctx.specRootTaskId ? { _id: contextId, specRootTaskId: ctx.specRootTaskId } : { _id: contextId }
2345
- );
2346
- return;
2347
- }
2348
- console.error(`Triggered sync for spec: ${contextId}`);
2349
- if (ctx.specRootTaskId) {
2350
- console.error(`Root project: ${ctx.specRootTaskId}`);
2351
- }
2352
- console.log(contextId);
2353
- });
2354
- spec.command("review <contextId>").description("Manually trigger the PR Intent Review bot for a spec").requiredOption("--pr <prNumber>", "PR number to review against").option("--sha <headSha>", "Commit SHA the review runs against (defaults to current HEAD)").action(async (contextId, opts) => {
2355
- const prNumber = Number.parseInt(opts.pr, 10);
2356
- if (!Number.isFinite(prNumber)) {
2357
- console.error("--pr must be an integer.");
2358
- process.exit(1);
2359
- }
2360
- const headSha = opts.sha ?? getGitContext().sha;
2361
- if (!headSha) {
2362
- console.error("Could not determine head SHA \u2014 pass --sha or run inside a git checkout.");
2363
- process.exit(1);
2364
- }
2365
- const client = getClient();
2366
- await client.post(`/api/cli/contexts/${contextId}/review`, {
2367
- prNumber,
2368
- headSha
2369
- });
2370
- console.log(
2371
- `Scheduled review: ${contextId} against PR #${String(prNumber)} @ ${headSha.slice(0, 7)}`
2372
- );
2373
- });
2374
- spec.command("drift <contextId>").description("Dispatch the Claude Code drift-fix workflow against a PR").requiredOption("--pr <prNumber>", "PR number to dispatch the drift-fix workflow against").action(async (contextId, opts) => {
2375
- const prNumber = Number.parseInt(opts.pr, 10);
2376
- if (!Number.isFinite(prNumber)) {
2377
- console.error("--pr must be an integer.");
2378
- process.exit(1);
2379
- }
2380
- const client = getClient();
2381
- const result = await client.post(`/api/cli/contexts/${contextId}/drift`, {
2382
- prNumber
2383
- });
2384
- if (result.dispatched) {
2385
- const ref = result.runUrl ? `: ${result.runUrl}` : "";
2386
- console.log(`Dispatched drift-fix workflow${ref}`);
2387
- } else {
2388
- console.error(
2389
- "Drift-fix dispatch failed. Likely causes: actions:write App scope not granted, primitive-drift-fix.yml workflow file missing, or no findings on the latest review."
2390
- );
2391
- process.exit(1);
2392
- }
2393
- });
2394
- spec.command("status <taskId>").description(
2395
- "Show task status, auto-complete suppression flag, and the most-recent bot auto-completion"
2396
- ).action(async (taskId) => {
2397
- const client = getClient();
2398
- const result = await client.get(`/api/cli/tasks/${taskId}/status`);
2399
- console.log(`status: ${result.status}`);
2400
- console.log(`auto-complete suppressed: ${result.autoCompleteSuppressed ? "yes" : "no"}`);
2401
- const last = result.lastAutoCompleteActivity;
2402
- if (last) {
2403
- const when = last.createdAt ? new Date(last.createdAt).toISOString() : "\u2014";
2404
- const pr = last.prNumber ? `#${String(last.prNumber)}` : "\u2014";
2405
- console.log(`last auto-complete: ${when} (PR ${pr})`);
2406
- if (last.explanation) {
2407
- console.log(` ${last.explanation}`);
2408
- }
2409
- } else {
2410
- console.log("last auto-complete: \u2014");
2411
- }
2412
- });
2413
- spec.command("map <contextId>").description("Map file patterns to a spec (used by pre-commit hook to detect affected specs)").requiredOption(
2414
- "-p, --pattern <patterns...>",
2415
- 'Glob pattern(s) to associate, e.g. "src/auth/**"'
2416
- ).option("--json", "Output as JSON").action(async (contextId, opts) => {
2417
- const client = getClient();
2418
- const result = await client.post(`/api/cli/contexts/${contextId}/map`, {
2419
- patterns: opts.pattern
2420
- });
2421
- if (opts.json) {
2422
- printJson({ _id: contextId, filePatterns: result.filePatterns });
2423
- return;
2424
- }
2425
- console.error(`Mapped patterns to spec ${contextId}:`);
2426
- for (const p of result.filePatterns) {
2427
- console.error(` ${p}`);
2428
- }
2429
- console.log(contextId);
2430
- });
2431
- spec.command("unmap <contextId>").description("Remove file pattern mappings from a spec (omit --pattern to clear all)").option("-p, --pattern <patterns...>", "Specific pattern(s) to remove (omit to clear all)").option("--json", "Output as JSON").action(async (contextId, opts) => {
2432
- const client = getClient();
2433
- const result = await client.post(`/api/cli/contexts/${contextId}/unmap`, {
2434
- patterns: opts.pattern
2435
- });
2436
- if (opts.json) {
2437
- printJson({ _id: contextId, filePatterns: result.filePatterns });
2438
- return;
2439
- }
2440
- if (result.filePatterns.length === 0) {
2441
- console.error(`Cleared all file patterns from spec ${contextId}`);
2442
- } else {
2443
- console.error(`Updated patterns for spec ${contextId}:`);
2444
- for (const p of result.filePatterns) {
2445
- console.error(` ${p}`);
2446
- }
2447
- }
2448
- console.log(contextId);
2449
- });
2450
- spec.command("auto-map <contextId>").description("Trigger auto-mapping of file patterns for a spec").option("--json", "Output as JSON").action(async (contextId, opts) => {
2451
- const client = getClient();
2452
- await client.post(`/api/cli/contexts/${contextId}/auto-map`);
2453
- if (opts.json) {
2454
- printJson({ _id: contextId });
2455
- return;
2456
- }
2457
- console.error(`Auto-mapping triggered for spec: ${contextId}`);
2458
- console.log(contextId);
2459
- });
2460
- }
2461
- function printSpec(ctx) {
2462
- const name = ctx.name ?? ctx.title ?? "(unnamed)";
2463
- const review = ctx.specReviewStatus ?? "\u2014";
2464
- const patterns = ctx.filePatterns;
2465
- console.log(`ID: ${ctx._id}`);
2466
- console.log(`Name: ${name}`);
2467
- console.log(`Scope: ${ctx.scope === "task" ? "project" : ctx.scope ?? "project"}`);
2468
- console.log(`Review Status: ${review}`);
2469
- console.log(`Root Project: ${ctx.specRootTaskId ?? "\u2014"}`);
2470
- console.log(`Sync Version: ${ctx.syncVersion ?? 0}`);
2471
- console.log(`Index Status: ${ctx.indexStatus ?? "\u2014"}`);
2472
- console.log(`File Patterns: ${patterns?.length ? patterns.join(", ") : "\u2014"}`);
2473
- if (ctx.text) {
2474
- const text = ctx.text;
2475
- const preview = text.length > 500 ? `${text.slice(0, 500)}\u2026` : text;
2476
- console.log(`
2477
- --- Text ---
2478
- ${preview}`);
2479
- }
2480
- }
2481
-
2482
2170
  // 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";
2171
+ import { readFileSync as readFileSync8 } from "fs";
2172
+ import { dirname as dirname5, resolve as resolve3 } from "path";
2173
+ import { fileURLToPath as fileURLToPath3 } from "url";
2486
2174
  var STATUSLINE_TIMEOUT_MS = 200;
2487
2175
  function readPackageVersion() {
2488
2176
  try {
2489
- const here = dirname4(fileURLToPath2(import.meta.url));
2177
+ const here = dirname5(fileURLToPath3(import.meta.url));
2490
2178
  const candidates = [resolve3(here, "../../package.json"), resolve3(here, "../package.json")];
2491
2179
  for (const path of candidates) {
2492
2180
  try {
2493
- const pkg2 = JSON.parse(readFileSync9(path, "utf-8"));
2181
+ const pkg2 = JSON.parse(readFileSync8(path, "utf-8"));
2494
2182
  if (pkg2.version) {
2495
2183
  return pkg2.version;
2496
2184
  }
@@ -2535,18 +2223,15 @@ function registerStatuslineCommands(program2) {
2535
2223
  }
2536
2224
 
2537
2225
  // src/index.ts
2538
- var __dirname2 = dirname5(fileURLToPath3(import.meta.url));
2539
- var pkg = JSON.parse(readFileSync10(resolve4(__dirname2, "../package.json"), "utf-8"));
2226
+ var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
2227
+ var pkg = JSON.parse(readFileSync9(resolve4(__dirname2, "../package.json"), "utf-8"));
2540
2228
  updateNotifier({ pkg }).notify();
2541
2229
  var program = new Command();
2542
- program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
2230
+ program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
2543
2231
  "--non-interactive",
2544
2232
  "fail fast instead of prompting (also: CI=1, PRIM_NON_INTERACTIVE=1)"
2545
2233
  );
2546
2234
  registerAuthCommands(program);
2547
- registerContextCommands(program);
2548
- registerSpecCommands(program);
2549
- registerProjectCommands(program);
2550
2235
  registerHooksCommands(program);
2551
2236
  registerSkillCommands(program);
2552
2237
  registerMovesCommands(program);