@primitive.ai/prim 0.1.0-alpha.11 → 0.1.0-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/SKILL.md +132 -0
  2. package/dist/index.js +160 -9
  3. package/package.json +8 -3
package/SKILL.md ADDED
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: prim
3
+ description: Use the prim CLI for managing Primitive specs, contexts, projects, and pre-commit hooks. TRIGGER when the user mentions Primitive, prim, "specs" (in the Primitive sense), or "contexts" (in the Primitive sense); when the repo's package.json depends on @primitive.ai/prim; when the user asks to sync, map, update, or auto-map a spec; when configuring Primitive pre-commit hooks. SKIP when "spec" means test specs (vitest, jest, rspec), when "context" means React context or LLM context window, or for unrelated CLIs.
4
+ ---
5
+
6
+ # Working with the prim CLI
7
+
8
+ `prim` is the official CLI for [Primitive](https://app.getprimitive.ai). Use it -- don't reach for shell or curl.
9
+
10
+ ## Mental model
11
+
12
+ A **spec** captures intent for execution -- it defines what should be done, usually so other agents (or humans) can act on it. A **context** is everything else: supporting material that informs but doesn't define the work -- design docs, references, prior art, shared documentation, examples. When deciding which to create, ask: does this say *what to do*, or does it *inform* whoever's doing it? A project has at most one spec but can link many contexts.
13
+
14
+ In Primitive, a markdown spec is associated with a **project**. The spec is the source of truth: `npx --yes @primitive.ai/prim spec sync` parses the spec, diffs it against the project, and **applies the diff** -- adding, updating, or **archiving** items in the project to match. Items removed from a spec are soft-archived (recoverable via the dashboard), not deleted -- but they leave the active view, so flag the user before large spec rewrites on projects with work in flight.
15
+
16
+ A **spec is a kind of context** -- same IDs, same storage. The `npx --yes @primitive.ai/prim spec ...` commands are a focused view onto specs; `npx --yes @primitive.ai/prim context get <id>` works on a spec ID and vice versa. For structured metadata on a spec (review status, root project, sync version, scope, file patterns), use `npx --yes @primitive.ai/prim context get <specId>` -- it returns JSON.
17
+
18
+ `npx --yes @primitive.ai/prim spec list` returns only spec-type contexts. `npx --yes @primitive.ai/prim context list` returns all contexts regardless of type.
19
+
20
+ ## Auth
21
+
22
+ Run `npx --yes @primitive.ai/prim auth status` first. It exits **0 if authenticated, 1 if not** -- branch on the exit code, don't parse the message.
23
+
24
+ Three ways to authenticate, in priority order:
25
+
26
+ 1. **`PRIM_TOKEN` environment variable** -- preferred for agents and CI. Set it before invoking prim and you're done; no interactive flow, no token files.
27
+ 2. **`npx --yes @primitive.ai/prim auth set-token <token>`** -- saves a bearer token to `~/.config/prim/token`. Use when the user has a long-lived token in hand.
28
+ 3. **`npx --yes @primitive.ai/prim auth login`** -- opens a browser via WorkOS OAuth. **An agent cannot complete this.** If `auth status` exits non-zero and `PRIM_TOKEN` is unset, **stop and ask the user** to run `npx --yes @primitive.ai/prim auth login` themselves.
29
+
30
+ The CLI auto-refreshes expired tokens. On unrecoverable expiry it throws `Authentication expired. Run prim auth login to re-authenticate.` -- relay it.
31
+
32
+ ## Ground rules
33
+
34
+ 1. Don't guess IDs. Discover them with `npx --yes @primitive.ai/prim spec list`, `npx --yes @primitive.ai/prim spec list --project-id <pid>`, or `npx --yes @primitive.ai/prim context list`.
35
+ 2. Every command accepts `--help`. When unsure of flags, run `npx --yes @primitive.ai/prim <cmd> --help` rather than guessing.
36
+ 3. The CLI prints API errors as one-liners to stderr and exits non-zero. Treat any non-zero exit as actionable. If a command fails with an unrecognized error, re-run with `--help` to check your flags. If auth-related, re-check `auth status`.
37
+
38
+ ## Common workflows
39
+
40
+ ### Read a spec's current text (do this before any partial edit)
41
+ ```
42
+ npx --yes @primitive.ai/prim spec get <id> --text-only > spec.md
43
+ ```
44
+ `npx --yes @primitive.ai/prim spec update <id> --file <path>` replaces the entire body. Fetch first if you're only changing part of it.
45
+
46
+ ### Update a spec from a local file and apply to the project
47
+ ```
48
+ npx --yes @primitive.ai/prim spec list --project-id <pid> # find the spec for a project
49
+ npx --yes @primitive.ai/prim spec update <id> --file spec.md # replaces spec body
50
+ npx --yes @primitive.ai/prim spec sync <id> # required -- update doesn't apply changes to the project
51
+ ```
52
+ `npx --yes @primitive.ai/prim spec sync` is **async**: it returns immediately with `Triggered sync for spec`, then applies in the background. The project isn't updated when the command returns -- surface that to the user.
53
+
54
+ Auto-map runs automatically on the server after every `spec update`. Call `npx --yes @primitive.ai/prim spec auto-map <id>` explicitly only to re-run mapping without changing the spec text.
55
+
56
+ ### Map files to a spec (so pre-commit auto-syncs all affected specs)
57
+ ```
58
+ npx --yes @primitive.ai/prim spec map <id> -p "src/auth/**" "src/foo/**" # multiple patterns at once
59
+ npx --yes @primitive.ai/prim spec unmap <id> -p "src/auth/**" # remove one
60
+ npx --yes @primitive.ai/prim spec unmap <id> # clear all manual patterns
61
+ ```
62
+
63
+ ### Create or link a context
64
+ ```
65
+ npx --yes @primitive.ai/prim context create -s project -n "<name>" --file <path> --project-id <pid> # add --spec to make it a spec
66
+ npx --yes @primitive.ai/prim context create -s global -n "<name>" --text "..." # filed in the global context pane, not linked to a specific project
67
+ npx --yes @primitive.ai/prim context link <ctxId> --project <projectId> # works on any scope
68
+ npx --yes @primitive.ai/prim context unlink <ctxId> --project <projectId> # remove a link
69
+ ```
70
+
71
+ ### Update or delete a context
72
+ ```
73
+ npx --yes @primitive.ai/prim context update <id> -n "<new name>" # rename
74
+ npx --yes @primitive.ai/prim context update <id> --file <path> # replace body
75
+ npx --yes @primitive.ai/prim context delete <id> # permanent -- confirm with the user first
76
+ ```
77
+
78
+ ### Create a project (optionally with a linked spec)
79
+ ```
80
+ npx --yes @primitive.ai/prim project create -n "<name>" -d "<desc>"
81
+ npx --yes @primitive.ai/prim project create -n "<name>" --spec <contextId> # value is a context ID
82
+ ```
83
+
84
+ ### Install the pre-commit hook
85
+ ```
86
+ npx --yes @primitive.ai/prim hooks install # auto-detects Husky and prompts
87
+ npx --yes @primitive.ai/prim hooks uninstall
88
+ ```
89
+ **Note:** `hooks uninstall` only removes `.git/hooks/pre-commit`. If the hook was installed into `.husky/pre-commit`, you must remove the prim block from that file manually.
90
+
91
+ ## How the pre-commit hook behaves
92
+
93
+ `npx --yes @primitive.ai/prim hooks install` adds a hook that, on every commit:
94
+
95
+ 1. Fetches the org's spec-to-file-pattern mappings.
96
+ 2. Glob-matches staged files against each spec's patterns (`*` and `**` supported).
97
+ 3. For each affected spec, sends `git diff --cached` to `/api/cli/contexts/:id/sync-diff`. The backend runs an **LLM over (current spec + diff)** to produce edits, updates the spec text, then applies the new spec to the project.
98
+ 4. Prints `[synced] <id> -- <name>` or `[skip] <id> -- <reason>` per affected spec to stdout, and `[error]` lines to stderr.
99
+
100
+ What that means:
101
+
102
+ - **The hook is not `npx --yes @primitive.ai/prim spec sync`.** `npx --yes @primitive.ai/prim spec sync` re-applies the *existing* spec to the project. The hook calls `sync-diff` -- an LLM updates the spec from the code change, then applies the new spec to the project. The casual "just commit and the hook will sync" is ambiguous; when explaining to the user, specify which operation you mean.
103
+ - **The hook never blocks the commit.** Failures (auth, network, backend) print `[error]` to stderr but exit 0, so a successful `git commit` doesn't prove the spec changed. Check the hook's `[synced]` / `[error]` / `[skip]` output, or verify with `npx --yes @primitive.ai/prim spec get <id>`.
104
+ - **Diffs over 256 KiB are truncated.** The hook logs `(truncated: X KiB -> Y KiB analyzed)`. The LLM only sees the first 256 KiB of the diff.
105
+ - **To suppress the hook for one commit** (e.g., when intentionally desyncing code from spec, or when committing unrelated changes), use `git commit --no-verify`.
106
+
107
+ ## Output formats
108
+
109
+ | Command | Output | Where the ID is |
110
+ |---|---|---|
111
+ | `npx --yes @primitive.ai/prim context create` | `Created context: <id>` | Match `^Created context: (\S+)` |
112
+ | `npx --yes @primitive.ai/prim project create` | `Created project: <id>` | Match `^Created project: (\S+)` |
113
+ | `npx --yes @primitive.ai/prim spec update` | `Updated spec: <id>` | Match `^Updated spec: (\S+)` |
114
+ | `npx --yes @primitive.ai/prim spec sync` | `Triggered sync for spec: <id>` | Match `^Triggered sync for spec: (\S+)` |
115
+ | `npx --yes @primitive.ai/prim context list`, `npx --yes @primitive.ai/prim spec list` | Table with trailing count line | First token of each row (skip the final `N spec(s)` / `N context(s)` summary line) |
116
+ | `npx --yes @primitive.ai/prim spec list --project-id <pid>` | Single-spec block (key:value) | `ID:` line |
117
+ | `npx --yes @primitive.ai/prim context get <id>` | Pretty-printed JSON | `._id` field |
118
+ | `npx --yes @primitive.ai/prim spec get <id>` | Human-readable key:value block | `ID:` line |
119
+ | `npx --yes @primitive.ai/prim spec get <id> --text-only` | Raw spec markdown, nothing else | n/a |
120
+
121
+ ## Pitfalls
122
+
123
+ - **`npx --yes @primitive.ai/prim spec sync` archives anything dropped from the spec.** Removed content is archived (recoverable), not deleted.
124
+ - **`npx --yes @primitive.ai/prim spec update` doesn't apply changes to the project.** Always follow with `npx --yes @primitive.ai/prim spec sync <id>`.
125
+ - **`npx --yes @primitive.ai/prim spec update --file` replaces the whole body.** Fetch with `npx --yes @primitive.ai/prim spec get <id> --text-only` before any partial edit.
126
+ - **`npx --yes @primitive.ai/prim spec sync` rejects non-spec contexts** with "Context is not a spec document. Use `prim context` instead." Use `npx --yes @primitive.ai/prim spec list` to find spec IDs.
127
+ - **`npx --yes @primitive.ai/prim context delete` is permanent.** Confirm with the user before deleting.
128
+ - **Scope is set at creation.** To change it, delete and recreate the context.
129
+
130
+ ## After each task
131
+
132
+ Report the names and IDs you touched (spec, context, project) so the user can verify in the dashboard. If you ran `npx --yes @primitive.ai/prim spec sync`, remind the user it's async -- the project settles in the background.
package/dist/index.js CHANGED
@@ -11,10 +11,11 @@ import {
11
11
  } from "./chunk-3APLWTLB.js";
12
12
 
13
13
  // src/index.ts
14
- import { readFileSync as readFileSync5 } from "fs";
15
- import { dirname as dirname2, resolve as resolve2 } from "path";
16
- import { fileURLToPath } from "url";
14
+ import { readFileSync as readFileSync6 } from "fs";
15
+ import { dirname as dirname3, resolve as resolve3 } from "path";
16
+ import { fileURLToPath as fileURLToPath2 } from "url";
17
17
  import { Command } from "commander";
18
+ import updateNotifier from "update-notifier";
18
19
 
19
20
  // src/commands/auth.ts
20
21
  import { exec } from "child_process";
@@ -103,10 +104,10 @@ function registerAuthCommands(program2) {
103
104
  process.exit(1);
104
105
  });
105
106
  });
106
- const port = await new Promise((resolve3) => {
107
+ const port = await new Promise((resolve4) => {
107
108
  server.listen(CALLBACK_PORT, LOCALHOST, () => {
108
109
  const addr = server.address();
109
- resolve3(typeof addr === "object" && addr ? addr.port : 0);
110
+ resolve4(typeof addr === "object" && addr ? addr.port : 0);
110
111
  });
111
112
  });
112
113
  const redirectUri = `http://${LOCALHOST}:${port}/callback`;
@@ -473,8 +474,156 @@ function registerProjectCommands(program2) {
473
474
  });
474
475
  }
475
476
 
477
+ // src/commands/skill.ts
478
+ import {
479
+ closeSync,
480
+ existsSync as existsSync3,
481
+ fsyncSync,
482
+ openSync,
483
+ readFileSync as readFileSync4,
484
+ renameSync,
485
+ writeFileSync as writeFileSync3
486
+ } from "fs";
487
+ import { dirname as dirname2, resolve as resolve2 } from "path";
488
+ import { fileURLToPath } from "url";
489
+ import { createPatch } from "diff";
490
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
491
+ var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
492
+ var SKILL_END = "<!-- END PRIM SKILL v1 -->";
493
+ var TARGET_CANDIDATES = [
494
+ "CLAUDE.md",
495
+ ".cursor/rules",
496
+ ".windsurfrules",
497
+ ".github/instructions/primitive.md"
498
+ ];
499
+ var DEFAULT_TARGET = "CLAUDE.md";
500
+ function loadSkill() {
501
+ let dir = __dirname;
502
+ while (dir !== dirname2(dir)) {
503
+ const p = resolve2(dir, "SKILL.md");
504
+ if (existsSync3(p)) return readFileSync4(p, "utf-8");
505
+ dir = dirname2(dir);
506
+ }
507
+ throw new Error("SKILL.md not found in package");
508
+ }
509
+ function detectTargets(cwd) {
510
+ return TARGET_CANDIDATES.filter((p) => existsSync3(resolve2(cwd, p)));
511
+ }
512
+ function detectNewline(content) {
513
+ return content.includes("\r\n") ? "\r\n" : "\n";
514
+ }
515
+ function composeBlock(skill, eol) {
516
+ const body = skill.replace(/\r?\n/g, eol);
517
+ return `${SKILL_BEGIN}${eol}${body}${eol}${SKILL_END}`;
518
+ }
519
+ function applyBlock(existing, block, eol) {
520
+ const b = existing.indexOf(SKILL_BEGIN);
521
+ const e = existing.indexOf(SKILL_END);
522
+ if (b !== -1 && e !== -1) {
523
+ return existing.slice(0, b) + block + existing.slice(e + SKILL_END.length);
524
+ }
525
+ if (existing.length === 0) return `${block}${eol}`;
526
+ const sep = existing.endsWith(eol) ? "" : eol;
527
+ return `${existing}${sep}${block}${eol}`;
528
+ }
529
+ function removeBlock(existing) {
530
+ const b = existing.indexOf(SKILL_BEGIN);
531
+ const e = existing.indexOf(SKILL_END);
532
+ if (b === -1 || e === -1) return null;
533
+ const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
534
+ return out.replace(/(\r?\n){2,}$/, "$1");
535
+ }
536
+ function atomicWrite(target, content) {
537
+ const tmp = `${target}.tmp`;
538
+ writeFileSync3(tmp, content);
539
+ const fd = openSync(tmp, "r+");
540
+ try {
541
+ fsyncSync(fd);
542
+ } finally {
543
+ closeSync(fd);
544
+ }
545
+ renameSync(tmp, target);
546
+ }
547
+ function resolveTarget(cwd, override) {
548
+ if (override) return resolve2(cwd, override);
549
+ const matches = detectTargets(cwd);
550
+ if (matches.length === 0) return resolve2(cwd, DEFAULT_TARGET);
551
+ if (matches.length === 1) return resolve2(cwd, matches[0]);
552
+ console.error("Multiple rules files detected. Use --target to disambiguate:");
553
+ for (const m of matches) console.error(` ${m}`);
554
+ return null;
555
+ }
556
+ function runInstall(cwd, opts) {
557
+ const target = resolveTarget(cwd, opts.target);
558
+ if (target === null) return 1;
559
+ const existing = existsSync3(target) ? readFileSync4(target, "utf-8") : "";
560
+ const eol = existing ? detectNewline(existing) : "\n";
561
+ const block = composeBlock(loadSkill(), eol);
562
+ const next = applyBlock(existing, block, eol);
563
+ if (next === existing) {
564
+ console.log("No changes \u2014 skill block already up to date.");
565
+ return 0;
566
+ }
567
+ if (opts.dryRun) {
568
+ process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
569
+ return 0;
570
+ }
571
+ atomicWrite(target, next);
572
+ console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
573
+ return 0;
574
+ }
575
+ function runUninstall(cwd, opts) {
576
+ const target = resolveTarget(cwd, opts.target);
577
+ if (target === null) return 1;
578
+ if (!existsSync3(target)) {
579
+ console.log(`Skill block not present at ${target}`);
580
+ return 0;
581
+ }
582
+ const existing = readFileSync4(target, "utf-8");
583
+ const next = removeBlock(existing);
584
+ if (next === null) {
585
+ console.log(`Skill block not present at ${target}`);
586
+ return 0;
587
+ }
588
+ atomicWrite(target, next);
589
+ console.log(`Removed skill block from ${target}`);
590
+ return 0;
591
+ }
592
+ function runStatus(cwd, opts) {
593
+ const target = resolveTarget(cwd, opts.target);
594
+ if (target === null) return 1;
595
+ if (!existsSync3(target)) {
596
+ console.log(`No rules file at ${target}`);
597
+ return 1;
598
+ }
599
+ const content = readFileSync4(target, "utf-8");
600
+ if (content.includes(SKILL_BEGIN) && content.includes(SKILL_END)) {
601
+ console.log(`PRIM SKILL v1 installed at ${target}`);
602
+ return 0;
603
+ }
604
+ console.log(`No PRIM SKILL block at ${target}`);
605
+ return 1;
606
+ }
607
+ function registerSkillCommands(program2) {
608
+ const skill = program2.command("skill").description("Manage the prim skill in your project rules file");
609
+ skill.command("install").description("Install the prim skill block into your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--dry-run", "Print a unified diff without writing").action((opts) => {
610
+ try {
611
+ process.exit(runInstall(process.cwd(), opts));
612
+ } catch (err) {
613
+ console.error(err instanceof Error ? err.message : String(err));
614
+ process.exit(2);
615
+ }
616
+ });
617
+ skill.command("uninstall").description("Remove the prim skill block from your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").action((opts) => {
618
+ process.exit(runUninstall(process.cwd(), opts));
619
+ });
620
+ skill.command("status").description("Report whether the prim skill block is installed").option("--target <path>", "Path to the rules file (overrides auto-detection)").action((opts) => {
621
+ process.exit(runStatus(process.cwd(), opts));
622
+ });
623
+ }
624
+
476
625
  // src/commands/spec.ts
477
- import { readFileSync as readFileSync4 } from "fs";
626
+ import { readFileSync as readFileSync5 } from "fs";
478
627
  function registerSpecCommands(program2) {
479
628
  const spec = program2.command("spec").description("Manage spec documents");
480
629
  spec.command("list").description("List spec documents").option("-t, --project-id <projectId>", "List spec for a specific root project").action(async (opts) => {
@@ -515,7 +664,7 @@ ${contexts.length} spec(s)`);
515
664
  const client = getClient();
516
665
  let text = opts.text;
517
666
  if (opts.file) {
518
- text = readFileSync4(opts.file, "utf-8");
667
+ text = readFileSync5(opts.file, "utf-8");
519
668
  }
520
669
  if (!(text || opts.name)) {
521
670
  console.error("Provide --text, --file, or --name to update.");
@@ -599,8 +748,9 @@ ${preview}`);
599
748
  }
600
749
 
601
750
  // src/index.ts
602
- var __dirname = dirname2(fileURLToPath(import.meta.url));
603
- var pkg = JSON.parse(readFileSync5(resolve2(__dirname, "../package.json"), "utf-8"));
751
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
752
+ var pkg = JSON.parse(readFileSync6(resolve3(__dirname2, "../package.json"), "utf-8"));
753
+ updateNotifier({ pkg }).notify();
604
754
  var program = new Command();
605
755
  program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version);
606
756
  registerAuthCommands(program);
@@ -608,6 +758,7 @@ registerContextCommands(program);
608
758
  registerSpecCommands(program);
609
759
  registerProjectCommands(program);
610
760
  registerHooksCommands(program);
761
+ registerSkillCommands(program);
611
762
  process.on("unhandledRejection", (err) => {
612
763
  const msg = err instanceof Error ? err.message : String(err);
613
764
  console.error(msg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.13",
4
4
  "description": "CLI for managing Primitive specs, contexts, and git hooks",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,7 +35,8 @@
35
35
  "files": [
36
36
  "dist",
37
37
  "LICENSE",
38
- "README.md"
38
+ "README.md",
39
+ "SKILL.md"
39
40
  ],
40
41
  "scripts": {
41
42
  "build": "tsup src/index.ts src/hooks/pre-commit.ts --format esm --clean",
@@ -51,11 +52,15 @@
51
52
  "test:coverage": "vitest run --coverage"
52
53
  },
53
54
  "dependencies": {
54
- "commander": "^12.1.0"
55
+ "commander": "^12.1.0",
56
+ "diff": "^5.2.0",
57
+ "update-notifier": "^7.3.1"
55
58
  },
56
59
  "devDependencies": {
57
60
  "@biomejs/biome": "^1.9.0",
61
+ "@types/diff": "^5.2.0",
58
62
  "@types/node": "^25.5.0",
63
+ "@types/update-notifier": "^6.0.8",
59
64
  "@vitest/coverage-v8": "^3.1.0",
60
65
  "tsup": "^8.0.0",
61
66
  "typescript": "^5.5.0",