@rafinery/cli 0.1.0 → 0.2.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # @rafinery/cli — changelog
2
+
3
+ The machine-readable version of this lives in [`lib/releases.mjs`](lib/releases.mjs) — the
4
+ CLI reads it to tell a client, on `rafa update`, exactly what an upgrade requires (nothing,
5
+ a re-scan, or a `rafa migrate`).
6
+
7
+ ## 0.2.0
8
+
9
+ **Adopt with:** `npm i -g @rafinery/cli@latest && rafa update` — no re-scan, no migration.
10
+ Your brain data is still valid (contract v1, plans v1 unchanged).
11
+
12
+ - **Non-destructive vendoring.** `init`/`update` never clobber tuned files: owned files
13
+ (agents, capabilities) are preserved — a changed one is written as `<file>.new` for you to
14
+ merge — while tools + contract move in lockstep. Required permissions are **merged** into
15
+ `.claude/settings.json` (union into `permissions.allow`; every other key preserved; a file
16
+ we can't parse is left untouched).
17
+ - **Secrets guardrail.** The scan reads env var **names from source only** — never opens
18
+ `.env*` or captures a value.
19
+ - **`rafa leverage`.** Inspects your agent toolbox (permissions, skills, MCP, config) and
20
+ prints prioritized, advisory tips. Modular behind a `ToolchainAdapter` seam (Claude Code
21
+ today; Codex/Cursor slot in later).
22
+ - **Version-aware updates.** `init`/`update` notify you when a newer rafinery is published;
23
+ `update` shows what changed since your version and whether a re-scan or migration is needed.
24
+ - **`rafa migrate`.** Brings structured files (plans, config) up to the schema the installed
25
+ CLI ships. Idempotent; a no-op until a release changes a shape — the seam is in place.
26
+ - A `rafa.json` version stamp records the contract/plans versions this repo was provisioned
27
+ at (the anchor `update`/`migrate` diff against).
28
+
29
+ ## 0.1.0
30
+
31
+ - Deterministic contract + `manifest.json` ingest.
32
+ - `@rafinery/cli` — `init` (vendor blueprint + wire brain remote), `update`, `compile`, `push`.
package/bin/rafa.mjs CHANGED
@@ -5,6 +5,8 @@
5
5
  // rafa update re-sync the blueprint into this repo (shadcn-style)
6
6
  // rafa compile run the contract gate → .rafa/manifest.json
7
7
  // rafa push compile + push .rafa/ to the brain remote
8
+ // rafa leverage inspect your agent toolbox → proactive tips
9
+ // rafa migrate bring structured files (plans/config) up to the current schema
8
10
  //
9
11
  // The LLM work (scan / improve / plan) runs inside Claude Code via /rafa.
10
12
 
@@ -15,7 +17,7 @@ import { fileURLToPath } from "node:url";
15
17
  const HERE = dirname(fileURLToPath(import.meta.url));
16
18
  const pkg = JSON.parse(readFileSync(join(HERE, "..", "package.json"), "utf8"));
17
19
 
18
- const COMMANDS = ["init", "update", "compile", "push"];
20
+ const COMMANDS = ["init", "update", "compile", "push", "leverage", "migrate"];
19
21
  const [cmd, ...rest] = process.argv.slice(2);
20
22
 
21
23
  function help() {
@@ -29,6 +31,10 @@ Commands:
29
31
  update Re-sync the blueprint into this repo (you own your copy; opt-in updates).
30
32
  compile Validate the brain against the contract → .rafa/manifest.json.
31
33
  push Compile, then push .rafa/ to your brain remote.
34
+ leverage Inspect your agent toolbox (permissions, skills, MCP) and print
35
+ prioritized tips for getting more out of it. Advisory — changes nothing.
36
+ migrate Bring this repo's structured files (plans, config) up to the schema
37
+ the installed CLI ships. Idempotent; review the diff after.
32
38
 
33
39
  Then, inside Claude Code: /rafa scan · /rafa improve · /rafa plan
34
40
 
@@ -72,6 +72,10 @@ Never act cold; never over-load.
72
72
  - **Capture the trace, not the tool.** Knowledge is tool-agnostic — record the decision, the
73
73
  cited code, and the *why*, never which (possibly personal) skill produced it. Committed
74
74
  `.claude/` is org config (mappable); the dev's personal `~/.claude/` is private (never indexed).
75
+ - **Secrets are off-limits.** Record env var **names and where they're read** (from source only);
76
+ **never open `.env`/`.env.*` or any secret store, and never read or copy a value.** A key's
77
+ *name* is a contract; its *value* is a secret. If a note genuinely needs a value, stop and ask
78
+ the dev — don't harvest it. (Enforced in [scan](../../.rafa/capabilities/scan.md) step 4.)
75
79
  - **Orchestrate, don't bury.** At work-time, ride the host + the dev's existing skills/tools/MCP
76
80
  and recommend the best-fit one; never reinvent a capability that already exists. rafa conducts
77
81
  what's present.
@@ -0,0 +1,71 @@
1
+ # leverage — get more out of the toolbox you already have (capability #3)
2
+
3
+ rafa's third mission: **make the dev's agent toolchain pull its full weight.** rafa already
4
+ reads your *code* to build a brain; leverage turns the same lens on your *toolbox* — the
5
+ config layer (permissions, skills, MCP servers, hooks, commands) that decides how effective
6
+ the AI engineer actually is. It surfaces what's missing, misconfigured, or unused, and points
7
+ at the fix. This is platform DNA: **leverage what's present; never reinvent it.**
8
+
9
+ Invoke via `rafa leverage` (CLI, reads the repo — no brain required). Advisory only: it reads,
10
+ ranks, and reports; it **changes nothing**.
11
+
12
+ ---
13
+
14
+ ## Prime directives
15
+
16
+ 1. **Leverage, don't replace** — always prefer the dev's existing capability (a skill, an MCP
17
+ server, a host feature) over hand-rolling one. The best tip points at something already
18
+ installed but unwired.
19
+ 2. **Signal, not noise** — emit a tip only from a clear, checkable signal (a missing permission,
20
+ an un-ignored `.env`, a stack with no matching MCP). A false or obvious tip mutes the whole
21
+ advisor. Few, relevant, dismissible.
22
+ 3. **Leverage-ranked** — `P1` security/correctness → `P2` friction/leverage → `P3` nice-to-have.
23
+ Lead with impact × ease. Never dump an unranked wall.
24
+ 4. **Advisory, never authoritarian** — the dev owns their setup. Propose + show the fix; never
25
+ auto-apply. (`rafa init`/`update` apply the *permission* fixes, and only because the dev ran them.)
26
+ 5. **Tool-agnostic by construction** — the advice engine knows nothing about any specific tool;
27
+ all tool knowledge lives in an **adapter**. This is what makes "leverage what's available"
28
+ true for Codex, Cursor, or any other toolchain, not just Claude Code.
29
+
30
+ ---
31
+
32
+ ## Architecture — the ToolchainAdapter seam
33
+
34
+ The engine ([`lib/leverage/engine.mjs`](../../packages/cli/lib/leverage/engine.mjs)) gathers
35
+ light, tool-agnostic repo signals (`repoContext`: stack deps, `.env` files + whether they're
36
+ ignored, declared MCP servers), then runs every **detected** adapter and ranks the tips.
37
+
38
+ Each adapter implements one contract ([`lib/leverage/adapters/index.mjs`](../../packages/cli/lib/leverage/adapters/index.mjs)):
39
+
40
+ | Member | Shape | Job |
41
+ |---|---|---|
42
+ | `id` / `name` | string | identity + human label |
43
+ | `detect(cwd)` | → boolean | is this toolchain present in the repo? |
44
+ | `inspect(cwd)` | → Capabilities | normalized view: `{ permissions, skills, agents, commands, … }` |
45
+ | `recommend(cwd, ctx)` | → Tip[] | proactive tips `{ id, priority, title, detail, fix }` |
46
+
47
+ **Implemented:** `claude-code` — reads `.claude/settings.json`, `skills/`, `agents/`,
48
+ `commands/`, and `.mcp.json`. **Declared, not yet built:** `codex`, `cursor` (the `PLANNED`
49
+ list) — the seam is real, the adapters are added when a user actually runs on that toolchain.
50
+ *Don't build an adapter ahead of the phase that consumes it.*
51
+
52
+ ### What the Claude Code adapter checks today
53
+ - **Secrets** — `.env*` present but not gitignored → `P1` (protect the keys before a scan or a commit).
54
+ - **Permissions** — `.rafa/bin` vendored but its tools aren't allow-listed / no `settings.json`
55
+ → `P2`, fix = `rafa init`/`update` (which *merge*, never clobber — see the CLI).
56
+ - **MCP** — a stack with no matching server wired (e.g. Convex repo, no `convex` MCP) → `P2`.
57
+ - **Skills** — no project skills captured → `P3`.
58
+
59
+ Adding a check = one entry in an adapter's `recommend`. Adding a toolchain = one new adapter
60
+ behind the same contract. The engine and the `rafa leverage` command never change.
61
+
62
+ ---
63
+
64
+ ## Boundaries
65
+
66
+ - **Committed config only.** Read the repo's `.claude/` (org config). The dev's personal
67
+ `~/.claude/` is private — never inspected. (Mirrors [scan](scan.md)'s privacy line.)
68
+ - **Secrets are off-limits.** Detect that an `.env` is unprotected; **never open it or read a
69
+ value.** Same hard line as the scan guardrail.
70
+ - **Read-only.** leverage itself mutates nothing. The only writes are `rafa init`/`update`
71
+ merging permissions, run explicitly by the dev.
@@ -138,8 +138,13 @@ retrieval index. Bodies read like a senior engineer explaining that one concept
138
138
  data-op shape, naming).
139
139
  **One `convention` note minimum per domain** — this guarantees breadth. For the
140
140
  **external-integrations** domain, produce an **env-and-integrations** note: every
141
- required env var and the `file:line` where each is read (so "what keys does this repo
142
- need, and where?" is answerable at work-time). A domain marked `mapped` must have a note.
141
+ required env var **name** and the `file:line` in **source** where each is read (so "what keys
142
+ does this repo need, and where?" is answerable at work-time). A domain marked `mapped` must
143
+ have a note.
144
+ **Secrets guardrail (hard):** record env var **names only, read from code**. **Never open
145
+ `.env`, `.env.*`, or any secret file, and never read, log, or copy a value** — a name is a
146
+ contract, a value is a secret. If a note truly needs a value, **stop and ask the dev**; never
147
+ harvest it. (`.env*` is excluded from every read/grep in this step.)
143
148
  **Flag exceptions as non-exemplars.** When a convention has a grandfathered/legacy file
144
149
  that *violates* it — often the **most salient** example — the note MUST name that file and
145
150
  mark it *"exception, not the model — don't pattern-match it."* Salience gets copied; an
@@ -150,8 +155,8 @@ retrieval index. Bodies read like a senior engineer explaining that one concept
150
155
 
151
156
  5. **Seam detection** [grep + LLM judge] — anchor tokens (agent/graph names, tool names,
152
157
  env vars, route strings, exported state/types, table names). For each, run
153
- `git grep -n <token>` repo-wide and record **every** hit as a site (site list == grep
154
- hit list; never infer or sample). Any token in ≥2 boundary-crossing files = a
158
+ `git grep -n <token> -- ':!.env*'` repo-wide (env files excluded see the secrets guardrail
159
+ above) and record **every** hit as a site (site list == grep hit list; never infer or sample). Any token in ≥2 boundary-crossing files = a
155
160
  `contract`. Judge *what must stay in sync* and *does it fail silently*. State-shape
156
161
  contracts count too (e.g. a frontend `AgentState` type that must match a backend graph
157
162
  state). **Composition/ordering contracts count too** — provider nesting, middleware
package/lib/blueprint.mjs CHANGED
@@ -7,25 +7,31 @@ import {
7
7
  existsSync,
8
8
  mkdirSync,
9
9
  cpSync,
10
+ readFileSync,
10
11
  readdirSync,
11
12
  statSync,
12
13
  } from "node:fs";
13
14
  import { dirname, join } from "node:path";
14
15
  import { fileURLToPath } from "node:url";
15
16
 
16
- // Files a client owns + tunes (agents, SOPs). Tools + contract move in lockstep.
17
+ // Two classes of blueprint file:
18
+ // - owned → the client tunes these (agents, SOPs). We NEVER clobber a modified copy;
19
+ // a changed file is written alongside as `<file>.new` so the dev merges by hand.
20
+ // - lockstep → tools + contract. These move with the CLI version, so they overwrite.
17
21
  export const BLUEPRINT = {
18
- files: [
22
+ owned: [
19
23
  ".claude/agents/atlas.md",
20
24
  ".claude/agents/prism.md",
21
25
  ".claude/agents/bloom.md",
22
26
  ".claude/commands/rafa.md",
27
+ ],
28
+ ownedDirs: [".rafa/capabilities"],
29
+ lockstep: [
23
30
  ".rafa/contract.md",
24
31
  ".rafa/bin/verify-citations.mjs",
25
32
  ".rafa/bin/rafa-compile.mjs",
26
33
  ".rafa/bin/rafa-push.mjs",
27
34
  ],
28
- dirs: [".rafa/capabilities"],
29
35
  };
30
36
 
31
37
  const HERE = dirname(fileURLToPath(import.meta.url)); // …/packages/cli/lib
@@ -60,27 +66,70 @@ export function repoRoot() {
60
66
  return root;
61
67
  }
62
68
 
63
- // Copy every blueprint file/dir from `from` into `to`. Returns the relative paths written.
64
- export function copyBlueprint(from, to) {
65
- const written = [];
66
- for (const rel of BLUEPRINT.files) {
67
- const src = join(from, rel);
68
- if (!existsSync(src)) continue;
69
- const dst = join(to, rel);
70
- mkdirSync(dirname(dst), { recursive: true });
71
- cpSync(src, dst);
72
- written.push(rel);
69
+ // Expand a blueprint dir into its flat list of relative file paths.
70
+ function filesUnder(from, relDir) {
71
+ const out = [];
72
+ const abs = join(from, relDir);
73
+ if (!existsSync(abs)) return out;
74
+ for (const e of readdirSync(abs)) {
75
+ const p = join(abs, e);
76
+ if (statSync(p).isDirectory()) out.push(...filesUnder(from, join(relDir, e)));
77
+ else out.push(join(relDir, e));
78
+ }
79
+ return out;
80
+ }
81
+
82
+ // The resolved owned/lockstep file lists (dirs expanded), for a given source root.
83
+ export function blueprintFiles(from) {
84
+ const owned = [
85
+ ...BLUEPRINT.owned,
86
+ ...BLUEPRINT.ownedDirs.flatMap((d) => filesUnder(from, d)),
87
+ ];
88
+ return { owned, lockstep: [...BLUEPRINT.lockstep] };
89
+ }
90
+
91
+ function sameBytes(a, b) {
92
+ try {
93
+ return readFileSync(a).equals(readFileSync(b));
94
+ } catch {
95
+ return false;
73
96
  }
74
- for (const rel of BLUEPRINT.dirs) {
97
+ }
98
+
99
+ // Copy the blueprint from `from` into `to`.
100
+ // - mode "vendor" (default): non-destructive. lockstep files overwrite; owned files are
101
+ // created if absent, left untouched if identical, and preserved (writing `<file>.new`)
102
+ // if the dev has changed them — we never clobber tuning.
103
+ // - mode "bundle": force-copy everything (building the package tarball into a scratch dir).
104
+ // Returns { created, updated, unchanged, preserved } — arrays of relative paths.
105
+ export function copyBlueprint(from, to, { mode = "vendor" } = {}) {
106
+ const { owned, lockstep } = blueprintFiles(from);
107
+ const report = { created: [], updated: [], unchanged: [], preserved: [] };
108
+
109
+ const put = (rel, isOwned) => {
75
110
  const src = join(from, rel);
76
- if (!existsSync(src)) continue;
111
+ if (!existsSync(src)) return;
77
112
  const dst = join(to, rel);
78
- mkdirSync(dst, { recursive: true });
79
- cpSync(src, dst, { recursive: true });
80
- // list the .md files copied, for a useful summary
81
- for (const e of readdirSync(src)) {
82
- if (statSync(join(src, e)).isFile()) written.push(join(rel, e));
113
+ mkdirSync(dirname(dst), { recursive: true });
114
+ if (!existsSync(dst)) {
115
+ cpSync(src, dst);
116
+ report.created.push(rel);
117
+ return;
83
118
  }
84
- }
85
- return written;
119
+ if (sameBytes(src, dst)) {
120
+ report.unchanged.push(rel);
121
+ return;
122
+ }
123
+ if (mode === "bundle" || !isOwned) {
124
+ cpSync(src, dst); // lockstep, or a scratch bundle: safe to overwrite
125
+ report.updated.push(rel);
126
+ } else {
127
+ cpSync(src, `${dst}.new`); // owned + modified: keep the dev's copy, offer .new
128
+ report.preserved.push(rel);
129
+ }
130
+ };
131
+
132
+ for (const rel of lockstep) put(rel, false);
133
+ for (const rel of owned) put(rel, true);
134
+ return report;
86
135
  }
@@ -0,0 +1,57 @@
1
+ // Non-destructive reads/writes of the host's Claude Code config. rafa augments the
2
+ // dev's toolbox; it must never clear or overwrite what's already there.
3
+
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+
7
+ // The permissions rafa's installed tooling needs to run without a prompt each time.
8
+ // Kept minimal + specific to what `rafa init` actually vendors into `.rafa/bin`.
9
+ export const RAFA_PERMISSIONS = [
10
+ "Read(.rafa/**)",
11
+ "Bash(node .rafa/bin/rafa-compile.mjs:*)",
12
+ "Bash(node .rafa/bin/rafa-push.mjs:*)",
13
+ "Bash(node .rafa/bin/verify-citations.mjs:*)",
14
+ ];
15
+
16
+ export function settingsPath(targetDir) {
17
+ return join(targetDir, ".claude", "settings.json");
18
+ }
19
+
20
+ // Read + parse .claude/settings.json. Returns null if absent, `{ error }` if it exists
21
+ // but can't be parsed (so callers can refuse to touch a file they can't understand).
22
+ export function readClaudeSettings(targetDir) {
23
+ const file = settingsPath(targetDir);
24
+ if (!existsSync(file)) return null;
25
+ try {
26
+ return JSON.parse(readFileSync(file, "utf8"));
27
+ } catch (e) {
28
+ return { error: e instanceof Error ? e.message : String(e) };
29
+ }
30
+ }
31
+
32
+ // Merge rafa's required permissions into .claude/settings.json WITHOUT clobbering the
33
+ // dev's config — union into `permissions.allow`, preserve every other key untouched.
34
+ // Never overwrites a file it can't parse. Returns:
35
+ // { added: [...] } entries newly written (empty = already present)
36
+ // { skipped: "reason" } we declined to touch the file (e.g. unparseable)
37
+ export function mergeClaudeSettings(targetDir, perms = RAFA_PERMISSIONS) {
38
+ const file = settingsPath(targetDir);
39
+ const existing = readClaudeSettings(targetDir);
40
+ if (existing && existing.error) {
41
+ return { skipped: `.claude/settings.json is not valid JSON (${existing.error})` };
42
+ }
43
+
44
+ const settings = existing ?? {};
45
+ const permissions = { ...(settings.permissions ?? {}) };
46
+ const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
47
+ const have = new Set(allow);
48
+ const added = perms.filter((p) => !have.has(p));
49
+
50
+ if (added.length === 0 && existing) return { added: [] };
51
+
52
+ permissions.allow = [...allow, ...added];
53
+ const next = { ...settings, permissions };
54
+ mkdirSync(dirname(file), { recursive: true });
55
+ writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
56
+ return { added };
57
+ }
package/lib/init.mjs CHANGED
@@ -10,6 +10,32 @@ import {
10
10
  import { execSync } from "node:child_process";
11
11
  import { join } from "node:path";
12
12
  import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
13
+ import { mergeClaudeSettings } from "./claude-config.mjs";
14
+ import { stampCurrent } from "./stamp.mjs";
15
+ import { notifyIfBehind } from "./version-check.mjs";
16
+
17
+ // One-line summary of a copyBlueprint report; notes preserved (un-clobbered) files.
18
+ export function reportBlueprint(r) {
19
+ const n = (a) => a.length;
20
+ console.log(
21
+ ` ✓ blueprint: ${n(r.created)} new · ${n(r.updated)} updated · ` +
22
+ `${n(r.unchanged)} unchanged · ${n(r.preserved)} preserved`,
23
+ );
24
+ for (const rel of r.preserved)
25
+ console.log(` • kept your ${rel} — latest written to ${rel}.new (merge by hand)`);
26
+ }
27
+
28
+ // Union rafa's required permissions into .claude/settings.json without clobbering it.
29
+ export function reportSettings(target) {
30
+ const res = mergeClaudeSettings(target);
31
+ if (res.skipped) {
32
+ console.log(` ! settings untouched — ${res.skipped}`);
33
+ } else if (res.added.length) {
34
+ console.log(` ✓ merged ${res.added.length} permission(s) into .claude/settings.json`);
35
+ } else {
36
+ console.log(" ✓ .claude/settings.json already grants rafa's tools");
37
+ }
38
+ }
13
39
 
14
40
  const die = (m) => {
15
41
  console.error(`✗ ${m}`);
@@ -42,11 +68,13 @@ export default async function init(args) {
42
68
  console.log("rafa init — vendoring the blueprint (no setup URL)");
43
69
  }
44
70
 
45
- // Vendor the blueprint into .claude/ + .rafa/ (you own these copies).
46
- const written = copyBlueprint(blueprintSource(), TARGET);
71
+ // Vendor the blueprint into .claude/ + .rafa/ (you own these copies) — non-destructive:
72
+ // owned files (agents, capabilities) are never clobbered, tools/contract move in lockstep.
73
+ reportBlueprint(copyBlueprint(blueprintSource(), TARGET));
47
74
  if (!existsSync(join(TARGET, ".rafa", "active.md")))
48
75
  writeFileSync(join(TARGET, ".rafa", "active.md"), "# No active plan\n");
49
- console.log(` ✓ vendored ${written.length} blueprint files .claude/ + .rafa/`);
76
+ // Grant rafa's vendored tools their permissions (merge, never overwrite).
77
+ reportSettings(TARGET);
50
78
 
51
79
  // Keep the brain out of the code repo's diffs.
52
80
  const gi = join(TARGET, ".gitignore");
@@ -56,21 +84,22 @@ export default async function init(args) {
56
84
  console.log(" ✓ .rafa/ → .gitignore");
57
85
  }
58
86
 
59
- if (p) {
60
- writeFileSync(
61
- join(TARGET, "rafa.json"),
62
- JSON.stringify(
63
- {
87
+ // Stamp the versions this repo was provisioned at — the compat anchor `update`/`migrate`
88
+ // diff against. Carries platform identity too when provisioned via a setup URL.
89
+ stampCurrent(
90
+ TARGET,
91
+ p
92
+ ? {
64
93
  name: p.repoName,
65
94
  repoId: p.repoId,
66
95
  codeRepo: p.codeRepoUrl ?? null,
67
96
  brainRepo: p.brainRepoUrl,
68
97
  provisionedFor: p.owner ?? null,
69
- },
70
- null,
71
- 2,
72
- ) + "\n",
73
- );
98
+ }
99
+ : {},
100
+ );
101
+
102
+ if (p) {
74
103
  const rafaDir = join(TARGET, ".rafa");
75
104
  if (!existsSync(join(rafaDir, ".git"))) run("git init -b main", rafaDir);
76
105
  try {
@@ -88,4 +117,6 @@ export default async function init(args) {
88
117
  " 2. Run /rafa scan — build + validate the brain, then rafa push to sync it.\n" +
89
118
  " 3. Open the repo on the rafinery platform — its Overview shows the brain once pushed.",
90
119
  );
120
+
121
+ await notifyIfBehind();
91
122
  }
@@ -0,0 +1,103 @@
1
+ // Claude Code toolchain adapter. Normalizes the host's `.claude/` + `.mcp.json` config
2
+ // and emits proactive tips for getting more out of it. Implements the ToolchainAdapter
3
+ // contract in ../index.mjs.
4
+
5
+ import { existsSync, readdirSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { readClaudeSettings, RAFA_PERMISSIONS } from "../../claude-config.mjs";
8
+
9
+ const P1 = "P1", P2 = "P2", P3 = "P3";
10
+
11
+ function count(dir) {
12
+ return existsSync(dir) ? readdirSync(dir).length : 0;
13
+ }
14
+
15
+ const adapter = {
16
+ id: "claude-code",
17
+ name: "Claude Code",
18
+
19
+ // Present if the repo carries Claude Code config.
20
+ detect(cwd) {
21
+ return existsSync(join(cwd, ".claude")) || existsSync(join(cwd, ".mcp.json"));
22
+ },
23
+
24
+ // Normalized capability view — the shape every adapter reports in.
25
+ inspect(cwd) {
26
+ const settings = readClaudeSettings(cwd);
27
+ const ok = settings && !settings.error;
28
+ return {
29
+ permissions: ok ? (settings.permissions?.allow ?? []) : [],
30
+ settingsState: !settings ? "missing" : settings.error ? "unparseable" : "ok",
31
+ skills: count(join(cwd, ".claude", "skills")),
32
+ agents: count(join(cwd, ".claude", "agents")),
33
+ commands: count(join(cwd, ".claude", "commands")),
34
+ };
35
+ },
36
+
37
+ // Proactive tips, from clear signals only (no noise).
38
+ recommend(cwd, ctx) {
39
+ const tips = [];
40
+ const cap = this.inspect(cwd);
41
+
42
+ // Secrets: .env present but not ignored — leverage the toolbox to protect them.
43
+ if (ctx.envFiles.length && !ctx.envGitignored) {
44
+ tips.push({
45
+ id: "env-not-ignored",
46
+ priority: P1,
47
+ title: "`.env` files aren't gitignored",
48
+ detail: `Found ${ctx.envFiles.join(", ")} untracked-by-ignore — secrets can be committed or read into a scan.`,
49
+ fix: "add `.env*` to .gitignore (keep `!.env.example`)",
50
+ });
51
+ }
52
+
53
+ // rafa's own tools should run without a prompt each time.
54
+ if (existsSync(join(cwd, ".rafa", "bin"))) {
55
+ if (cap.settingsState === "missing") {
56
+ tips.push({
57
+ id: "no-settings",
58
+ priority: P2,
59
+ title: "No `.claude/settings.json` — every rafa tool call prompts",
60
+ detail: "rafa vendored gates into .rafa/bin but nothing grants them.",
61
+ fix: "run `rafa init` (or `rafa update`) — it merges the permissions in",
62
+ });
63
+ } else if (cap.settingsState === "ok") {
64
+ const missing = RAFA_PERMISSIONS.filter((p) => !cap.permissions.includes(p));
65
+ if (missing.length) {
66
+ tips.push({
67
+ id: "perms-missing",
68
+ priority: P2,
69
+ title: `${missing.length} rafa tool permission(s) not allow-listed`,
70
+ detail: "the vendored gates will prompt until granted.",
71
+ fix: "run `rafa update` — it unions them into settings.json without clobbering yours",
72
+ });
73
+ }
74
+ }
75
+ }
76
+
77
+ // MCP leverage: a Convex repo with no Convex MCP server wired.
78
+ if (ctx.stack.convex && !ctx.mcp.servers.has("convex")) {
79
+ tips.push({
80
+ id: "mcp-convex",
81
+ priority: P2,
82
+ title: "Convex in the stack, but no Convex MCP server",
83
+ detail: "wiring it gives agents schema-aware queries + function specs instead of guessing.",
84
+ fix: "add a `convex` entry to .mcp.json",
85
+ });
86
+ }
87
+
88
+ // Skills: package repeatable procedures so agents reuse them.
89
+ if (cap.skills === 0) {
90
+ tips.push({
91
+ id: "no-skills",
92
+ priority: P3,
93
+ title: "No project skills in `.claude/skills`",
94
+ detail: "recurring procedures (deploy, scaffold, review) reused across sessions belong here.",
95
+ fix: "capture one repeatable flow as a skill",
96
+ });
97
+ }
98
+
99
+ return tips;
100
+ },
101
+ };
102
+
103
+ export default adapter;
@@ -0,0 +1,23 @@
1
+ // Toolchain adapter registry. Every adapter implements the same contract so the
2
+ // leverage engine stays tool-agnostic:
3
+ //
4
+ // id string
5
+ // name string human label
6
+ // detect (cwd) => boolean is this toolchain present in the repo?
7
+ // inspect (cwd) => Capabilities normalized view { permissions, skills, ... }
8
+ // recommend(cwd, ctx) => Tip[] proactive tips { id, priority, title, detail, fix }
9
+ //
10
+ // This is the seam that makes "leverage what's available" true beyond Claude Code:
11
+ // Codex, Cursor, and any other agent toolchain slot in here behind the same interface.
12
+ // They're declared, not yet built — we add each adapter when a user actually runs on it
13
+ // (don't build ahead of the phase that consumes it).
14
+
15
+ import claudeCode from "./claude-code.mjs";
16
+
17
+ export const adapters = [claudeCode];
18
+
19
+ // Declared-but-unimplemented toolchains — the shape they'll fill.
20
+ export const PLANNED = [
21
+ { id: "codex", name: "Codex" },
22
+ { id: "cursor", name: "Cursor" },
23
+ ];
@@ -0,0 +1,82 @@
1
+ // The leverage advisor — tool-agnostic. It reads light signals about the repo, runs
2
+ // every detected toolchain adapter, and returns prioritized tips for getting more out
3
+ // of the dev's existing setup. The adapters (Claude Code today; Codex/Cursor behind the
4
+ // same seam) do the tool-specific work; this engine only orchestrates + ranks.
5
+
6
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { adapters } from "./adapters/index.mjs";
9
+
10
+ // Merge dependencies from the cwd package.json and any apps/*/package.json (monorepo).
11
+ function readDeps(cwd) {
12
+ const deps = new Set();
13
+ const add = (file) => {
14
+ if (!existsSync(file)) return;
15
+ try {
16
+ const p = JSON.parse(readFileSync(file, "utf8"));
17
+ for (const k of Object.keys({ ...p.dependencies, ...p.devDependencies })) deps.add(k);
18
+ } catch {
19
+ /* ignore unparseable package.json */
20
+ }
21
+ };
22
+ add(join(cwd, "package.json"));
23
+ const appsDir = join(cwd, "apps");
24
+ if (existsSync(appsDir)) {
25
+ for (const app of readdirSync(appsDir)) add(join(appsDir, app, "package.json"));
26
+ }
27
+ return deps;
28
+ }
29
+
30
+ // Env files present in the repo root (excluding the committed example).
31
+ function listEnvFiles(cwd) {
32
+ return readdirSync(cwd).filter((f) => /^\.env(\.|$)/.test(f) && f !== ".env.example");
33
+ }
34
+
35
+ // Does .gitignore actually cover .env files?
36
+ function envIgnored(cwd) {
37
+ const gi = join(cwd, ".gitignore");
38
+ if (!existsSync(gi)) return false;
39
+ return /^\s*\.env/m.test(readFileSync(gi, "utf8"));
40
+ }
41
+
42
+ // Server names declared in .mcp.json.
43
+ function readMcp(cwd) {
44
+ const file = join(cwd, ".mcp.json");
45
+ if (!existsSync(file)) return { present: false, servers: new Set() };
46
+ try {
47
+ const j = JSON.parse(readFileSync(file, "utf8"));
48
+ return { present: true, servers: new Set(Object.keys(j.mcpServers ?? j.servers ?? {})) };
49
+ } catch {
50
+ return { present: true, servers: new Set() };
51
+ }
52
+ }
53
+
54
+ // Light, tool-agnostic view of the repo so adapters can tailor tips to the real stack.
55
+ export function repoContext(cwd) {
56
+ const deps = readDeps(cwd);
57
+ const has = (n) => deps.has(n);
58
+ const envFiles = listEnvFiles(cwd);
59
+ return {
60
+ stack: {
61
+ next: has("next"),
62
+ convex: has("convex") || existsSync(join(cwd, "convex")) || existsSync(join(cwd, "apps", "web", "convex")),
63
+ langgraph: has("@langchain/langgraph"),
64
+ clerk: [...deps].some((d) => d.startsWith("@clerk/")),
65
+ },
66
+ envFiles,
67
+ envGitignored: envFiles.length === 0 || envIgnored(cwd),
68
+ mcp: readMcp(cwd),
69
+ };
70
+ }
71
+
72
+ const RANK = { P1: 0, P2: 1, P3: 2 };
73
+
74
+ // Run every detected adapter, tag tips with their tool, and rank by priority.
75
+ export function advise(cwd) {
76
+ const ctx = repoContext(cwd);
77
+ const active = adapters.filter((a) => a.detect(cwd));
78
+ const tips = active
79
+ .flatMap((a) => a.recommend(cwd, ctx).map((t) => ({ ...t, tool: a.name })))
80
+ .sort((x, y) => (RANK[x.priority] ?? 9) - (RANK[y.priority] ?? 9));
81
+ return { ctx, detected: active.map((a) => a.name), tips };
82
+ }
@@ -0,0 +1,36 @@
1
+ // rafa leverage — inspect the dev's agent toolbox (permissions, skills, MCP, config)
2
+ // and print prioritized tips for getting more out of what's already installed. Advisory
3
+ // only: it reads, ranks, and reports — it changes nothing. Run from your repo root.
4
+
5
+ import { advise } from "./leverage/engine.mjs";
6
+ import { PLANNED } from "./leverage/adapters/index.mjs";
7
+
8
+ export default async function leverage() {
9
+ const cwd = process.cwd();
10
+ const { detected, tips } = advise(cwd);
11
+
12
+ if (detected.length === 0) {
13
+ console.log(
14
+ "No supported agent toolchain detected here.\n" +
15
+ ` Supported now: Claude Code. Planned: ${PLANNED.map((p) => p.name).join(", ")}.`,
16
+ );
17
+ return;
18
+ }
19
+
20
+ console.log(`rafa leverage — inspecting your toolbox (${detected.join(", ")})\n`);
21
+
22
+ if (tips.length === 0) {
23
+ console.log("✓ Nothing to tune — your toolbox is set up well.");
24
+ return;
25
+ }
26
+
27
+ for (const t of tips) {
28
+ console.log(` [${t.priority}] ${t.title} · ${t.tool}`);
29
+ console.log(` ${t.detail}`);
30
+ if (t.fix) console.log(` fix: ${t.fix}`);
31
+ console.log("");
32
+ }
33
+ console.log(
34
+ `${tips.length} suggestion(s), most impactful first. These adjust your setup — nothing was changed.`,
35
+ );
36
+ }
@@ -0,0 +1,47 @@
1
+ // rafa migrate — bring a repo's structured files (plans, config) up to the schema the
2
+ // installed CLI ships. Reads the version stamp to know where the repo is coming FROM,
3
+ // runs the applicable migrations idempotently, and re-stamps. Advisory + reversible:
4
+ // it only rewrites files you can review in `git diff` (and re-run safely).
5
+
6
+ import { CURRENT, releaseOf } from "./releases.mjs";
7
+ import { stampedVersions, stampCurrent } from "./stamp.mjs";
8
+ import { pendingMigrations } from "./migrations/index.mjs";
9
+
10
+ export default async function migrate() {
11
+ const cwd = process.cwd();
12
+ const from = stampedVersions(cwd);
13
+ const pending = pendingMigrations(from, CURRENT);
14
+
15
+ if (pending.length === 0) {
16
+ console.log(
17
+ `✓ Nothing to migrate — plans v${from.plans}, contract v${from.contract} ` +
18
+ `already match ${CURRENT.version}.`,
19
+ );
20
+ // Still advance the stamp so the recorded CLI version reflects reality.
21
+ stampCurrent(cwd);
22
+ return;
23
+ }
24
+
25
+ console.log(`rafa migrate — applying ${pending.length} migration(s) → ${CURRENT.version}\n`);
26
+ for (const m of pending) {
27
+ console.log(` • ${m.id} [${m.kind}] — ${m.summary}`);
28
+ let r;
29
+ try {
30
+ r = m.run(cwd);
31
+ } catch (e) {
32
+ console.error(` ✗ failed: ${e instanceof Error ? e.message : e}`);
33
+ console.error(" Stamp left unchanged — fix the error and re-run `rafa migrate`.");
34
+ process.exit(1);
35
+ }
36
+ console.log(` ✓ ${r.changed.length} file(s) updated`);
37
+ for (const n of r.notes) console.log(` ${n}`);
38
+ }
39
+
40
+ stampCurrent(cwd);
41
+ const rescan = releaseOf(CURRENT.version)?.requires === "rescan";
42
+ console.log(
43
+ `\n✓ Migrated to ${CURRENT.version}. Review the diff` +
44
+ (rescan ? ", then run `/rafa scan` (the brain contract changed)" : "") +
45
+ ", then `rafa push` if this is a connected repo.",
46
+ );
47
+ }
@@ -0,0 +1,30 @@
1
+ // Migration registry. When a release changes the SHAPE of a structured file (a plan's
2
+ // frontmatter, the contract, vendored config), it adds a migration here so clients move
3
+ // forward with one `rafa migrate` instead of hand-editing. Migrations are idempotent and
4
+ // run in order; each transforms the client's on-disk files from one schema to the next.
5
+ //
6
+ // Contract:
7
+ // id string stable identifier, shown in the log
8
+ // kind "plans" | "contract" | "config"
9
+ // summary string one line: what it changes + why
10
+ // appliesTo(from, to) => boolean from = stampedVersions(cwd), to = CURRENT
11
+ // run(cwd) => { changed: string[], notes: string[] } idempotent; report what it touched
12
+ //
13
+ // Empty by design until a release actually changes a shape — the SEAM is what matters,
14
+ // so the first breaking change ships its migration here and `rafa migrate` just works.
15
+ // Example (kept as a reference, not registered):
16
+ //
17
+ // const plans_v1_to_v2 = {
18
+ // id: "plans-add-owner-field",
19
+ // kind: "plans",
20
+ // summary: "plan frontmatter gains a required `owner` field (default: repo owner).",
21
+ // appliesTo: (from, to) => from.plans < 2 && to.plans >= 2,
22
+ // run: (cwd) => backfillPlanField(cwd, "owner", ownerOf(cwd)),
23
+ // };
24
+
25
+ export const MIGRATIONS = [];
26
+
27
+ // The migrations that apply to a given move (stamped `from` → shipped `to`), in order.
28
+ export function pendingMigrations(from, to) {
29
+ return MIGRATIONS.filter((m) => m.appliesTo(from, to));
30
+ }
@@ -0,0 +1,63 @@
1
+ // The release ledger — the CLI's machine-readable source of truth for "what does moving
2
+ // to version X require?" Every published release adds one entry here (in lockstep with
3
+ // package.json). It declares the contract + plans schema versions that release ships and
4
+ // the strongest action a client must take to adopt it:
5
+ //
6
+ // requires: "update" → re-sync the blueprint (`rafa update`); no data change
7
+ // "migrate" → run `rafa migrate` (a structured file changed shape)
8
+ // "rescan" → the brain contract changed incompatibly; regenerate via `/rafa scan`
9
+ //
10
+ // `update` reads this to tell the client — transparently — what an upgrade means for them.
11
+
12
+ import { readFileSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ export const RELEASES = [
17
+ {
18
+ version: "0.1.0",
19
+ contract: 1,
20
+ plans: 1,
21
+ requires: "update",
22
+ summary:
23
+ "Deterministic contract + manifest ingest; @rafinery/cli (init/update/compile/push).",
24
+ },
25
+ {
26
+ version: "0.2.0",
27
+ contract: 1,
28
+ plans: 1,
29
+ requires: "update",
30
+ summary:
31
+ "Non-destructive vendoring (merge settings, never clobber tuned agents); secrets " +
32
+ "guardrail in scan; `rafa leverage` toolbox advisor; version-aware updates + `rafa migrate`.",
33
+ },
34
+ ];
35
+
36
+ // The release this CLI build ships (last entry).
37
+ export const CURRENT = RELEASES[RELEASES.length - 1];
38
+
39
+ // The installed CLI version, read from package.json (the single numeric source of truth).
40
+ const HERE = dirname(fileURLToPath(import.meta.url)); // …/packages/cli/lib
41
+ export const CLI_VERSION = JSON.parse(
42
+ readFileSync(join(HERE, "..", "package.json"), "utf8"),
43
+ ).version;
44
+
45
+ // Compare two x.y.z strings → -1 | 0 | 1.
46
+ export function cmpSemver(a, b) {
47
+ const pa = String(a).split(".").map(Number);
48
+ const pb = String(b).split(".").map(Number);
49
+ for (let i = 0; i < 3; i++) {
50
+ const d = (pa[i] || 0) - (pb[i] || 0);
51
+ if (d) return d < 0 ? -1 : 1;
52
+ }
53
+ return 0;
54
+ }
55
+
56
+ export function releaseOf(version) {
57
+ return RELEASES.find((r) => r.version === version) ?? null;
58
+ }
59
+
60
+ // Releases strictly newer than `fromVersion`, oldest-first — the changelog to show on update.
61
+ export function releasesAfter(fromVersion) {
62
+ return RELEASES.filter((r) => cmpSemver(r.version, fromVersion) > 0);
63
+ }
package/lib/stamp.mjs ADDED
@@ -0,0 +1,55 @@
1
+ // The version stamp — `rafa.json` at the repo root records what versions this repo was
2
+ // last provisioned/updated at. It is the compat anchor: `update` and `migrate` diff the
3
+ // stamp against what the running CLI ships to know what changed and what to migrate FROM.
4
+ // (Also holds platform identity fields when the repo was provisioned via a setup URL.)
5
+
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { CURRENT, CLI_VERSION, RELEASES } from "./releases.mjs";
9
+
10
+ export function stampPath(cwd) {
11
+ return join(cwd, "rafa.json");
12
+ }
13
+
14
+ export function readStamp(cwd) {
15
+ const f = stampPath(cwd);
16
+ if (!existsSync(f)) return {};
17
+ try {
18
+ return JSON.parse(readFileSync(f, "utf8"));
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+
24
+ // Merge a patch into rafa.json without dropping existing keys (platform fields survive
25
+ // a version bump; the `rafa` version block merges field-by-field).
26
+ export function writeStamp(cwd, patch = {}) {
27
+ const cur = readStamp(cwd);
28
+ const next = {
29
+ ...cur,
30
+ ...patch,
31
+ rafa: { ...(cur.rafa ?? {}), ...(patch.rafa ?? {}) },
32
+ };
33
+ writeFileSync(stampPath(cwd), JSON.stringify(next, null, 2) + "\n");
34
+ return next;
35
+ }
36
+
37
+ // The versions this repo was last stamped at. Un-stamped repos predate the stamp (added in
38
+ // 0.2.0), so default to the FIRST release's schema — never raise a false migration alarm.
39
+ export function stampedVersions(cwd) {
40
+ const base = RELEASES[0];
41
+ const s = readStamp(cwd).rafa ?? {};
42
+ return {
43
+ cli: s.cli ?? base.version,
44
+ contract: s.contract ?? base.contract,
45
+ plans: s.plans ?? base.plans,
46
+ };
47
+ }
48
+
49
+ // Re-stamp to the versions this CLI build ships (called after init/update/migrate).
50
+ export function stampCurrent(cwd, extra = {}) {
51
+ return writeStamp(cwd, {
52
+ ...extra,
53
+ rafa: { cli: CLI_VERSION, contract: CURRENT.contract, plans: CURRENT.plans },
54
+ });
55
+ }
package/lib/update.mjs CHANGED
@@ -1,10 +1,18 @@
1
- // rafa update — re-sync the blueprint into this repo. shadcn-style: you own your
2
- // copies, so this is an opt-in refresh (run it to pull the latest agents,
3
- // capabilities, contract, and gates after upgrading the CLI).
1
+ // rafa update — re-sync the blueprint into this repo. shadcn-style: you own your copies,
2
+ // so this is non-destructive tools + contract move to the latest, but your tuned
3
+ // agents/capabilities are never clobbered (a changed one is written as `<file>.new`).
4
+ //
5
+ // It also makes the upgrade TRANSPARENT: it shows what changed since your last version,
6
+ // and whether the change needs a re-scan (brain contract) or a `rafa migrate` (plan shape).
4
7
 
5
8
  import { existsSync } from "node:fs";
6
9
  import { join } from "node:path";
7
10
  import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
11
+ import { reportBlueprint, reportSettings } from "./init.mjs";
12
+ import { CURRENT, releasesAfter } from "./releases.mjs";
13
+ import { stampedVersions, stampCurrent } from "./stamp.mjs";
14
+ import { pendingMigrations } from "./migrations/index.mjs";
15
+ import { notifyIfBehind } from "./version-check.mjs";
8
16
 
9
17
  export default async function update() {
10
18
  const TARGET = process.cwd();
@@ -12,11 +20,49 @@ export default async function update() {
12
20
  console.error("✗ This repo isn't provisioned yet — run `rafa init` first.");
13
21
  process.exit(1);
14
22
  }
15
- const written = copyBlueprint(blueprintSource(), TARGET);
16
- console.log(`✓ rafa update synced ${written.length} blueprint files:`);
17
- for (const f of written) console.log(` ${f}`);
23
+
24
+ // Where this repo is coming from (before we re-stamp).
25
+ const from = stampedVersions(TARGET);
26
+
27
+ await notifyIfBehind();
28
+
29
+ console.log("rafa update — syncing the blueprint (non-destructive)");
30
+ reportBlueprint(copyBlueprint(blueprintSource(), TARGET));
31
+ reportSettings(TARGET);
32
+
33
+ // What changed since the client's last version — the transparent part.
34
+ const news = releasesAfter(from.cli);
35
+ if (news.length) {
36
+ console.log(`\n Changes since ${from.cli}:`);
37
+ for (const r of news) console.log(` • ${r.version} — ${r.summary}`);
38
+ }
39
+
40
+ // Does the upgrade require action beyond re-syncing files?
41
+ const rescan = CURRENT.contract > from.contract;
42
+ const migrate = CURRENT.plans > from.plans || pendingMigrations(from, CURRENT).length > 0;
43
+
44
+ console.log("");
45
+ if (rescan) {
46
+ console.log(
47
+ ` ⚠ Brain contract v${from.contract} → v${CURRENT.contract}: your brain is now stale.\n` +
48
+ " Run /rafa scan to regenerate it, then rafa push.",
49
+ );
50
+ }
51
+ if (migrate) {
52
+ console.log(
53
+ ` ⚠ Plan/structure schema changed (plans v${from.plans} → v${CURRENT.plans}).\n` +
54
+ " Run rafa migrate to bring your files forward.",
55
+ );
56
+ }
57
+ if (!rescan && !migrate) {
58
+ console.log(" ✓ No re-scan or migration needed — your brain data is still valid.");
59
+ }
60
+
61
+ // Advance the stamp to what we just installed.
62
+ stampCurrent(TARGET);
63
+
18
64
  console.log(
19
65
  "\n Your brain data (.rafa/brain, .rafa/improve) was untouched. " +
20
- "Restart Claude Code to reload the agents.",
66
+ "Review any `.new` files, then restart Claude Code to reload the agents.",
21
67
  );
22
68
  }
@@ -0,0 +1,33 @@
1
+ // Version awareness — tell the client, gently, when a newer rafinery is published. Fail
2
+ // SILENT: a network hiccup or offline machine must never break `init`/`update`.
3
+
4
+ import { cmpSemver, CLI_VERSION } from "./releases.mjs";
5
+
6
+ const REGISTRY = "https://registry.npmjs.org/@rafinery/cli/latest";
7
+
8
+ // The latest published version on npm, or null if it can't be determined quickly.
9
+ export async function latestOnNpm(timeoutMs = 2500) {
10
+ try {
11
+ const ctrl = new AbortController();
12
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
13
+ const res = await fetch(REGISTRY, { signal: ctrl.signal });
14
+ clearTimeout(t);
15
+ if (!res.ok) return null;
16
+ const j = await res.json();
17
+ return typeof j.version === "string" ? j.version : null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ // Print an upgrade nudge iff a strictly-newer version is published. Returns the latest
24
+ // version (or null) so callers can decide whether to say more.
25
+ export async function notifyIfBehind(installed = CLI_VERSION) {
26
+ const latest = await latestOnNpm();
27
+ if (!latest || cmpSemver(latest, installed) <= 0) return latest;
28
+ console.log(
29
+ `\n ⬆ rafinery ${latest} is available (you're on ${installed}).\n` +
30
+ ` Update: npm i -g @rafinery/cli@latest && rafa update\n`,
31
+ );
32
+ return latest;
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafinery/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "rafa — the AI engineer CLI. Vendors the rafa blueprint into your repo (shadcn-style) and runs the deterministic contract gates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "files": [
10
10
  "bin",
11
11
  "lib",
12
- "blueprint"
12
+ "blueprint",
13
+ "CHANGELOG.md"
13
14
  ],
14
15
  "engines": {
15
16
  "node": ">=18"