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

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
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ bold,
3
4
  color,
4
5
  colorForArea,
6
+ dim,
5
7
  stripAnsi
6
- } from "./chunk-BEEGFDGU.js";
8
+ } from "./chunk-4QJOQIY6.js";
7
9
  import {
8
10
  checkAffectedDecisions,
9
11
  daemonOrDirectGet,
10
- formatDecisionsWarning,
11
- getGitContext
12
- } from "./chunk-TPQ3X244.js";
12
+ formatDecisionsWarning
13
+ } from "./chunk-E5UZXMZL.js";
13
14
  import {
14
15
  HttpError,
15
16
  REFRESH_TOKEN_PATH,
@@ -20,7 +21,7 @@ import {
20
21
  getSiteUrl,
21
22
  getTokenExpiresAt,
22
23
  saveTokenExpiry
23
- } from "./chunk-6SIEWWUL.js";
24
+ } from "./chunk-26VA3ADF.js";
24
25
  import {
25
26
  JOURNAL_DIR,
26
27
  SESSIONS_DIR,
@@ -34,7 +35,7 @@ import {
34
35
  } from "./chunk-UTKQTZHL.js";
35
36
 
36
37
  // src/index.ts
37
- import { readFileSync as readFileSync11 } from "fs";
38
+ import { readFileSync as readFileSync9 } from "fs";
38
39
  import { dirname as dirname6, resolve as resolve4 } from "path";
39
40
  import { fileURLToPath as fileURLToPath4 } from "url";
40
41
  import { Command } from "commander";
@@ -747,128 +748,27 @@ ${line("project", result.project)}`);
747
748
  });
748
749
  }
749
750
 
750
- // src/commands/context.ts
751
- import { readFileSync as readFileSync4 } from "fs";
752
- function registerContextCommands(program2) {
753
- const context = program2.command("context").description("Manage contexts");
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) => {
755
- const client = getClient();
756
- const params = new URLSearchParams();
757
- if (opts.projectId) {
758
- params.set("taskId", opts.projectId);
759
- }
760
- if (opts.scope) {
761
- params.set("scope", opts.scope === "project" ? "task" : opts.scope);
762
- }
763
- const contexts = await client.get(`/api/cli/contexts?${params.toString()}`);
764
- if (opts.json) {
765
- printJson(contexts);
766
- return;
767
- }
768
- printContextList(contexts);
769
- });
770
- context.command("get <contextId>").description("Get a context by ID").option("--json", "Output as JSON (default behavior; accepted for symmetry)").action(async (contextId) => {
771
- const client = getClient();
772
- const ctx = await client.get(`/api/cli/contexts/${contextId}`);
773
- printJson(ctx);
774
- });
775
- 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(
776
- async (opts) => {
777
- const client = getClient();
778
- let text = opts.text;
779
- if (opts.file) {
780
- text = readFileSync4(opts.file, "utf-8");
781
- }
782
- const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
783
- const result = await client.post("/api/cli/contexts", {
784
- scope: opts.scope === "project" ? "task" : opts.scope,
785
- name: opts.name,
786
- text,
787
- taskIds,
788
- isSpecDocument: opts.spec ?? false
789
- });
790
- if (opts.json) {
791
- printJson({ _id: result._id });
792
- return;
793
- }
794
- console.error(`Created context: ${result._id}`);
795
- console.log(result._id);
796
- }
797
- );
798
- 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(
799
- async (contextId, opts) => {
800
- const client = getClient();
801
- let text = opts.text;
802
- if (opts.file) {
803
- text = readFileSync4(opts.file, "utf-8");
804
- }
805
- await client.patch(`/api/cli/contexts/${contextId}`, {
806
- name: opts.name,
807
- text
808
- });
809
- if (opts.json) {
810
- printJson({ _id: contextId });
811
- return;
812
- }
813
- console.error(`Updated context: ${contextId}`);
814
- console.log(contextId);
815
- }
816
- );
817
- context.command("delete <contextId>").description("Delete a context").option("--json", "Output as JSON").action(async (contextId, opts) => {
818
- const client = getClient();
819
- await client.delete(`/api/cli/contexts/${contextId}`);
820
- if (opts.json) {
821
- printJson({ _id: contextId });
822
- return;
823
- }
824
- console.error(`Deleted context: ${contextId}`);
825
- console.log(contextId);
826
- });
827
- 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) => {
828
- const client = getClient();
829
- await client.post(`/api/cli/contexts/${contextId}/link`, {
830
- taskId: opts.project
831
- });
832
- if (opts.json) {
833
- printJson({ _id: contextId, project: opts.project });
834
- return;
835
- }
836
- console.error(`Linked context ${contextId} to project ${opts.project}`);
837
- console.log(contextId);
838
- });
839
- 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) => {
840
- const client = getClient();
841
- await client.post(`/api/cli/contexts/${contextId}/unlink`, {
842
- taskId: opts.project
843
- });
844
- if (opts.json) {
845
- printJson({ _id: contextId, project: opts.project });
846
- return;
847
- }
848
- console.error(`Unlinked context ${contextId} from project ${opts.project}`);
849
- console.log(contextId);
850
- });
851
- }
852
- function printContextList(contexts) {
853
- if (contexts.length === 0) {
854
- console.error("No contexts found.");
855
- return;
751
+ // src/commands/daemon.ts
752
+ import { spawn } from "child_process";
753
+ import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
754
+ import { homedir as homedir3 } from "os";
755
+ import { join as join4 } from "path";
756
+
757
+ // src/lib/presence.ts
758
+ function formatTeammates(names, cap) {
759
+ if (names === void 0) {
760
+ return "\u2014";
856
761
  }
857
- for (const ctx of contexts) {
858
- const scope = ctx.scope === "task" ? "project" : ctx.scope ?? "project";
859
- const spec = ctx.isSpecDocument ? " [SPEC]" : "";
860
- const name = ctx.name ?? ctx.title ?? "(unnamed)";
861
- console.log(`${ctx._id} ${scope.padEnd(8)} ${name}${spec}`);
762
+ if (names.length === 0) {
763
+ return "just you";
764
+ }
765
+ if (names.length <= cap) {
766
+ return names.join(", ");
862
767
  }
863
- console.error(`
864
- ${contexts.length} context(s)`);
768
+ return `${names.slice(0, cap).join(", ")} +${String(names.length - cap)}`;
865
769
  }
866
770
 
867
771
  // src/commands/daemon.ts
868
- import { spawn } from "child_process";
869
- import { existsSync as existsSync4, readFileSync as readFileSync5, unlinkSync } from "fs";
870
- import { homedir as homedir3 } from "os";
871
- import { join as join4 } from "path";
872
772
  var DAEMON_BIN = "prim-daemon-server";
873
773
  var PID_PATH = join4(homedir3(), ".config", "prim", "daemon.pid");
874
774
  var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
@@ -885,7 +785,7 @@ function readPidfile() {
885
785
  if (!existsSync4(PID_PATH)) {
886
786
  return null;
887
787
  }
888
- const raw = readFileSync5(PID_PATH, "utf-8").trim();
788
+ const raw = readFileSync4(PID_PATH, "utf-8").trim();
889
789
  const pid = Number(raw);
890
790
  if (!Number.isInteger(pid) || pid <= 0) {
891
791
  return null;
@@ -1042,8 +942,9 @@ async function daemonStatus() {
1042
942
  } else if (!snapshot) {
1043
943
  process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
1044
944
  } else {
945
+ const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
1045
946
  process.stderr.write(
1046
- `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
947
+ `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}${team}
1047
948
  `
1048
949
  );
1049
950
  }
@@ -1460,6 +1361,44 @@ function formatConfirmJson(result) {
1460
1361
  return JSON.stringify(result.outcome, null, 2);
1461
1362
  }
1462
1363
 
1364
+ // src/decisions/create.ts
1365
+ var CREATE_TIMEOUT_MS = 1e4;
1366
+ var defaultDeps4 = { getClient };
1367
+ function toRequestBody(request) {
1368
+ const candidate = {
1369
+ intent: request.intent,
1370
+ kind: request.kind,
1371
+ rationale: request.rationale,
1372
+ area: request.area,
1373
+ decided: request.decided,
1374
+ alternatives: request.alternatives,
1375
+ confidence: request.confidence,
1376
+ reversibility: request.reversibility,
1377
+ files: request.files
1378
+ };
1379
+ const body = {};
1380
+ for (const [key, value] of Object.entries(candidate)) {
1381
+ const isEmpty = value === void 0 || Array.isArray(value) && value.length === 0;
1382
+ if (!isEmpty) {
1383
+ body[key] = value;
1384
+ }
1385
+ }
1386
+ return body;
1387
+ }
1388
+ async function fetchCreate(request, deps = defaultDeps4) {
1389
+ const client = deps.getClient();
1390
+ return await client.post("/api/cli/decisions/create", toRequestBody(request), {
1391
+ signal: AbortSignal.timeout(CREATE_TIMEOUT_MS)
1392
+ });
1393
+ }
1394
+ function formatCreateHuman(outcome) {
1395
+ const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
1396
+ return `[prim] created ${id}.`;
1397
+ }
1398
+ function formatCreateJson(outcome) {
1399
+ return JSON.stringify(outcome, null, 2);
1400
+ }
1401
+
1463
1402
  // src/decisions/show.ts
1464
1403
  var NOT_FOUND_RE3 = /not found/i;
1465
1404
  function colorStatus(status) {
@@ -1472,14 +1411,14 @@ function colorStatus(status) {
1472
1411
  return color(status, "gray");
1473
1412
  }
1474
1413
  var SHOW_TIMEOUT_MS = 1e4;
1475
- var defaultDeps4 = { getClient };
1414
+ var defaultDeps5 = { getClient };
1476
1415
  var DecisionNotFoundError = class extends Error {
1477
1416
  constructor(idOrShortId) {
1478
1417
  super(`Decision not found: ${idOrShortId}`);
1479
1418
  this.name = "DecisionNotFoundError";
1480
1419
  }
1481
1420
  };
1482
- async function fetchShow(idOrShortId, deps = defaultDeps4) {
1421
+ async function fetchShow(idOrShortId, deps = defaultDeps5) {
1483
1422
  const params = new URLSearchParams({ id: idOrShortId });
1484
1423
  const client = deps.getClient();
1485
1424
  try {
@@ -1596,13 +1535,15 @@ function formatShowJson(result) {
1596
1535
 
1597
1536
  // src/commands/decisions.ts
1598
1537
  var EXIT_NOT_FOUND = 4;
1538
+ var EXIT_USAGE = 2;
1539
+ var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
1599
1540
  function registerDecisionsCommands(program2) {
1600
1541
  const decisions = program2.command("decisions").description("Inspect the project Decision Graph");
1601
1542
  decisions.command("check").description("Look up active decisions that reference one or more file paths").requiredOption(
1602
1543
  "--files <files>",
1603
1544
  "Comma-separated file paths to check against the Decision Graph"
1604
1545
  ).action(async (opts) => {
1605
- const filePaths = opts.files.split(",").map((s) => s.trim()).filter(Boolean);
1546
+ const filePaths = splitList(opts.files);
1606
1547
  const result = await checkAffectedDecisions(filePaths);
1607
1548
  const warning = formatDecisionsWarning(result);
1608
1549
  if (warning) {
@@ -1664,11 +1605,45 @@ function registerDecisionsCommands(program2) {
1664
1605
  throw err;
1665
1606
  }
1666
1607
  });
1608
+ decisions.command("create").description("Author a decision directly \u2014 the deliberate manual path around automatic capture").requiredOption("--intent <text>", "What was decided (required)").option("--kind <kind>", "change | exploration | task_execution | unclear (default change)").option("--rationale <text>", "Why the decision was made").option(
1609
+ "--area <area>",
1610
+ "Functional area (auth, data, infra, ui, api, billing, mobile, docs, testing)"
1611
+ ).option("--decided <items>", "Comma-separated option(s) chosen").option("--alternatives <items>", "Comma-separated options considered but rejected").option("--confidence <level>", "high | medium | low (default high)").option("--reversibility <level>", "high | low (default high)").option(
1612
+ "--files <paths>",
1613
+ "Comma-separated repo-relative paths this decision governs (gates edits to them)"
1614
+ ).action(async (opts) => {
1615
+ const request = {
1616
+ intent: opts.intent,
1617
+ kind: opts.kind,
1618
+ rationale: opts.rationale,
1619
+ area: opts.area,
1620
+ decided: splitList(opts.decided),
1621
+ alternatives: splitList(opts.alternatives),
1622
+ confidence: opts.confidence,
1623
+ reversibility: opts.reversibility,
1624
+ files: splitList(opts.files)
1625
+ };
1626
+ try {
1627
+ const outcome = await fetchCreate(request);
1628
+ console.error(formatCreateHuman(outcome));
1629
+ console.log(formatCreateJson(outcome));
1630
+ } catch (err) {
1631
+ if (err instanceof HttpError && err.status >= 400 && err.status < 500) {
1632
+ console.error(`[prim] create rejected: ${err.message}`);
1633
+ console.log(
1634
+ JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2)
1635
+ );
1636
+ process.exitCode = EXIT_USAGE;
1637
+ return;
1638
+ }
1639
+ throw err;
1640
+ }
1641
+ });
1667
1642
  }
1668
1643
 
1669
1644
  // src/commands/hooks.ts
1670
1645
  import { execSync } from "child_process";
1671
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1646
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1672
1647
  import { resolve } from "path";
1673
1648
  import { Option } from "commander";
1674
1649
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
@@ -1717,7 +1692,7 @@ function detectHusky(gitRoot) {
1717
1692
  const pkgPath = resolve(gitRoot, "package.json");
1718
1693
  if (existsSync5(pkgPath)) {
1719
1694
  try {
1720
- const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
1695
+ const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
1721
1696
  const scripts = pkg2.scripts ?? {};
1722
1697
  if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
1723
1698
  return true;
@@ -1745,7 +1720,7 @@ async function askConfirmation(question) {
1745
1720
  function installToHusky(gitRoot, spec = PRE_COMMIT) {
1746
1721
  const hookPath = resolve(gitRoot, ".husky", spec.hookName);
1747
1722
  if (existsSync5(hookPath)) {
1748
- const existing = readFileSync6(hookPath, "utf-8");
1723
+ const existing = readFileSync5(hookPath, "utf-8");
1749
1724
  if (containsPrimHook(existing, spec.binName)) {
1750
1725
  console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
1751
1726
  return;
@@ -1773,7 +1748,7 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
1773
1748
  mkdirSync3(hooksDir, { recursive: true });
1774
1749
  }
1775
1750
  if (existsSync5(hookPath)) {
1776
- const existing = readFileSync6(hookPath, "utf-8");
1751
+ const existing = readFileSync5(hookPath, "utf-8");
1777
1752
  if (containsPrimHook(existing, spec.binName)) {
1778
1753
  console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
1779
1754
  return;
@@ -1840,7 +1815,7 @@ function registerHooksCommands(program2) {
1840
1815
  console.log(`No ${spec.hookName} hook found.`);
1841
1816
  continue;
1842
1817
  }
1843
- if (containsPrimHook(readFileSync6(hookPath, "utf-8"), spec.binName)) {
1818
+ if (containsPrimHook(readFileSync5(hookPath, "utf-8"), spec.binName)) {
1844
1819
  unlinkSync2(hookPath);
1845
1820
  console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
1846
1821
  } else {
@@ -1982,31 +1957,9 @@ function registerMovesCommands(program2) {
1982
1957
  });
1983
1958
  }
1984
1959
 
1985
- // src/commands/project.ts
1986
- function registerProjectCommands(program2) {
1987
- const project = program2.command("project").description("Manage projects");
1988
- 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) => {
1989
- const client = getClient();
1990
- const result = await client.post("/api/cli/tasks", {
1991
- name: opts.name,
1992
- description: opts.description,
1993
- specContextId: opts.spec
1994
- });
1995
- if (opts.json) {
1996
- printJson(opts.spec ? { _id: result._id, spec: opts.spec } : { _id: result._id });
1997
- return;
1998
- }
1999
- console.error(`Created project: ${result._id}`);
2000
- if (opts.spec) {
2001
- console.error(`Linked spec: ${opts.spec}`);
2002
- }
2003
- console.log(result._id);
2004
- });
2005
- }
2006
-
2007
1960
  // src/commands/reconcile.ts
2008
1961
  var EXIT_OK2 = 0;
2009
- var EXIT_USAGE = 2;
1962
+ var EXIT_USAGE2 = 2;
2010
1963
  var EXIT_SERVER = 3;
2011
1964
  var HTTP_CLIENT_ERROR_MIN = 400;
2012
1965
  var HTTP_SERVER_ERROR_MIN = 500;
@@ -2050,7 +2003,7 @@ async function performReconcile(idOrShortId, opts = {}) {
2050
2003
  process.stderr.write(`[prim] reconcile rejected: ${err.message}
2051
2004
  `);
2052
2005
  console.log(JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2));
2053
- process.exitCode = EXIT_USAGE;
2006
+ process.exitCode = EXIT_USAGE2;
2054
2007
  return;
2055
2008
  }
2056
2009
  const message = err instanceof Error ? err.message : String(err);
@@ -2090,7 +2043,7 @@ function registerReconcileCommands(program2) {
2090
2043
  import {
2091
2044
  existsSync as existsSync7,
2092
2045
  mkdirSync as mkdirSync5,
2093
- readFileSync as readFileSync7,
2046
+ readFileSync as readFileSync6,
2094
2047
  readdirSync,
2095
2048
  unlinkSync as unlinkSync5,
2096
2049
  writeFileSync as writeFileSync5
@@ -2132,7 +2085,7 @@ function registerSessionCommands(program2) {
2132
2085
  for (const f of files) {
2133
2086
  const sessionId = f.replace(/\.json$/, "");
2134
2087
  try {
2135
- const m = JSON.parse(readFileSync7(join6(SESSIONS_DIR, f), "utf-8"));
2088
+ const m = JSON.parse(readFileSync6(join6(SESSIONS_DIR, f), "utf-8"));
2136
2089
  console.log(`${sessionId} org=${m.orgId}`);
2137
2090
  } catch {
2138
2091
  }
@@ -2155,7 +2108,7 @@ import {
2155
2108
  existsSync as existsSync8,
2156
2109
  fsyncSync as fsyncSync2,
2157
2110
  openSync as openSync2,
2158
- readFileSync as readFileSync8,
2111
+ readFileSync as readFileSync7,
2159
2112
  renameSync as renameSync3,
2160
2113
  writeFileSync as writeFileSync6
2161
2114
  } from "fs";
@@ -2177,7 +2130,7 @@ function loadSkill() {
2177
2130
  let dir = __dirname;
2178
2131
  while (dir !== dirname4(dir)) {
2179
2132
  const p = resolve2(dir, "SKILL.md");
2180
- if (existsSync8(p)) return readFileSync8(p, "utf-8");
2133
+ if (existsSync8(p)) return readFileSync7(p, "utf-8");
2181
2134
  dir = dirname4(dir);
2182
2135
  }
2183
2136
  throw new Error("SKILL.md not found in package");
@@ -2232,7 +2185,7 @@ function resolveTarget(cwd, override) {
2232
2185
  function runInstall(cwd, opts) {
2233
2186
  const target = resolveTarget(cwd, opts.target);
2234
2187
  if (target === null) return 1;
2235
- const existing = existsSync8(target) ? readFileSync8(target, "utf-8") : "";
2188
+ const existing = existsSync8(target) ? readFileSync7(target, "utf-8") : "";
2236
2189
  const eol = existing ? detectNewline(existing) : "\n";
2237
2190
  const block = composeBlock(loadSkill(), eol);
2238
2191
  const next = applyBlock(existing, block, eol);
@@ -2255,7 +2208,7 @@ function runUninstall(cwd, opts) {
2255
2208
  console.log(`Skill block not present at ${target}`);
2256
2209
  return 0;
2257
2210
  }
2258
- const existing = readFileSync8(target, "utf-8");
2211
+ const existing = readFileSync7(target, "utf-8");
2259
2212
  const next = removeBlock(existing);
2260
2213
  if (next === null) {
2261
2214
  console.log(`Skill block not present at ${target}`);
@@ -2271,7 +2224,7 @@ function runStatus(cwd, opts) {
2271
2224
  const fileExists = existsSync8(target);
2272
2225
  let installed = false;
2273
2226
  if (fileExists) {
2274
- const content = readFileSync8(target, "utf-8");
2227
+ const content = readFileSync7(target, "utf-8");
2275
2228
  installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
2276
2229
  }
2277
2230
  if (opts.json) {
@@ -2307,284 +2260,19 @@ function registerSkillCommands(program2) {
2307
2260
  });
2308
2261
  }
2309
2262
 
2310
- // src/commands/spec.ts
2311
- import { readFileSync as readFileSync9 } from "fs";
2312
- function registerSpecCommands(program2) {
2313
- const spec = program2.command("spec").description("Manage spec documents");
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) => {
2315
- const client = getClient();
2316
- if (opts.projectId) {
2317
- const specs = await client.get(`/api/cli/specs?rootTaskId=${opts.projectId}`);
2318
- if (opts.json) {
2319
- printJson(specs[0] ?? null);
2320
- return;
2321
- }
2322
- if (specs.length === 0) {
2323
- console.error("No spec document found for this project.");
2324
- return;
2325
- }
2326
- printSpec(specs[0]);
2327
- return;
2328
- }
2329
- const contexts = await client.get("/api/cli/specs");
2330
- if (opts.json) {
2331
- printJson(contexts);
2332
- return;
2333
- }
2334
- if (contexts.length === 0) {
2335
- console.error("No spec documents found.");
2336
- return;
2337
- }
2338
- for (const ctx of contexts) {
2339
- const scope = ctx.scope === "task" ? "project" : ctx.scope ?? "project";
2340
- const review = ctx.specReviewStatus ?? "\u2014";
2341
- const name = ctx.name ?? "(unnamed)";
2342
- console.log(`${ctx._id} ${scope.padEnd(8)} ${String(review).padEnd(10)} ${name}`);
2343
- }
2344
- console.error(`
2345
- ${contexts.length} spec(s)`);
2346
- });
2347
- 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) => {
2348
- const client = getClient();
2349
- const ctx = await client.get(`/api/cli/contexts/${contextId}`);
2350
- if (opts.json) {
2351
- printJson(ctx);
2352
- return;
2353
- }
2354
- if (opts.textOnly) {
2355
- console.log(ctx.text ?? "");
2356
- return;
2357
- }
2358
- printSpec(ctx);
2359
- });
2360
- 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(
2361
- async (opts) => {
2362
- const client = getClient();
2363
- let text = opts.text;
2364
- if (opts.file) {
2365
- text = readFileSync9(opts.file, "utf-8");
2366
- }
2367
- const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
2368
- let linkedBranch;
2369
- if (opts.branch) {
2370
- const { repoFullName } = getGitContext();
2371
- if (!repoFullName) {
2372
- console.warn(
2373
- "[prim] --branch supplied but origin remote is not GitHub; skipping link."
2374
- );
2375
- } else {
2376
- linkedBranch = { repoFullName, branch: opts.branch };
2377
- if (opts.pr) {
2378
- const n = Number.parseInt(opts.pr, 10);
2379
- if (Number.isFinite(n)) linkedBranch.prNumber = n;
2380
- }
2381
- }
2382
- }
2383
- const result = await client.post("/api/cli/contexts", {
2384
- scope: opts.scope === "project" ? "task" : opts.scope,
2385
- name: opts.name,
2386
- text,
2387
- taskIds,
2388
- isSpecDocument: true,
2389
- linkedBranch
2390
- });
2391
- if (opts.json) {
2392
- printJson({ _id: result._id });
2393
- return;
2394
- }
2395
- console.error(
2396
- `Created spec: ${result._id}${linkedBranch ? ` (linked to ${linkedBranch.branch})` : ""}`
2397
- );
2398
- console.log(result._id);
2399
- }
2400
- );
2401
- 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(
2402
- async (contextId, opts) => {
2403
- const client = getClient();
2404
- let text = opts.text;
2405
- if (opts.file) {
2406
- text = readFileSync9(opts.file, "utf-8");
2407
- }
2408
- if (!(text || opts.name)) {
2409
- console.error("Provide --text, --file, or --name to update.");
2410
- process.exit(1);
2411
- }
2412
- await client.patch(`/api/cli/contexts/${contextId}`, {
2413
- name: opts.name,
2414
- text,
2415
- skipTiptapLifecycle: !!text
2416
- });
2417
- if (text) {
2418
- await client.post(`/api/cli/contexts/${contextId}/inject`);
2419
- }
2420
- if (opts.json) {
2421
- printJson({ _id: contextId });
2422
- return;
2423
- }
2424
- console.error(`Updated spec: ${contextId}`);
2425
- console.log(contextId);
2426
- }
2427
- );
2428
- spec.command("sync <contextId>").description("Trigger spec \u2194 project DAG synchronization").option("--json", "Output as JSON").action(async (contextId, opts) => {
2429
- const client = getClient();
2430
- const ctx = await client.get(`/api/cli/contexts/${contextId}`);
2431
- if (!ctx.isSpecDocument) {
2432
- console.error("Context is not a spec document. Use `prim context` instead.");
2433
- process.exit(1);
2434
- }
2435
- await client.post(`/api/cli/contexts/${contextId}/sync`);
2436
- if (opts.json) {
2437
- printJson(
2438
- ctx.specRootTaskId ? { _id: contextId, specRootTaskId: ctx.specRootTaskId } : { _id: contextId }
2439
- );
2440
- return;
2441
- }
2442
- console.error(`Triggered sync for spec: ${contextId}`);
2443
- if (ctx.specRootTaskId) {
2444
- console.error(`Root project: ${ctx.specRootTaskId}`);
2445
- }
2446
- console.log(contextId);
2447
- });
2448
- 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) => {
2449
- const prNumber = Number.parseInt(opts.pr, 10);
2450
- if (!Number.isFinite(prNumber)) {
2451
- console.error("--pr must be an integer.");
2452
- process.exit(1);
2453
- }
2454
- const headSha = opts.sha ?? getGitContext().sha;
2455
- if (!headSha) {
2456
- console.error("Could not determine head SHA \u2014 pass --sha or run inside a git checkout.");
2457
- process.exit(1);
2458
- }
2459
- const client = getClient();
2460
- await client.post(`/api/cli/contexts/${contextId}/review`, {
2461
- prNumber,
2462
- headSha
2463
- });
2464
- console.log(
2465
- `Scheduled review: ${contextId} against PR #${String(prNumber)} @ ${headSha.slice(0, 7)}`
2466
- );
2467
- });
2468
- 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) => {
2469
- const prNumber = Number.parseInt(opts.pr, 10);
2470
- if (!Number.isFinite(prNumber)) {
2471
- console.error("--pr must be an integer.");
2472
- process.exit(1);
2473
- }
2474
- const client = getClient();
2475
- const result = await client.post(`/api/cli/contexts/${contextId}/drift`, {
2476
- prNumber
2477
- });
2478
- if (result.dispatched) {
2479
- const ref = result.runUrl ? `: ${result.runUrl}` : "";
2480
- console.log(`Dispatched drift-fix workflow${ref}`);
2481
- } else {
2482
- console.error(
2483
- "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."
2484
- );
2485
- process.exit(1);
2486
- }
2487
- });
2488
- spec.command("status <taskId>").description(
2489
- "Show task status, auto-complete suppression flag, and the most-recent bot auto-completion"
2490
- ).action(async (taskId) => {
2491
- const client = getClient();
2492
- const result = await client.get(`/api/cli/tasks/${taskId}/status`);
2493
- console.log(`status: ${result.status}`);
2494
- console.log(`auto-complete suppressed: ${result.autoCompleteSuppressed ? "yes" : "no"}`);
2495
- const last = result.lastAutoCompleteActivity;
2496
- if (last) {
2497
- const when = last.createdAt ? new Date(last.createdAt).toISOString() : "\u2014";
2498
- const pr = last.prNumber ? `#${String(last.prNumber)}` : "\u2014";
2499
- console.log(`last auto-complete: ${when} (PR ${pr})`);
2500
- if (last.explanation) {
2501
- console.log(` ${last.explanation}`);
2502
- }
2503
- } else {
2504
- console.log("last auto-complete: \u2014");
2505
- }
2506
- });
2507
- spec.command("map <contextId>").description("Map file patterns to a spec (used by pre-commit hook to detect affected specs)").requiredOption(
2508
- "-p, --pattern <patterns...>",
2509
- 'Glob pattern(s) to associate, e.g. "src/auth/**"'
2510
- ).option("--json", "Output as JSON").action(async (contextId, opts) => {
2511
- const client = getClient();
2512
- const result = await client.post(`/api/cli/contexts/${contextId}/map`, {
2513
- patterns: opts.pattern
2514
- });
2515
- if (opts.json) {
2516
- printJson({ _id: contextId, filePatterns: result.filePatterns });
2517
- return;
2518
- }
2519
- console.error(`Mapped patterns to spec ${contextId}:`);
2520
- for (const p of result.filePatterns) {
2521
- console.error(` ${p}`);
2522
- }
2523
- console.log(contextId);
2524
- });
2525
- 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) => {
2526
- const client = getClient();
2527
- const result = await client.post(`/api/cli/contexts/${contextId}/unmap`, {
2528
- patterns: opts.pattern
2529
- });
2530
- if (opts.json) {
2531
- printJson({ _id: contextId, filePatterns: result.filePatterns });
2532
- return;
2533
- }
2534
- if (result.filePatterns.length === 0) {
2535
- console.error(`Cleared all file patterns from spec ${contextId}`);
2536
- } else {
2537
- console.error(`Updated patterns for spec ${contextId}:`);
2538
- for (const p of result.filePatterns) {
2539
- console.error(` ${p}`);
2540
- }
2541
- }
2542
- console.log(contextId);
2543
- });
2544
- spec.command("auto-map <contextId>").description("Trigger auto-mapping of file patterns for a spec").option("--json", "Output as JSON").action(async (contextId, opts) => {
2545
- const client = getClient();
2546
- await client.post(`/api/cli/contexts/${contextId}/auto-map`);
2547
- if (opts.json) {
2548
- printJson({ _id: contextId });
2549
- return;
2550
- }
2551
- console.error(`Auto-mapping triggered for spec: ${contextId}`);
2552
- console.log(contextId);
2553
- });
2554
- }
2555
- function printSpec(ctx) {
2556
- const name = ctx.name ?? ctx.title ?? "(unnamed)";
2557
- const review = ctx.specReviewStatus ?? "\u2014";
2558
- const patterns = ctx.filePatterns;
2559
- console.log(`ID: ${ctx._id}`);
2560
- console.log(`Name: ${name}`);
2561
- console.log(`Scope: ${ctx.scope === "task" ? "project" : ctx.scope ?? "project"}`);
2562
- console.log(`Review Status: ${review}`);
2563
- console.log(`Root Project: ${ctx.specRootTaskId ?? "\u2014"}`);
2564
- console.log(`Sync Version: ${ctx.syncVersion ?? 0}`);
2565
- console.log(`Index Status: ${ctx.indexStatus ?? "\u2014"}`);
2566
- console.log(`File Patterns: ${patterns?.length ? patterns.join(", ") : "\u2014"}`);
2567
- if (ctx.text) {
2568
- const text = ctx.text;
2569
- const preview = text.length > 500 ? `${text.slice(0, 500)}\u2026` : text;
2570
- console.log(`
2571
- --- Text ---
2572
- ${preview}`);
2573
- }
2574
- }
2575
-
2576
2263
  // src/commands/statusline.ts
2577
- import { readFileSync as readFileSync10 } from "fs";
2264
+ import { readFileSync as readFileSync8 } from "fs";
2578
2265
  import { dirname as dirname5, resolve as resolve3 } from "path";
2579
2266
  import { fileURLToPath as fileURLToPath3 } from "url";
2580
2267
  var STATUSLINE_TIMEOUT_MS = 200;
2268
+ var STATUSLINE_NAME_CAP = 3;
2581
2269
  function readPackageVersion() {
2582
2270
  try {
2583
2271
  const here = dirname5(fileURLToPath3(import.meta.url));
2584
2272
  const candidates = [resolve3(here, "../../package.json"), resolve3(here, "../package.json")];
2585
2273
  for (const path of candidates) {
2586
2274
  try {
2587
- const pkg2 = JSON.parse(readFileSync10(path, "utf-8"));
2275
+ const pkg2 = JSON.parse(readFileSync8(path, "utf-8"));
2588
2276
  if (pkg2.version) {
2589
2277
  return pkg2.version;
2590
2278
  }
@@ -2615,7 +2303,14 @@ async function renderStatusline() {
2615
2303
  if (snapshot.presenceStale) {
2616
2304
  return `primitive ${version} (daemon: live \xB7 presence: stale)`;
2617
2305
  }
2618
- const team = typeof snapshot.onlineCount === "number" ? `team: ${String(snapshot.onlineCount)} online` : "team: \u2014";
2306
+ let team;
2307
+ if (snapshot.onlineNames !== void 0) {
2308
+ team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
2309
+ } else if (typeof snapshot.onlineCount === "number") {
2310
+ team = `team: ${String(snapshot.onlineCount)} online`;
2311
+ } else {
2312
+ team = "team: \u2014";
2313
+ }
2619
2314
  return `primitive ${version} (daemon: live \xB7 ${team})`;
2620
2315
  }
2621
2316
  function registerStatuslineCommands(program2) {
@@ -2628,19 +2323,52 @@ function registerStatuslineCommands(program2) {
2628
2323
  });
2629
2324
  }
2630
2325
 
2326
+ // src/commands/welcome.ts
2327
+ var CMD_GUTTER = 38;
2328
+ function formatWelcome() {
2329
+ const cmd = (command, desc) => ` ${dim(command.padEnd(CMD_GUTTER))}${desc}`;
2330
+ const bullet = (text) => ` ${color("\u2022", "green")} ${text}`;
2331
+ return [
2332
+ bold(color("Welcome to Primitive", "green")),
2333
+ "",
2334
+ "Primitive captures the decisions your team makes while coding into a",
2335
+ "shared graph \u2014 and flags edits that conflict with earlier ones before",
2336
+ "they land.",
2337
+ "",
2338
+ bold("How it works"),
2339
+ bullet("Capture is automatic \u2014 keep coding; your decisions are recorded for you."),
2340
+ bullet("The conflict gate has your back: when an edit conflicts with a"),
2341
+ " load-bearing decision, prim surfaces it. Run `prim reconcile dec_<id>` to clear",
2342
+ " that decision and retry.",
2343
+ bullet('Occasional yes/no prompts confirm the "why" behind a decision \u2014'),
2344
+ " answering keeps the graph trustworthy.",
2345
+ "",
2346
+ bold("Get started"),
2347
+ cmd("prim decisions recent", "what your team has decided lately"),
2348
+ cmd("prim decisions check --files <files>", "what governs files you're about to change"),
2349
+ cmd("prim --help", "everything else"),
2350
+ "",
2351
+ dim("App: https://app.getprimitive.ai")
2352
+ ].join("\n");
2353
+ }
2354
+ function registerWelcomeCommand(program2) {
2355
+ program2.command("welcome").description("Print a brief orientation to Primitive's decision graph").action(() => {
2356
+ process.stderr.write(`${formatWelcome()}
2357
+ `);
2358
+ printJson({ welcomed: true });
2359
+ });
2360
+ }
2361
+
2631
2362
  // src/index.ts
2632
2363
  var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
2633
- var pkg = JSON.parse(readFileSync11(resolve4(__dirname2, "../package.json"), "utf-8"));
2364
+ var pkg = JSON.parse(readFileSync9(resolve4(__dirname2, "../package.json"), "utf-8"));
2634
2365
  updateNotifier({ pkg }).notify();
2635
2366
  var program = new Command();
2636
- program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
2367
+ program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
2637
2368
  "--non-interactive",
2638
2369
  "fail fast instead of prompting (also: CI=1, PRIM_NON_INTERACTIVE=1)"
2639
2370
  );
2640
2371
  registerAuthCommands(program);
2641
- registerContextCommands(program);
2642
- registerSpecCommands(program);
2643
- registerProjectCommands(program);
2644
2372
  registerHooksCommands(program);
2645
2373
  registerSkillCommands(program);
2646
2374
  registerMovesCommands(program);
@@ -2651,6 +2379,7 @@ registerCodexCommands(program);
2651
2379
  registerDaemonCommands(program);
2652
2380
  registerReconcileCommands(program);
2653
2381
  registerStatuslineCommands(program);
2382
+ registerWelcomeCommand(program);
2654
2383
  process.on("unhandledRejection", (err) => {
2655
2384
  const msg = err instanceof Error ? err.message : String(err);
2656
2385
  console.error(msg);