@primitive.ai/prim 0.1.0-alpha.22 → 0.1.0-alpha.24
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/README.md +38 -6
- package/SKILL.md +20 -1
- package/dist/index.js +258 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,15 @@ npx @primitive.ai/prim
|
|
|
24
24
|
|
|
25
25
|
## Quick Start
|
|
26
26
|
|
|
27
|
+
One command does the whole install — auth, session hooks, daemon, git hooks,
|
|
28
|
+
skill, and the welcome:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
prim setup # add --agent codex for Codex, --no-daemon to skip the daemon
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or run the steps individually:
|
|
35
|
+
|
|
27
36
|
```bash
|
|
28
37
|
# 1. Authenticate via browser (WorkOS OAuth)
|
|
29
38
|
prim auth login
|
|
@@ -38,10 +47,26 @@ prim daemon start
|
|
|
38
47
|
prim hooks install
|
|
39
48
|
```
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
`prim claude install` also writes a scoped `Bash(npx --yes @primitive.ai/prim@latest:*)`
|
|
51
|
+
allow-rule into `.claude/settings.json`, so an agent's prim calls don't stall on a
|
|
52
|
+
permission prompt.
|
|
53
|
+
|
|
54
|
+
An AI coding agent can drive the setup itself — see [`setup.md`](./setup.md).
|
|
42
55
|
|
|
43
56
|
## Commands
|
|
44
57
|
|
|
58
|
+
### Setup
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
prim setup # Run the whole install in one shot
|
|
62
|
+
prim setup --agent codex # Same, for OpenAI Codex
|
|
63
|
+
prim setup --no-daemon # Skip the companion daemon
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Orchestrates auth → session hooks → daemon → git hooks → skill → welcome,
|
|
67
|
+
re-running each underlying command so every step behaves exactly as if run by
|
|
68
|
+
hand (including the browser login). Idempotent — safe to re-run.
|
|
69
|
+
|
|
45
70
|
### Auth
|
|
46
71
|
|
|
47
72
|
```bash
|
|
@@ -83,16 +108,23 @@ prim daemon start # start (stop / restart / status)
|
|
|
83
108
|
Read and respond to the decision graph.
|
|
84
109
|
|
|
85
110
|
```bash
|
|
86
|
-
prim decisions recent
|
|
87
|
-
prim decisions show <id>
|
|
88
|
-
prim decisions cascade <id>
|
|
89
|
-
prim decisions check --files <…>
|
|
90
|
-
prim decisions confirm <id>
|
|
111
|
+
prim decisions recent # Recent decisions feed
|
|
112
|
+
prim decisions show <id> # Drill into one decision
|
|
113
|
+
prim decisions cascade <id> # Blast radius of a decision
|
|
114
|
+
prim decisions check --files <…> # Active decisions referencing files (warn-only)
|
|
115
|
+
prim decisions confirm <id> # Answer a rationale-confirmation prompt
|
|
116
|
+
prim decisions create --intent <…> # Author a decision directly (flags-only)
|
|
117
|
+
prim decisions link <child> --on <parent> # Relate: <child> depends on <parent>
|
|
118
|
+
prim decisions unlink <child> --on <parent> # Remove that dependency
|
|
91
119
|
```
|
|
92
120
|
|
|
93
121
|
`<id>` accepts a full decision ID or its short ID. STDOUT is machine-readable
|
|
94
122
|
JSON; human-readable status goes to STDERR.
|
|
95
123
|
|
|
124
|
+
`link` / `unlink` curate the dependency edges the automatic linker would otherwise
|
|
125
|
+
own — `<child>` depends on `<parent>`. Both are idempotent and refuse any link that
|
|
126
|
+
would create a cycle (exit 2); an unresolved id exits 4.
|
|
127
|
+
|
|
96
128
|
### Reconcile
|
|
97
129
|
|
|
98
130
|
```bash
|
package/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ description: Use the prim CLI for Primitive's decision graph — passive decisio
|
|
|
9
9
|
|
|
10
10
|
## Mental model
|
|
11
11
|
|
|
12
|
-
As your team codes, prim passively captures the **decisions** you make -- which library, which pattern, which config value -- into a queryable graph, and links them: a decision can depend on earlier decisions and reference the files it touched. When a later change conflicts with a load-bearing prior decision, prim **gates** the edit and surfaces the decision for review.
|
|
12
|
+
As your team codes, prim passively captures the **decisions** you make -- which library, which pattern, which config value -- into a queryable graph, and links them: a decision can depend on earlier decisions (auto-linked from shared files, or related by hand — see *Relate decisions*) and reference the files it touched. When a later change conflicts with a load-bearing prior decision, prim **gates** the edit and surfaces the decision for review.
|
|
13
13
|
|
|
14
14
|
You never invoke capture. It runs automatically through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code) or `npx --yes @primitive.ai/prim codex install` (Codex). Your job is to **respond** to the gate, **read** the graph before load-bearing edits, and **answer** the occasional rationale confirmation.
|
|
15
15
|
|
|
@@ -77,6 +77,25 @@ npx --yes @primitive.ai/prim decisions create --intent "Adopt prosemirror-collab
|
|
|
77
77
|
|
|
78
78
|
Only `--intent` is required. Optional: `--kind` (change|exploration|task_execution|unclear, default change), `--rationale`, `--area`, `--decided`, `--alternatives` (comma-separated), `--confidence` (high|medium|low, default high), `--reversibility` (high|low, default high), and `--files` (comma-separated repo-relative paths the decision governs — pass these to make the conflict gate fire on later edits to those files, same path form as `decisions check`). STDOUT is the created identity `{ decisionId, shortId, createdAt }`; STDERR prints `[prim] created dec_<short>.` — pass that `dec_<short>` straight into `decisions show` / `cascade` / `confirm`. Author on the user's behalf only when they ask for a decision to be recorded; don't narrate your own routine edits into the graph (the hooks already do that).
|
|
79
79
|
|
|
80
|
+
## Relate decisions (link / unlink)
|
|
81
|
+
|
|
82
|
+
prim links decisions automatically when their files overlap, but that heuristic misses real connections and occasionally invents wrong ones. When the user asks you to **relate two existing decisions** — "B depends on A", "these are connected", wiring up two orphans — or to **cut a wrong link**, do it by hand:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
npx --yes @primitive.ai/prim decisions link <child> --on <parent> # record that <child> depends on <parent>
|
|
86
|
+
npx --yes @primitive.ai/prim decisions unlink <child> --on <parent> # remove that dependency
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Direction is **`<child>` depends on `<parent>`** — the parent is the prerequisite. Read the echoed verdict to confirm you got the arrow right: `[prim] <child> now depends on <parent>.` After linking, `decisions show <child>` lists `<parent>` upstream and `decisions cascade <parent>` shows `<child>` in its downstream blast radius; after unlinking they drop. Both ids accept `dec_<short>` or a full id and may be any two decisions in your org, regardless of status.
|
|
90
|
+
|
|
91
|
+
Safe to run repeatedly:
|
|
92
|
+
|
|
93
|
+
- **Idempotent** — re-linking an existing edge (or unlinking a missing one) is a no-op that still exits 0 (`already_linked` / `not_linked`).
|
|
94
|
+
- **Acyclic** — a self-loop, or any link that would close a dependency cycle, is refused with exit 2 (with the offending chain when it's short enough to render); the graph stays a DAG.
|
|
95
|
+
- **Exit codes** (treat non-zero as actionable): `0` success or no-op; `2` a refused link (self-loop, cycle, or an ambiguous short id — retry with the full id); `4` an id that doesn't resolve. After a non-zero exit, branch on the exit code and the `[prim]` STDERR verdict, **not** on STDOUT keys: only the exit-0 outcomes carry the full `{ outcome, childId, childShortId, parentId, parentShortId }`; a refused link prints a smaller `{ outcome, … }`, and an unresolved id (exit 4) prints nothing to STDOUT.
|
|
96
|
+
|
|
97
|
+
Like authoring, relate only what the user asks for — don't invent relationships they didn't state.
|
|
98
|
+
|
|
80
99
|
## Presence
|
|
81
100
|
|
|
82
101
|
With the daemon running (`npx --yes @primitive.ai/prim daemon start`), `npx --yes @primitive.ai/prim daemon status` includes the live online count in its STDOUT JSON (when presence is fresh); Claude Code surfaces it in the statusline as `team: N online`. Your captured decisions are attributed to your agent automatically -- no flag required.
|
package/dist/index.js
CHANGED
|
@@ -401,6 +401,7 @@ var REGISTRATIONS = [
|
|
|
401
401
|
makeRegistration("SessionStart", "*", SESSION_START_BIN),
|
|
402
402
|
makeRegistration("SessionEnd", "*", SESSION_END_BIN)
|
|
403
403
|
];
|
|
404
|
+
var PRIM_PERMISSION_RULE = "Bash(npx --yes @primitive.ai/prim@latest:*)";
|
|
404
405
|
function settingsPathFor(scope) {
|
|
405
406
|
return scope === "user" ? USER_SCOPE_PATH : projectScopePath();
|
|
406
407
|
}
|
|
@@ -463,12 +464,36 @@ function applyStatusLine(settings) {
|
|
|
463
464
|
}
|
|
464
465
|
};
|
|
465
466
|
}
|
|
467
|
+
function applyPermissions(settings) {
|
|
468
|
+
const permissions = settings.permissions ?? {};
|
|
469
|
+
const allow = permissions.allow ?? [];
|
|
470
|
+
if (allow.includes(PRIM_PERMISSION_RULE)) {
|
|
471
|
+
return settings;
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
...settings,
|
|
475
|
+
permissions: { ...permissions, allow: [...allow, PRIM_PERMISSION_RULE] }
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function removePrimPermission(settings) {
|
|
479
|
+
const permissions = settings.permissions;
|
|
480
|
+
if (!permissions?.allow?.includes(PRIM_PERMISSION_RULE)) {
|
|
481
|
+
return settings;
|
|
482
|
+
}
|
|
483
|
+
const allow = permissions.allow.filter((rule) => rule !== PRIM_PERMISSION_RULE);
|
|
484
|
+
const nextPermissions = {
|
|
485
|
+
...permissions,
|
|
486
|
+
allow: allow.length > 0 ? allow : void 0
|
|
487
|
+
};
|
|
488
|
+
const hasOtherPerms = Object.values(nextPermissions).some((v) => v !== void 0);
|
|
489
|
+
return { ...settings, permissions: hasOtherPerms ? nextPermissions : void 0 };
|
|
490
|
+
}
|
|
466
491
|
function applyInstall(settings, options = {}) {
|
|
467
492
|
const hooks = { ...settings.hooks ?? {} };
|
|
468
493
|
for (const reg of REGISTRATIONS) {
|
|
469
494
|
hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
|
|
470
495
|
}
|
|
471
|
-
return applyStatusLine({ ...settings, hooks });
|
|
496
|
+
return applyPermissions(applyStatusLine({ ...settings, hooks }));
|
|
472
497
|
}
|
|
473
498
|
function applyUninstall(settings) {
|
|
474
499
|
const source = settings.hooks ?? {};
|
|
@@ -486,7 +511,7 @@ function applyUninstall(settings) {
|
|
|
486
511
|
if (isPrimStatusLine(next)) {
|
|
487
512
|
next.statusLine = void 0;
|
|
488
513
|
}
|
|
489
|
-
return next;
|
|
514
|
+
return removePrimPermission(next);
|
|
490
515
|
}
|
|
491
516
|
function captureInstalled(settings) {
|
|
492
517
|
return CAPTURE_EVENTS.some(
|
|
@@ -1228,6 +1253,9 @@ async function fetchRecent(args, deps = defaultDeps2) {
|
|
|
1228
1253
|
RECENT_TIMEOUT_MS
|
|
1229
1254
|
);
|
|
1230
1255
|
const result = { decisions: res.decisions };
|
|
1256
|
+
if (res.viewerHasDecisions !== void 0) {
|
|
1257
|
+
result.viewerHasDecisions = res.viewerHasDecisions;
|
|
1258
|
+
}
|
|
1231
1259
|
if (res.unavailable !== void 0) {
|
|
1232
1260
|
result.unavailable = res.unavailable;
|
|
1233
1261
|
}
|
|
@@ -1414,8 +1442,85 @@ function formatCreateJson(outcome) {
|
|
|
1414
1442
|
return JSON.stringify(outcome, null, 2);
|
|
1415
1443
|
}
|
|
1416
1444
|
|
|
1417
|
-
// src/decisions/
|
|
1445
|
+
// src/decisions/link.ts
|
|
1446
|
+
var RELATE_TIMEOUT_MS = 1e4;
|
|
1447
|
+
var defaultDeps5 = { getClient };
|
|
1418
1448
|
var NOT_FOUND_RE3 = /not found/i;
|
|
1449
|
+
var AMBIGUOUS_RE2 = /ambiguous/i;
|
|
1450
|
+
var CYCLE_RE = /cycle/i;
|
|
1451
|
+
var LinkNotFoundError = class extends Error {
|
|
1452
|
+
constructor(which) {
|
|
1453
|
+
super(`Decision not found: ${which}`);
|
|
1454
|
+
this.name = "LinkNotFoundError";
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
function isRelateRejection(outcome) {
|
|
1458
|
+
return outcome.outcome === "self_loop" || outcome.outcome === "would_cycle" || outcome.outcome === "ambiguous";
|
|
1459
|
+
}
|
|
1460
|
+
function foldRelateError(err) {
|
|
1461
|
+
if (err instanceof Error) {
|
|
1462
|
+
if (NOT_FOUND_RE3.test(err.message)) {
|
|
1463
|
+
throw new LinkNotFoundError(err.message.includes("parent") ? "parent" : "child");
|
|
1464
|
+
}
|
|
1465
|
+
if (AMBIGUOUS_RE2.test(err.message)) {
|
|
1466
|
+
return { outcome: "ambiguous", which: err.message.includes("(parent)") ? "parent" : "child" };
|
|
1467
|
+
}
|
|
1468
|
+
if (CYCLE_RE.test(err.message)) {
|
|
1469
|
+
return { outcome: "would_cycle", detail: err.message };
|
|
1470
|
+
}
|
|
1471
|
+
if (/itself/i.test(err.message)) {
|
|
1472
|
+
return { outcome: "self_loop" };
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
throw err;
|
|
1476
|
+
}
|
|
1477
|
+
async function relate(path, request, deps) {
|
|
1478
|
+
const client = deps.getClient();
|
|
1479
|
+
try {
|
|
1480
|
+
const outcome = await client.post(path, request, {
|
|
1481
|
+
signal: AbortSignal.timeout(RELATE_TIMEOUT_MS)
|
|
1482
|
+
});
|
|
1483
|
+
return { request, outcome };
|
|
1484
|
+
} catch (err) {
|
|
1485
|
+
return { request, outcome: foldRelateError(err) };
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function fetchLink(child, parent, deps = defaultDeps5) {
|
|
1489
|
+
return relate("/api/cli/decisions/link", { child, parent }, deps);
|
|
1490
|
+
}
|
|
1491
|
+
function fetchUnlink(child, parent, deps = defaultDeps5) {
|
|
1492
|
+
return relate("/api/cli/decisions/unlink", { child, parent }, deps);
|
|
1493
|
+
}
|
|
1494
|
+
function endpointRef(outcome, side) {
|
|
1495
|
+
return side === "child" ? renderIdentifier({ shortId: outcome.childShortId, id: outcome.childId }) : renderIdentifier({ shortId: outcome.parentShortId, id: outcome.parentId });
|
|
1496
|
+
}
|
|
1497
|
+
function formatRelateHuman(result) {
|
|
1498
|
+
const { request, outcome } = result;
|
|
1499
|
+
switch (outcome.outcome) {
|
|
1500
|
+
case "linked":
|
|
1501
|
+
return `[prim] ${endpointRef(outcome, "child")} now depends on ${endpointRef(outcome, "parent")}.`;
|
|
1502
|
+
case "already_linked":
|
|
1503
|
+
return `[prim] ${endpointRef(outcome, "child")} already depends on ${endpointRef(outcome, "parent")}; nothing to change.`;
|
|
1504
|
+
case "unlinked":
|
|
1505
|
+
return `[prim] ${endpointRef(outcome, "child")} no longer depends on ${endpointRef(outcome, "parent")}.`;
|
|
1506
|
+
case "not_linked":
|
|
1507
|
+
return `[prim] ${endpointRef(outcome, "child")} did not depend on ${endpointRef(outcome, "parent")}; nothing to change.`;
|
|
1508
|
+
case "self_loop":
|
|
1509
|
+
return "[prim] a decision cannot depend on itself.";
|
|
1510
|
+
case "would_cycle":
|
|
1511
|
+
return `[prim] refusing to link \u2014 ${outcome.detail}.`;
|
|
1512
|
+
default: {
|
|
1513
|
+
const typed = outcome.which === "parent" ? request.parent : request.child;
|
|
1514
|
+
return `[prim] the ${outcome.which} id "${typed}" is ambiguous in this organization \u2014 retry with the full decision id.`;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
function formatRelateJson(result) {
|
|
1519
|
+
return JSON.stringify(result.outcome, null, 2);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/decisions/show.ts
|
|
1523
|
+
var NOT_FOUND_RE4 = /not found/i;
|
|
1419
1524
|
function colorStatus(status) {
|
|
1420
1525
|
if (status === "under_review") {
|
|
1421
1526
|
return color(status, "orange");
|
|
@@ -1426,14 +1531,14 @@ function colorStatus(status) {
|
|
|
1426
1531
|
return color(status, "gray");
|
|
1427
1532
|
}
|
|
1428
1533
|
var SHOW_TIMEOUT_MS = 1e4;
|
|
1429
|
-
var
|
|
1534
|
+
var defaultDeps6 = { getClient };
|
|
1430
1535
|
var DecisionNotFoundError = class extends Error {
|
|
1431
1536
|
constructor(idOrShortId) {
|
|
1432
1537
|
super(`Decision not found: ${idOrShortId}`);
|
|
1433
1538
|
this.name = "DecisionNotFoundError";
|
|
1434
1539
|
}
|
|
1435
1540
|
};
|
|
1436
|
-
async function fetchShow(idOrShortId, deps =
|
|
1541
|
+
async function fetchShow(idOrShortId, deps = defaultDeps6) {
|
|
1437
1542
|
const params = new URLSearchParams({ id: idOrShortId });
|
|
1438
1543
|
const client = deps.getClient();
|
|
1439
1544
|
try {
|
|
@@ -1444,7 +1549,7 @@ async function fetchShow(idOrShortId, deps = defaultDeps5) {
|
|
|
1444
1549
|
SHOW_TIMEOUT_MS
|
|
1445
1550
|
);
|
|
1446
1551
|
} catch (err) {
|
|
1447
|
-
if (err instanceof Error &&
|
|
1552
|
+
if (err instanceof Error && NOT_FOUND_RE4.test(err.message)) {
|
|
1448
1553
|
throw new DecisionNotFoundError(idOrShortId);
|
|
1449
1554
|
}
|
|
1450
1555
|
throw err;
|
|
@@ -1654,6 +1759,40 @@ function registerDecisionsCommands(program2) {
|
|
|
1654
1759
|
throw err;
|
|
1655
1760
|
}
|
|
1656
1761
|
});
|
|
1762
|
+
decisions.command("link <child>").description("Record that <child> depends on <parent> (adds a dependency edge)").requiredOption("--on <parent>", "The decision <child> depends on").action(async (child, opts) => {
|
|
1763
|
+
try {
|
|
1764
|
+
const result = await fetchLink(child, opts.on);
|
|
1765
|
+
console.error(formatRelateHuman(result));
|
|
1766
|
+
console.log(formatRelateJson(result));
|
|
1767
|
+
if (isRelateRejection(result.outcome)) {
|
|
1768
|
+
process.exitCode = EXIT_USAGE;
|
|
1769
|
+
}
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
if (err instanceof LinkNotFoundError) {
|
|
1772
|
+
console.error(`[prim] ${err.message}`);
|
|
1773
|
+
process.exitCode = EXIT_NOT_FOUND;
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
throw err;
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
decisions.command("unlink <child>").description("Remove <child>'s recorded dependency on <parent>").requiredOption("--on <parent>", "The decision <child> no longer depends on").action(async (child, opts) => {
|
|
1780
|
+
try {
|
|
1781
|
+
const result = await fetchUnlink(child, opts.on);
|
|
1782
|
+
console.error(formatRelateHuman(result));
|
|
1783
|
+
console.log(formatRelateJson(result));
|
|
1784
|
+
if (isRelateRejection(result.outcome)) {
|
|
1785
|
+
process.exitCode = EXIT_USAGE;
|
|
1786
|
+
}
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
if (err instanceof LinkNotFoundError) {
|
|
1789
|
+
console.error(`[prim] ${err.message}`);
|
|
1790
|
+
process.exitCode = EXIT_NOT_FOUND;
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
throw err;
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1657
1796
|
}
|
|
1658
1797
|
|
|
1659
1798
|
// src/commands/hooks.ts
|
|
@@ -2117,6 +2256,82 @@ function registerSessionCommands(program2) {
|
|
|
2117
2256
|
});
|
|
2118
2257
|
}
|
|
2119
2258
|
|
|
2259
|
+
// src/commands/setup.ts
|
|
2260
|
+
import { spawnSync } from "child_process";
|
|
2261
|
+
var EXIT_INCOMPLETE = 1;
|
|
2262
|
+
function planSetupSteps(opts) {
|
|
2263
|
+
const scopeArgs = opts.scope === "user" ? ["--scope", "user"] : [];
|
|
2264
|
+
const steps = [
|
|
2265
|
+
{
|
|
2266
|
+
key: "session",
|
|
2267
|
+
label: opts.agent === "codex" ? "Codex integration" : "Claude Code integration",
|
|
2268
|
+
args: [opts.agent, "install", ...scopeArgs],
|
|
2269
|
+
required: true
|
|
2270
|
+
}
|
|
2271
|
+
];
|
|
2272
|
+
if (opts.daemon) {
|
|
2273
|
+
steps.push({
|
|
2274
|
+
key: "daemon",
|
|
2275
|
+
label: "Companion daemon",
|
|
2276
|
+
args: ["daemon", "start"],
|
|
2277
|
+
required: false
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
steps.push({ key: "hooks", label: "Git hooks", args: ["hooks", "install"], required: true });
|
|
2281
|
+
steps.push({ key: "skill", label: "Agent skill", args: ["skill", "install"], required: true });
|
|
2282
|
+
return steps;
|
|
2283
|
+
}
|
|
2284
|
+
function registerSetupCommand(program2) {
|
|
2285
|
+
program2.command("setup").description(
|
|
2286
|
+
"Install everything in one shot (auth, session + git hooks, daemon, skill, welcome)"
|
|
2287
|
+
).option("--agent <agent>", "claude or codex", "claude").option("--scope <scope>", "project or user (session integration)", "project").option("--no-daemon", "skip starting the companion daemon").action((opts) => {
|
|
2288
|
+
const agent = opts.agent === "codex" ? "codex" : "claude";
|
|
2289
|
+
const scope = opts.scope === "user" ? "user" : "project";
|
|
2290
|
+
const self = process.argv[1];
|
|
2291
|
+
const run = (args, capture = false) => {
|
|
2292
|
+
const r = spawnSync(process.execPath, [self, ...args], {
|
|
2293
|
+
stdio: capture ? ["inherit", "pipe", "inherit"] : "inherit",
|
|
2294
|
+
encoding: "utf-8"
|
|
2295
|
+
});
|
|
2296
|
+
return { code: r.status ?? 1, stdout: capture ? r.stdout ?? "" : "" };
|
|
2297
|
+
};
|
|
2298
|
+
const results = {};
|
|
2299
|
+
const note = (msg) => {
|
|
2300
|
+
process.stderr.write(`[prim] ${msg}
|
|
2301
|
+
`);
|
|
2302
|
+
};
|
|
2303
|
+
const isAuthed = (json) => {
|
|
2304
|
+
try {
|
|
2305
|
+
return JSON.parse(json || "{}").authenticated === true;
|
|
2306
|
+
} catch {
|
|
2307
|
+
return false;
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
if (isAuthed(run(["auth", "status", "--json"], true).stdout)) {
|
|
2311
|
+
note("auth \xB7 already authenticated");
|
|
2312
|
+
results.auth = "ok";
|
|
2313
|
+
} else {
|
|
2314
|
+
note("auth \xB7 opening browser to authenticate\u2026");
|
|
2315
|
+
run(["auth", "login"]);
|
|
2316
|
+
results.auth = isAuthed(run(["auth", "status", "--json"], true).stdout) ? "ok" : "failed";
|
|
2317
|
+
}
|
|
2318
|
+
for (const step of planSetupSteps({ agent, daemon: opts.daemon, scope })) {
|
|
2319
|
+
note(`${step.label} \xB7 installing\u2026`);
|
|
2320
|
+
const { code } = run(step.args);
|
|
2321
|
+
results[step.key] = code === 0 ? "ok" : step.required ? "failed" : "skipped";
|
|
2322
|
+
}
|
|
2323
|
+
note("welcome");
|
|
2324
|
+
run(["welcome"]);
|
|
2325
|
+
results.welcome = "ok";
|
|
2326
|
+
const failed = Object.entries(results).filter(([, v]) => v === "failed").map(([k]) => k);
|
|
2327
|
+
const trail = Object.entries(results).map(([k, v]) => `${k}:${v}`).join(" \xB7 ");
|
|
2328
|
+
note(
|
|
2329
|
+
`setup ${failed.length === 0 ? "complete" : `incomplete (failed: ${failed.join(", ")})`} \u2014 ${trail}`
|
|
2330
|
+
);
|
|
2331
|
+
process.exit(failed.length === 0 ? 0 : EXIT_INCOMPLETE);
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2120
2335
|
// src/commands/skill.ts
|
|
2121
2336
|
import {
|
|
2122
2337
|
closeSync as closeSync2,
|
|
@@ -2347,14 +2562,32 @@ var REVERSE_PROMPT_LINES = [
|
|
|
2347
2562
|
"to focus on those goals?"
|
|
2348
2563
|
];
|
|
2349
2564
|
var REVERSE_PROMPT = REVERSE_PROMPT_LINES.join(" ");
|
|
2565
|
+
var CALLOUT_TITLE = "Your turn";
|
|
2566
|
+
var CALLOUT_INDENT = " ";
|
|
2567
|
+
function ruledQuestion(lines) {
|
|
2568
|
+
const prefix = `\u250C\u2500 ${CALLOUT_TITLE} `;
|
|
2569
|
+
const width = Math.max(
|
|
2570
|
+
`${prefix}\u2510`.length,
|
|
2571
|
+
...lines.map((line) => CALLOUT_INDENT.length + line.length + 1)
|
|
2572
|
+
);
|
|
2573
|
+
const top = `${prefix}${"\u2500".repeat(width - prefix.length - 1)}\u2510`;
|
|
2574
|
+
const bottom = `\u2514${"\u2500".repeat(top.length - 2)}\u2518`;
|
|
2575
|
+
return [
|
|
2576
|
+
color(top, "green"),
|
|
2577
|
+
...lines.map((line) => `${CALLOUT_INDENT}${line}`),
|
|
2578
|
+
color(bottom, "green")
|
|
2579
|
+
];
|
|
2580
|
+
}
|
|
2350
2581
|
function welcomeStateFromRecent(result) {
|
|
2351
2582
|
if (result.unavailable !== void 0) {
|
|
2352
2583
|
return { org: "unknown" };
|
|
2353
2584
|
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2585
|
+
const recent = result.decisions.slice(0, RECENT_LIMIT);
|
|
2586
|
+
const viewerHasDecisions = result.viewerHasDecisions ?? result.decisions.length > 0;
|
|
2587
|
+
if (!viewerHasDecisions) {
|
|
2588
|
+
return { org: "seed", recent };
|
|
2356
2589
|
}
|
|
2357
|
-
return { org: "active", recent
|
|
2590
|
+
return { org: "active", recent };
|
|
2358
2591
|
}
|
|
2359
2592
|
function formatWelcome(state) {
|
|
2360
2593
|
const cmd = (command, desc) => ` ${dim(command.padEnd(CMD_GUTTER))}${desc}`;
|
|
@@ -2385,14 +2618,15 @@ function formatWelcome(state) {
|
|
|
2385
2618
|
cmd("prim decisions check --files <files>", "what governs files you're about to change"),
|
|
2386
2619
|
cmd("prim --help", "everything else")
|
|
2387
2620
|
];
|
|
2388
|
-
} else if (state.org === "
|
|
2621
|
+
} else if (state.org === "seed") {
|
|
2622
|
+
const teamContext = state.recent.length > 0 ? [bold("Recent team decisions"), ...state.recent.map(formatRecentRow), ""] : [];
|
|
2389
2623
|
body = [
|
|
2624
|
+
...teamContext,
|
|
2390
2625
|
bold("Let's seed your decision graph"),
|
|
2391
|
-
"
|
|
2392
|
-
"",
|
|
2393
|
-
...REVERSE_PROMPT_LINES.map((line) => ` ${line}`),
|
|
2626
|
+
"You haven't recorded a decision yet \u2014 answer this and I'll record",
|
|
2627
|
+
"each goal as a decision:",
|
|
2394
2628
|
"",
|
|
2395
|
-
|
|
2629
|
+
...ruledQuestion(REVERSE_PROMPT_LINES)
|
|
2396
2630
|
];
|
|
2397
2631
|
} else {
|
|
2398
2632
|
body = [
|
|
@@ -2402,14 +2636,20 @@ function formatWelcome(state) {
|
|
|
2402
2636
|
cmd("prim --help", "everything else")
|
|
2403
2637
|
];
|
|
2404
2638
|
}
|
|
2405
|
-
|
|
2639
|
+
const footer = state.org === "seed" ? [] : ["", dim("App: https://app.getprimitive.ai")];
|
|
2640
|
+
return [...head, ...body, ...footer].join("\n");
|
|
2406
2641
|
}
|
|
2407
2642
|
function welcomeJson(state) {
|
|
2408
2643
|
if (state.org === "active") {
|
|
2409
2644
|
return { welcomed: true, org: "active", recent: state.recent };
|
|
2410
2645
|
}
|
|
2411
|
-
if (state.org === "
|
|
2412
|
-
return {
|
|
2646
|
+
if (state.org === "seed") {
|
|
2647
|
+
return {
|
|
2648
|
+
welcomed: true,
|
|
2649
|
+
org: "seed",
|
|
2650
|
+
reversePrompt: REVERSE_PROMPT,
|
|
2651
|
+
recent: state.recent
|
|
2652
|
+
};
|
|
2413
2653
|
}
|
|
2414
2654
|
return { welcomed: true, org: "unknown" };
|
|
2415
2655
|
}
|
|
@@ -2444,6 +2684,7 @@ registerDaemonCommands(program);
|
|
|
2444
2684
|
registerReconcileCommands(program);
|
|
2445
2685
|
registerStatuslineCommands(program);
|
|
2446
2686
|
registerWelcomeCommand(program);
|
|
2687
|
+
registerSetupCommand(program);
|
|
2447
2688
|
process.on("unhandledRejection", (err) => {
|
|
2448
2689
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2449
2690
|
console.error(msg);
|
package/package.json
CHANGED