auriga-cli 1.18.0 → 1.18.3

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.
@@ -1,4 +1,13 @@
1
1
  export type ItemStatus = "installed" | "update-available" | "not-installed";
2
+ /**
3
+ * Per-category scan scope. Each category (workflow / skills / plugins / hooks)
4
+ * can be independently scanned in either user scope (~/.claude/, ~/.codex/)
5
+ * or project scope (<proj>/.claude/). The Web UI's per-column scope picker
6
+ * carries these through the `/api/state` query so the scanner reads the
7
+ * right truth source per category. Codex plugins are user-scope only by
8
+ * design and ignore this field.
9
+ */
10
+ export type ScanScope = "user" | "project";
2
11
  export interface StateReport {
3
12
  /** Absolute path to the project the server was launched against. Surfaced
4
13
  * in the UI's top bar so users know where Apply will write project-scope
@@ -16,6 +25,13 @@ export interface WorkflowState {
16
25
  status: ItemStatus;
17
26
  currentVersion?: string;
18
27
  expectedVersion: string;
28
+ /** Which scope the scanner read to produce this row. Reflects the scope
29
+ * scanned, not where the file was found — e.g. when scope=user and
30
+ * ~/.claude/CLAUDE.md is absent, observedScope is still "user". The
31
+ * scanner ALWAYS sets this field at runtime; it is typed optional only
32
+ * so the mergePluginsById regression helper (which carries over from the
33
+ * pre-rewrite suite without an explicit scope) continues to compile. */
34
+ observedScope?: ScanScope;
19
35
  }
20
36
  export interface SkillState {
21
37
  name: string;
@@ -24,6 +40,8 @@ export interface SkillState {
24
40
  isWorkflow: boolean;
25
41
  currentHash?: string;
26
42
  expectedHash: string;
43
+ /** Scope the scanner read to produce this row. See WorkflowState comment. */
44
+ observedScope?: ScanScope;
27
45
  }
28
46
  export type ApplyAgent = "claude" | "codex";
29
47
  export interface PluginState {
@@ -41,6 +59,10 @@ export interface PluginState {
41
59
  currentVersion?: string;
42
60
  expectedVersion?: string;
43
61
  versionSource: "upstream-live" | "catalog";
62
+ /** Scope the scanner read to produce this row. Codex plugins are always
63
+ * "user" (Codex has no project-scope plugin concept). See WorkflowState
64
+ * comment on why this is typed optional. */
65
+ observedScope?: ScanScope;
44
66
  }
45
67
  export interface HookState {
46
68
  name: string;
@@ -48,9 +70,11 @@ export interface HookState {
48
70
  status: ItemStatus;
49
71
  currentHash?: string;
50
72
  expectedHash: string;
73
+ /** Scope the scanner read to produce this row. See WorkflowState comment. */
74
+ observedScope?: ScanScope;
51
75
  }
52
76
  export interface StateWarning {
53
- code: "claude-cli-missing" | "codex-cli-missing" | "marketplace-offline";
77
+ code: "claude-cli-missing" | "codex-cli-missing" | "marketplace-offline" | "claude-code-not-installed" | "settings-unreadable" | "skill-malformed" | "workflow-unknown-version";
54
78
  message: string;
55
79
  }
56
80
  export type ApplyCategory = "workflow" | "skill" | "recommended-skill" | "plugin" | "hook";
package/dist/catalog.d.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  export interface CatalogEntry {
2
2
  name: string;
3
3
  description: string;
4
+ /** Build-time-baked plugin version. Set ONLY for plugin entries whose
5
+ * source lives in this repo's `plugins/<name>/` directory — the scanner
6
+ * uses it to surface "update-available" when the user's installed copy
7
+ * is older. Absent for skill / hook entries and for external-marketplace
8
+ * plugins (whose manifest lives upstream). Must be baked at build time
9
+ * because `plugins/<name>/.claude-plugin/plugin.json` is NOT shipped in
10
+ * the npm tarball (see `package.json` `files` field). */
11
+ expectedVersion?: string;
4
12
  }
5
13
  export interface Catalog {
6
14
  generatedAt: string;
package/dist/catalog.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-05-12T11:46:16.926Z",
2
+ "generatedAt": "2026-05-12T13:58:42.583Z",
3
3
  "workflowSkills": [
4
4
  {
5
5
  "name": "brainstorming",
@@ -87,19 +87,23 @@
87
87
  },
88
88
  {
89
89
  "name": "auriga-go",
90
- "description": "(Claude/Codex) Workflow autopilot for the auriga workflow. Reminder-based navigation across CLAUDE.md's phases. Bundles a /goalify skill that plans an autonomous goal from a spec or work-in-progress and dispatches it via Claude Code's built-in /goal command."
90
+ "description": "(Claude/Codex) Workflow autopilot for the auriga workflow. Reminder-based navigation across CLAUDE.md's phases. Bundles a /goalify skill that plans an autonomous goal from a spec or work-in-progress and dispatches it via Claude Code's built-in /goal command.",
91
+ "expectedVersion": "1.1.0"
91
92
  },
92
93
  {
93
94
  "name": "auriga-git-guards",
94
- "description": "(Claude/Codex) Git lifecycle guardrails: commit-reminder + PR-create snapshot inject + PR-ready structural block. Bundles the git-workflow skill (Claude Code + Codex)."
95
+ "description": "(Claude/Codex) Git lifecycle guardrails: commit-reminder + PR-create snapshot inject + PR-ready structural block. Bundles the git-workflow skill (Claude Code + Codex).",
96
+ "expectedVersion": "1.1.0"
95
97
  },
96
98
  {
97
99
  "name": "deep-review",
98
- "description": "(Claude/Codex) Multi-dimensional PR review orchestrator. Dispatches parallel reviewers (spec-conformance, correctness, test-quality, docs-sync, robustness, security, ux, performance, structure, code-quality, skill-plugin-quality) and synthesizes findings into an actionable punch list. Supports project-level custom reviewers via docs/rules/review/ and ships a reviewer-creator skill for scaffolding them."
100
+ "description": "(Claude/Codex) Multi-dimensional PR review orchestrator. Dispatches parallel reviewers (spec-conformance, correctness, test-quality, docs-sync, robustness, security, ux, performance, structure, code-quality, skill-plugin-quality) and synthesizes findings into an actionable punch list. Supports project-level custom reviewers via docs/rules/review/ and ships a reviewer-creator skill for scaffolding them.",
101
+ "expectedVersion": "0.3.1"
99
102
  },
100
103
  {
101
104
  "name": "session-instructions-loader",
102
- "description": "(Codex) Injects extra instruction files on session start"
105
+ "description": "(Codex) Injects extra instruction files on session start",
106
+ "expectedVersion": "1.0.0"
103
107
  }
104
108
  ],
105
109
  "hooks": [
package/dist/cli.js CHANGED
@@ -562,7 +562,12 @@ async function dispatchInstaller(category, packageRoot, opts) {
562
562
  // ---------------------------------------------------------------------------
563
563
  const UI_DEFAULT_PORT = 4747;
564
564
  const UI_PORT_RANGE = 10; // 4747..4756
565
- const UI_HEARTBEAT_TIMEOUT_MS = 15_000;
565
+ // 2 minutes covers Chrome's "intensive throttling" of background tabs
566
+ // (kicks in after ~5 min of being hidden, drops setInterval to ~1 ping/min).
567
+ // At 15s the browser tab being switched away for a moment would tear down
568
+ // the server — bad UX. Closing the browser now takes up to 2 min to release
569
+ // the port; users who care can ctrl+C the CLI for immediate exit.
570
+ const UI_HEARTBEAT_TIMEOUT_MS = 120_000;
566
571
  async function runUi(p, version) {
567
572
  // Lazy-load the server-side deps so the install / guide paths stay light.
568
573
  const { randomBytes } = await import("node:crypto");
@@ -8,7 +8,9 @@
8
8
  // skills-lock.json — expected SHA256 for every vendored skill
9
9
  // .claude/plugins.json — Claude plugin entries (agent = "claude")
10
10
  // .agents/plugins/install.json — Codex plugin entries (agent = "codex")
11
- // .claude/hooks/<name>/index.mjsruntime SHA256 = expected hash
11
+ // .claude/hooks/hooks.jsonregisters settingsEvents per hook (event /
12
+ // matcher / if) used by state.ts for drift
13
+ // detection in <scope>/.claude/settings.json
12
14
  // CLAUDE.md — `# auriga Workflow (vX.Y.Z)` provides
13
15
  // workflowVersion
14
16
  //
@@ -20,6 +22,15 @@ import { createHash } from "node:crypto";
20
22
  import { readFile } from "node:fs/promises";
21
23
  import path from "node:path";
22
24
  import { loadCatalog } from "./catalog.js";
25
+ async function sha256SkillMd(skillsRoot, name) {
26
+ try {
27
+ const buf = await readFile(path.join(skillsRoot, name, "SKILL.md"));
28
+ return createHash("sha256").update(buf).digest("hex");
29
+ }
30
+ catch {
31
+ return "";
32
+ }
33
+ }
23
34
  const WORKFLOW_VERSION_RE = /^#\s*auriga Workflow\s*\(v([\d.]+)\)/m;
24
35
  async function tryReadFile(p) {
25
36
  try {
@@ -29,10 +40,6 @@ async function tryReadFile(p) {
29
40
  return null;
30
41
  }
31
42
  }
32
- async function sha256File(p) {
33
- const bytes = await readFile(p);
34
- return createHash("sha256").update(bytes).digest("hex");
35
- }
36
43
  export async function buildScanCatalog(packageRoot) {
37
44
  const dist = loadCatalog(packageRoot);
38
45
  // Workflow version: parse from auriga-cli's own CLAUDE.md template.
@@ -41,22 +48,18 @@ export async function buildScanCatalog(packageRoot) {
41
48
  const claudeMd = await tryReadFile(path.join(packageRoot, "CLAUDE.md"));
42
49
  const m = claudeMd ? WORKFLOW_VERSION_RE.exec(claudeMd) : null;
43
50
  const workflowVersion = m ? m[1] : "";
44
- // Skills + recommended: hashes from skills-lock.json.
45
- let lock = {};
46
- const lockText = await tryReadFile(path.join(packageRoot, "skills-lock.json"));
47
- if (lockText) {
48
- try {
49
- lock = JSON.parse(lockText);
50
- }
51
- catch {
52
- // corrupted lock → no expectations; user state still classifies safely
53
- }
54
- }
51
+ // Skills + recommended: sha256 of each shipped SKILL.md. This is the same
52
+ // hash the scanner computes for `<scope>/.claude/skills/<name>/SKILL.md`
53
+ // at scan time, so a match means "user's installed copy is identical to
54
+ // the version auriga-cli ships". skills-lock.json's `computedHash` field
55
+ // hashes the entire skill directory (every file, sorted), which doesn't
56
+ // line up with the scanner's per-file model — we deliberately ignore it.
57
+ const skillsRoot = path.join(packageRoot, ".agents", "skills");
55
58
  const skills = {};
56
59
  for (const entry of dist.workflowSkills) {
57
60
  skills[entry.name] = {
58
61
  description: entry.description,
59
- expectedHash: lock.skills?.[entry.name]?.computedHash ?? "",
62
+ expectedHash: await sha256SkillMd(skillsRoot, entry.name),
60
63
  isWorkflow: true,
61
64
  };
62
65
  }
@@ -64,7 +67,7 @@ export async function buildScanCatalog(packageRoot) {
64
67
  for (const entry of dist.recommendedSkills) {
65
68
  recommendedSkills[entry.name] = {
66
69
  description: entry.description,
67
- expectedHash: lock.skills?.[entry.name]?.computedHash ?? "",
70
+ expectedHash: await sha256SkillMd(skillsRoot, entry.name),
68
71
  };
69
72
  }
70
73
  // Plugins: split by agent based on which config file lists them. A
@@ -111,22 +114,48 @@ export async function buildScanCatalog(packageRoot) {
111
114
  agents.push("codex");
112
115
  if (agents.length === 0)
113
116
  agents.push("claude"); // unknown defaults to claude
114
- plugins[entry.name] = { description: entry.description, agents };
117
+ // expectedVersion comes from the build-time-baked field in dist/catalog.json
118
+ // (populated by `src/build/generate-catalog.ts` from
119
+ // `plugins/<name>/.claude-plugin/plugin.json`). It MUST be baked at build
120
+ // time because `plugins/<name>/` is NOT shipped in the npm tarball
121
+ // (`package.json` `files` field allowlists only `dist/`), so reading it
122
+ // at runtime from packageRoot would silently fail for npm-installed users.
123
+ plugins[entry.name] = {
124
+ description: entry.description,
125
+ agents,
126
+ ...(typeof entry.expectedVersion === "string" && entry.expectedVersion.length > 0
127
+ ? { expectedVersion: entry.expectedVersion }
128
+ : {}),
129
+ };
130
+ }
131
+ // Hooks: the scanner reads <scope>/.claude/settings.json and matches by
132
+ // `_marker` (see state.ts scanHooks). Drift detection compares the
133
+ // registered event / matcher / if values against the hook's canonical
134
+ // settingsEvents[0] from .claude/hooks/hooks.json. We deliberately do NOT
135
+ // hash index.mjs — the user's installed index.mjs lives at <scope>/.claude/
136
+ // hooks/<name>/index.mjs and isn't part of the settings.json drift signal.
137
+ const hooksJsonPath = path.join(packageRoot, ".claude", "hooks", "hooks.json");
138
+ const hooksJsonRaw = await tryReadFile(hooksJsonPath);
139
+ const hooksJson = hooksJsonRaw ? JSON.parse(hooksJsonRaw) : {};
140
+ const settingsEventsByName = new Map();
141
+ for (const h of hooksJson.hooks ?? []) {
142
+ if (typeof h.name === "string" && h.settingsEvents?.length) {
143
+ settingsEventsByName.set(h.name, h.settingsEvents[0]);
144
+ }
115
145
  }
116
- // Hooks: runtime SHA256 of each hook's index.mjs serves as the expected
117
- // hash. If the file can't be read, leave the expectation empty so the
118
- // hook classifies as not-installed.
119
146
  const hooks = {};
120
147
  for (const entry of dist.hooks) {
121
- const hookEntry = path.join(packageRoot, ".claude", "hooks", entry.name, "index.mjs");
122
- let expectedHash = "";
123
- try {
124
- expectedHash = await sha256File(hookEntry);
125
- }
126
- catch {
127
- /* missing or unreadable hook payload; leave hash empty */
128
- }
129
- hooks[entry.name] = { description: entry.description, expectedHash };
148
+ const ev = settingsEventsByName.get(entry.name);
149
+ hooks[entry.name] = {
150
+ description: entry.description,
151
+ // Empty expectedHash flips state.ts into wildcard mode — drift judged
152
+ // purely from the structured expected* fields below, not from a
153
+ // settings-entry content hash.
154
+ expectedHash: "",
155
+ ...(typeof ev?.event === "string" ? { expectedEvent: ev.event } : {}),
156
+ ...(typeof ev?.matcher === "string" ? { expectedMatcher: ev.matcher } : {}),
157
+ ...(typeof ev?.if === "string" ? { expectedIf: ev.if } : {}),
158
+ };
130
159
  }
131
160
  return {
132
161
  workflowVersion,
package/dist/server.js CHANGED
@@ -7,13 +7,14 @@
7
7
  //
8
8
  // Public contract is anchored in docs/architecture/web-ui.md §4 (server
9
9
  // surface), §6 (data flow + types), §7 (errors).
10
+ import { spawnSync } from "node:child_process";
10
11
  import { createServer } from "node:http";
11
12
  import { Buffer } from "node:buffer";
12
13
  import { randomBytes, timingSafeEqual } from "node:crypto";
13
14
  import { readFile } from "node:fs/promises";
14
15
  import path from "node:path";
15
16
  import { buildScanCatalog } from "./scan-catalog.js";
16
- import { scanState } from "./state.js";
17
+ import { defaultExecPluginList, scanState } from "./state.js";
17
18
  // Body parsing cap. /api/apply payloads are tiny (an array of item refs);
18
19
  // 1 MiB is generously above the largest realistic batch and small enough that
19
20
  // abusive clients can't pin memory.
@@ -576,7 +577,7 @@ export async function startServer(opts) {
576
577
  return;
577
578
  }
578
579
  if (pathname === "/api/state" && method === "GET") {
579
- await routeState(opts.cwd, opts.packageRoot ?? opts.cwd, res);
580
+ await routeState(opts.cwd, opts.packageRoot ?? opts.cwd, searchParams, res);
580
581
  return;
581
582
  }
582
583
  if (pathname === "/api/apply" && method === "POST") {
@@ -705,10 +706,21 @@ export async function startServer(opts) {
705
706
  // ---------------------------------------------------------------------------
706
707
  // Route: GET /api/state
707
708
  // ---------------------------------------------------------------------------
708
- async function routeState(cwd, packageRoot, res) {
709
+ async function routeState(cwd, packageRoot, searchParams, res) {
709
710
  try {
710
711
  const catalog = await buildScanCatalog(packageRoot);
711
- const report = await scanState(cwd, catalog);
712
+ const scopes = parseScopesParam(searchParams);
713
+ // Only wire `defaultExecPluginList` if the `claude` CLI is on PATH —
714
+ // otherwise the scanner's degraded-path warning ("claude-cli-missing")
715
+ // is what we want the UI to surface. Cache the result per process so we
716
+ // don't re-`which` on every /api/state poll.
717
+ const execPluginList = isClaudeOnPath()
718
+ ? (scope) => defaultExecPluginList(scope, cwd)
719
+ : undefined;
720
+ const report = await scanState(cwd, catalog, {
721
+ ...(scopes ? { scopes } : {}),
722
+ ...(execPluginList ? { execPluginList } : {}),
723
+ });
712
724
  sendJson(res, 200, report);
713
725
  }
714
726
  catch {
@@ -717,6 +729,40 @@ async function routeState(cwd, packageRoot, res) {
717
729
  sendJson(res, 500, { error: "scan-failed" });
718
730
  }
719
731
  }
732
+ let claudeOnPathCache = null;
733
+ function isClaudeOnPath() {
734
+ if (claudeOnPathCache !== null)
735
+ return claudeOnPathCache;
736
+ const res = spawnSync("which", ["claude"], { stdio: "ignore" });
737
+ claudeOnPathCache = res.status === 0;
738
+ return claudeOnPathCache;
739
+ }
740
+ /**
741
+ * Parses the /api/state `scopes` query into a per-category map for
742
+ * `scanState`. The query string shape is comma-separated `category:scope`
743
+ * pairs, e.g. `?scopes=workflow:user,skills:project,plugins:user`. Unknown
744
+ * categories and unknown scope values are dropped (rather than throwing) so
745
+ * the scanner falls back to install defaults on garbled input — keeps the
746
+ * Web UI degraded-but-functional rather than 500-ing on a stale link.
747
+ *
748
+ * Returns `null` when the param is absent so the caller can omit `opts.scopes`
749
+ * entirely and let `scanState` apply its built-in defaults.
750
+ */
751
+ function parseScopesParam(searchParams) {
752
+ const raw = searchParams.get("scopes");
753
+ if (!raw)
754
+ return null;
755
+ const allowedCategories = new Set(["workflow", "skills", "plugins", "hooks"]);
756
+ const allowedScopes = new Set(["user", "project"]);
757
+ const out = {};
758
+ for (const pair of raw.split(",")) {
759
+ const [cat, scope] = pair.split(":");
760
+ if (allowedCategories.has(cat) && allowedScopes.has(scope)) {
761
+ out[cat] = scope;
762
+ }
763
+ }
764
+ return Object.keys(out).length > 0 ? out : null;
765
+ }
720
766
  // ---------------------------------------------------------------------------
721
767
  // Route: GET /api/catalog
722
768
  // ---------------------------------------------------------------------------
package/dist/state.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginState, StateReport } from "./api-types.js";
1
+ import type { PluginState, ScanScope, StateReport } from "./api-types.js";
2
2
  export interface Catalog {
3
3
  workflowVersion: string;
4
4
  skills: Record<string, {
@@ -18,16 +18,51 @@ export interface Catalog {
18
18
  }>;
19
19
  hooks: Record<string, {
20
20
  description: string;
21
+ /** Coarse drift signal. The current production scan-catalog still
22
+ * populates this with sha256(index.mjs) for back-compat with the v0.x
23
+ * scanner that hashed the user's installed script. The new
24
+ * settings.json-based scanner ignores it for drift comparison unless
25
+ * the catalog also exposes the structured expected* fields below. */
21
26
  expectedHash: string;
27
+ /** Settings.json event name (e.g. "PostToolUse", "Notification"). When
28
+ * set, the scanner flags drift if the on-disk settings entry registers
29
+ * under a different event. Optional; left undefined the scanner trusts
30
+ * whatever event the marker was found under. */
31
+ expectedEvent?: string;
32
+ /** Settings.json `matcher` field (e.g. "Write|Edit" for PostToolUse).
33
+ * Empty string means "no matcher" (e.g. Notification hooks). When set,
34
+ * the scanner flags drift if the on-disk value differs. */
35
+ expectedMatcher?: string;
36
+ /** Settings.json `if` field (Claude-Code-specific filter expression).
37
+ * Same drift semantics as expectedMatcher. */
38
+ expectedIf?: string;
22
39
  }>;
23
40
  }
24
41
  export interface ScanOptions {
25
- execPluginList?: () => Promise<{
42
+ /** Run `claude plugins list` for the given scope. The scope argument is
43
+ * required so server.ts can pass --user / --project through to the CLI
44
+ * per opts.scopes.plugins. Implementations may accept a zero-arg legacy
45
+ * form for back-compat but MUST honor a scope argument when given. */
46
+ execPluginList?: (scope: ScanScope) => Promise<{
26
47
  installed: any[];
27
48
  available: any[];
28
49
  }>;
29
50
  readCodexConfig?: () => Promise<string | null>;
30
51
  readCodexPluginsDir?: () => Promise<Map<string, string>>;
52
+ /** Per-category scope picker. Each field is independently routed to the
53
+ * right truth source. Defaults match the Web UI's per-column picker:
54
+ * workflow = 'project', skills = 'project',
55
+ * plugins = 'user', hooks = 'user'. */
56
+ scopes?: {
57
+ workflow?: ScanScope;
58
+ skills?: ScanScope;
59
+ plugins?: ScanScope;
60
+ hooks?: ScanScope;
61
+ };
62
+ /** Test-time HOME override. When unset the scanner reads os.homedir()
63
+ * (which itself consults process.env.HOME / USERPROFILE), so tests that
64
+ * redirect HOME via env vars also work. */
65
+ homeDir?: string;
31
66
  }
32
67
  export declare function scanState(projectRoot: string, catalog: Catalog, opts?: ScanOptions): Promise<StateReport>;
33
68
  /**
@@ -48,10 +83,12 @@ export declare function scanState(projectRoot: string, catalog: Catalog, opts?:
48
83
  * we'll need a deliberate merge policy.
49
84
  */
50
85
  export declare function mergePluginsById(records: PluginState[]): PluginState[];
51
- /** Default: run `claude plugins list --json` and `claude plugins list
52
- * --available --json`. Returns null is NOT an option here — server.ts
53
- * decides whether to pass this function based on `which claude`. */
54
- export declare function defaultExecPluginList(): Promise<{
86
+ /** Default: run `claude plugins list --json` (no scope flag — the CLI
87
+ * doesn't expose one) plus the `--available` variant, then filter the
88
+ * installed records to the requested scope (and current projectRoot for
89
+ * project-scope) client-side. Server.ts decides whether to pass this
90
+ * function based on `which claude`. */
91
+ export declare function defaultExecPluginList(scope?: ScanScope, projectRoot?: string): Promise<{
55
92
  installed: any[];
56
93
  available: any[];
57
94
  }>;