@curdx/flow 2.0.0-beta.9 → 2.0.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/cli/utils.js CHANGED
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Shared utilities for curdx-flow CLI.
3
- * Zero npm deps — only Node built-ins.
4
3
  */
5
4
 
6
5
  import { spawn, spawnSync } from "node:child_process";
7
6
  import { createInterface } from "node:readline";
8
- import { readFileSync } from "node:fs";
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, symlinkSync, lstatSync, unlinkSync, readlinkSync } from "node:fs";
9
8
  import { fileURLToPath } from "node:url";
10
9
  import { dirname, join } from "node:path";
10
+ import { homedir } from "node:os";
11
11
 
12
12
  // Read version dynamically from package.json so `curdx-flow --version` always
13
13
  // reflects the installed package version (avoids drift after npm version bumps).
@@ -88,7 +88,154 @@ export function has(cmd) {
88
88
  return res.code === 0 && res.stdout.trim().length > 0;
89
89
  }
90
90
 
91
- // ---------- Interactive prompts (readline, no deps) ----------
91
+ // ---------- @clack/prompts wrappers ----------
92
+ let _clack = null;
93
+
94
+ /**
95
+ * Lazy-load @clack/prompts (ESM module)
96
+ */
97
+ async function getClack() {
98
+ if (!_clack) {
99
+ _clack = await import("@clack/prompts");
100
+ }
101
+ return _clack;
102
+ }
103
+
104
+ /**
105
+ * Handle user cancellation gracefully
106
+ */
107
+ async function handleCancel(value, message = "Operation cancelled") {
108
+ const clack = await getClack();
109
+ if (clack.isCancel(value)) {
110
+ clack.cancel(message);
111
+ process.exit(0);
112
+ }
113
+ return false;
114
+ }
115
+
116
+ /**
117
+ * Single-select prompt with arrow key navigation
118
+ * @param {Object} options
119
+ * @param {string} options.message - Question to ask
120
+ * @param {Array} options.options - Array of {value, label, hint?}
121
+ * @param {any} [options.initialValue] - Default selected value
122
+ * @returns {Promise<any>} Selected value
123
+ */
124
+ export async function select(options) {
125
+ const clack = await getClack();
126
+ const result = await clack.select({
127
+ message: options.message,
128
+ options: options.options,
129
+ initialValue: options.initialValue,
130
+ });
131
+ await handleCancel(result);
132
+ return result;
133
+ }
134
+
135
+ /**
136
+ * Multi-select prompt with checkboxes (arrow keys + space to toggle)
137
+ * @param {Object} options
138
+ * @param {string} options.message - Question to ask
139
+ * @param {Array} options.options - Array of {value, label, hint?}
140
+ * @param {Array} [options.initialValues] - Default selected values
141
+ * @param {boolean} [options.required] - Whether at least one must be selected
142
+ * @returns {Promise<Array>} Array of selected values
143
+ */
144
+ export async function multiselectClack(options) {
145
+ const clack = await getClack();
146
+ const result = await clack.multiselect({
147
+ message: options.message,
148
+ options: options.options,
149
+ initialValues: options.initialValues || [],
150
+ required: options.required !== undefined ? options.required : false,
151
+ });
152
+ await handleCancel(result);
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * Text input prompt with validation
158
+ * @param {Object} options
159
+ * @param {string} options.message - Question to ask
160
+ * @param {string} [options.placeholder] - Placeholder text
161
+ * @param {string} [options.defaultValue] - Default value
162
+ * @param {Function} [options.validate] - Validation function (return string for error, undefined for success)
163
+ * @returns {Promise<string>} User input
164
+ */
165
+ export async function text(options) {
166
+ const clack = await getClack();
167
+ const result = await clack.text({
168
+ message: options.message,
169
+ placeholder: options.placeholder,
170
+ defaultValue: options.defaultValue,
171
+ validate: options.validate,
172
+ });
173
+ await handleCancel(result);
174
+ return result;
175
+ }
176
+
177
+ /**
178
+ * Spinner for async operations
179
+ * @returns {Promise<Object>} Spinner controller
180
+ */
181
+ export async function spinner() {
182
+ const clack = await getClack();
183
+ return clack.spinner();
184
+ }
185
+
186
+ /**
187
+ * Display intro message
188
+ */
189
+ export async function intro(message) {
190
+ const clack = await getClack();
191
+ clack.intro(message);
192
+ }
193
+
194
+ /**
195
+ * Display outro message
196
+ */
197
+ export async function outro(message) {
198
+ const clack = await getClack();
199
+ clack.outro(message);
200
+ }
201
+
202
+ /**
203
+ * Display a note/info box
204
+ */
205
+ export async function note(message, title) {
206
+ const clack = await getClack();
207
+ clack.note(message, title);
208
+ }
209
+
210
+ // ---------- Config file helpers ----------
211
+ const CONFIG_DIR = join(homedir(), ".claude");
212
+ const CONFIG_FILE = join(CONFIG_DIR, "curdx-flow-config.json");
213
+
214
+ /**
215
+ * Read curdx-flow config from ~/.claude/curdx-flow-config.json
216
+ */
217
+ export function readConfig() {
218
+ if (!existsSync(CONFIG_FILE)) {
219
+ return {};
220
+ }
221
+ try {
222
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
223
+ } catch {
224
+ return {};
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Write curdx-flow config to ~/.claude/curdx-flow-config.json
230
+ */
231
+ export function writeConfig(config) {
232
+ if (!existsSync(CONFIG_DIR)) {
233
+ mkdirSync(CONFIG_DIR, { recursive: true });
234
+ }
235
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
236
+ }
237
+
238
+ // ---------- Interactive prompts (readline, legacy) ----------
92
239
  /**
93
240
  * Ask user a yes/no question. Default applies on empty input.
94
241
  */
@@ -108,39 +255,6 @@ export function confirm(message, defaultYes = true) {
108
255
  });
109
256
  }
110
257
 
111
- /**
112
- * Ask user to pick from a list. Returns selected value or null if aborted.
113
- */
114
- export function select(message, choices, defaultIndex = 0) {
115
- return new Promise((resolve) => {
116
- console.log(`${color.cyan("?")} ${message}`);
117
- choices.forEach((ch, i) => {
118
- const marker = i === defaultIndex ? color.green("▸") : " ";
119
- console.log(` ${marker} ${color.bold(String(i + 1))}. ${ch.label}`);
120
- });
121
-
122
- const rl = createInterface({
123
- input: process.stdin,
124
- output: process.stdout,
125
- });
126
- rl.question(
127
- ` ${color.dim(`(default: ${defaultIndex + 1}, q to abort) `)}`,
128
- (ans) => {
129
- rl.close();
130
- const v = ans.trim().toLowerCase();
131
- if (v === "q") return resolve(null);
132
- if (v === "") return resolve(choices[defaultIndex].value);
133
- const n = parseInt(v, 10);
134
- if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
135
- return resolve(choices[n - 1].value);
136
- }
137
- console.log(color.yellow(" (invalid, using default)"));
138
- resolve(choices[defaultIndex].value);
139
- }
140
- );
141
- });
142
- }
143
-
144
258
  /**
145
259
  * Multi-select (checkbox-style via comma-separated input).
146
260
  * Returns array of selected values.
@@ -199,47 +313,190 @@ export function claudeVersion() {
199
313
  return m ? m[1] : res.stdout.trim().split("\n")[0];
200
314
  }
201
315
 
202
- /** List installed plugins via `claude plugin list`. Returns array of { name, version, status }. */
316
+ /**
317
+ * List installed plugins. Prefers the structured `claude plugin list --json`
318
+ * output (stable machine-readable format; confirmed present in claude
319
+ * 2.1.117+). Falls back to parsing the human-readable stream-text output
320
+ * for older CLI versions, but warns that parser is brittle.
321
+ *
322
+ * Returns array of { id, name, marketplaceId, version, status, scope }.
323
+ */
203
324
  export function listPlugins() {
204
- const res = runSync("claude", ["plugin", "list"]);
205
- if (res.code !== 0) return [];
206
- const out = res.stdout;
207
- const plugins = [];
208
- // Parse format like:
325
+ // Preferred: structured JSON output.
326
+ const j = runSync("claude", ["plugin", "list", "--json"]);
327
+ if (j.code === 0 && j.stdout.trim().startsWith("[")) {
328
+ try {
329
+ const arr = JSON.parse(j.stdout);
330
+ return arr.map((p) => ({
331
+ // id has form "name@marketplace" — name is stable for dedup/lookup.
332
+ id: String(p.id || ""),
333
+ name: String(p.id || "").split("@")[0],
334
+ marketplaceId: String(p.id || "").split("@")[1] || undefined,
335
+ version: p.version,
336
+ status: p.enabled === false ? "disabled" : "enabled",
337
+ scope: p.scope,
338
+ raw: JSON.stringify(p),
339
+ }));
340
+ } catch {
341
+ // JSON parse failed — fall through to legacy text parser.
342
+ }
343
+ }
344
+
345
+ // Legacy fallback: parse the human-readable format.
209
346
  // ❯ curdx-flow@curdx-flow-marketplace
210
347
  // Version: 1.1.1
211
- // Scope: user
212
348
  // Status: ✔ enabled
213
- const blocks = out.split(/\n\s*❯\s*/).slice(1);
349
+ // Fragile matches unicode markers. Kept only for older claude CLIs.
350
+ const res = runSync("claude", ["plugin", "list"]);
351
+ if (res.code !== 0) return [];
352
+ const plugins = [];
353
+ const blocks = res.stdout.split(/\n\s*❯\s*/).slice(1);
214
354
  for (const block of blocks) {
215
355
  const lines = block.split("\n");
216
- const name = lines[0].trim().split("@")[0];
356
+ const id = lines[0].trim();
357
+ const name = id.split("@")[0];
217
358
  const version = (block.match(/Version:\s*(\S+)/) || [])[1];
218
- const status = block.includes("✔") ? "enabled" : block.includes("✘") ? "failed" : "unknown";
219
- plugins.push({ name, version, status, raw: block });
359
+ const status = block.includes("✔")
360
+ ? "enabled"
361
+ : block.includes("✘")
362
+ ? "failed"
363
+ : "unknown";
364
+ plugins.push({ id, name, marketplaceId: id.split("@")[1], version, status, raw: block });
220
365
  }
221
366
  return plugins;
222
367
  }
223
368
 
224
- /** List MCPs via `claude mcp list`. Returns array of { name, status }. */
369
+ /**
370
+ * List configured Claude Code plugin marketplaces.
371
+ * Returns array of { name, source, repo, path } when `--json` is supported.
372
+ */
373
+ export function listPluginMarketplaces() {
374
+ const j = runSync("claude", ["plugin", "marketplace", "list", "--json"]);
375
+ if (j.code === 0 && j.stdout.trim().startsWith("[")) {
376
+ try {
377
+ return JSON.parse(j.stdout);
378
+ } catch {
379
+ return [];
380
+ }
381
+ }
382
+ return [];
383
+ }
384
+
385
+ /**
386
+ * Read the user-level MCP registrations from ~/.claude.json. These are the
387
+ * MCPs the user added manually via `claude mcp add …` — distinct from
388
+ * plugin-bundled MCPs (which live in plugin.json).
389
+ *
390
+ * Returns a Map keyed by server name with the raw config object. Returns
391
+ * an empty Map if the file is missing / unreadable / has no mcpServers
392
+ * section — all of which are normal states and not errors.
393
+ */
394
+ export function readUserMcpConfig() {
395
+ try {
396
+ const path = join(HOME, ".claude.json");
397
+ if (!existsSync(path)) return new Map();
398
+ const cfg = JSON.parse(readFileSync(path, "utf-8"));
399
+ const servers = cfg?.mcpServers || {};
400
+ return new Map(Object.entries(servers));
401
+ } catch {
402
+ return new Map();
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Given the output of listMcps() and a user-level MCP config map, find
408
+ * MCPs that are registered BOTH as user-level AND as plugin-bundled.
409
+ * The plugin-bundled form shows up as `plugin:<plugin>:<name>` in
410
+ * listMcps output, so a user-level "context7" and a plugin-level
411
+ * "plugin:curdx-flow:context7" are a duplicate pair.
412
+ *
413
+ * Returns array of { name, userConfig, pluginEntry }.
414
+ */
415
+ export function findDuplicateMcps(mcps, userConfig) {
416
+ const duplicates = [];
417
+ for (const m of mcps) {
418
+ // Only look at plugin-prefixed entries — they're the reference for
419
+ // what's bundled. Check if user has their own non-prefixed version.
420
+ if (m.plugin && userConfig.has(m.name)) {
421
+ duplicates.push({
422
+ name: m.name,
423
+ userConfig: userConfig.get(m.name),
424
+ pluginEntry: m,
425
+ });
426
+ }
427
+ }
428
+ return duplicates;
429
+ }
430
+
431
+ /**
432
+ * List MCP servers registered with the `claude` CLI. Returns array of
433
+ * { name, plugin, fullName, status, command }
434
+ * where `plugin` is set when the MCP came from a plugin (real name is
435
+ * `plugin:<plugin>:<mcp>`), `name` is the trailing segment, and `fullName`
436
+ * is the original as reported by claude.
437
+ *
438
+ * Fixture captured from `claude mcp list` (2.1.117):
439
+ * Checking MCP server health…
440
+ *
441
+ * plugin:curdx-flow:context7: npx -y @upstash/context7-mcp@latest - ✓ Connected
442
+ * context7: npx -y @upstash/context7-mcp --api-key ... - ✓ Connected
443
+ * claude.ai Gmail: https://gmailmcp... - ✓ Connected
444
+ *
445
+ * `claude mcp list --json` does not exist on 2.1.117 (verified), so this
446
+ * parser is the primary path. It is fixture-tested in test/utils.test.js
447
+ * so format regressions get caught in CI.
448
+ */
225
449
  export function listMcps() {
226
450
  const res = runSync("claude", ["mcp", "list"]);
227
451
  if (res.code !== 0) return [];
228
- const lines = res.stdout.split("\n");
452
+ return parseMcpList(res.stdout);
453
+ }
454
+
455
+ /** Exported for testing against a fixed input. */
456
+ export function parseMcpList(output) {
229
457
  const mcps = [];
230
- for (const line of lines) {
231
- // Rough parse — adjust if format differs
232
- const m = line.match(/^\s*([a-z0-9-]+)\s*[:\-]/i);
233
- if (m) mcps.push({ name: m[1], status: "registered" });
458
+ for (const raw of output.split("\n")) {
459
+ const line = raw.trimEnd();
460
+ if (!line) continue;
461
+ // skip the health-check header line
462
+ if (line.startsWith("Checking") || line.startsWith("checking")) continue;
463
+ // Expected format: "<fullName>: <command-or-url> - <status>"
464
+ // fullName may itself contain colons when prefixed with "plugin:<p>:<m>".
465
+ // Match from the end to find the status sentinel " - ", then split off
466
+ // the name at the first ": " after the identifier prefix.
467
+ const statusSplit = line.lastIndexOf(" - ");
468
+ if (statusSplit === -1) continue;
469
+ const statusRaw = line.slice(statusSplit + 3).trim();
470
+ const beforeStatus = line.slice(0, statusSplit);
471
+ // Find the first ": " that separates name from command. Note the space
472
+ // after the colon — this disambiguates from the colons inside
473
+ // "plugin:foo:bar".
474
+ const nameSplit = beforeStatus.indexOf(": ");
475
+ if (nameSplit === -1) continue;
476
+ const fullName = beforeStatus.slice(0, nameSplit).trim();
477
+ const command = beforeStatus.slice(nameSplit + 2).trim();
478
+
479
+ let plugin = null;
480
+ let name = fullName;
481
+ if (fullName.startsWith("plugin:")) {
482
+ const parts = fullName.split(":");
483
+ if (parts.length >= 3) {
484
+ plugin = parts[1];
485
+ name = parts.slice(2).join(":");
486
+ }
487
+ }
488
+
489
+ const status = /Connected|✓/.test(statusRaw)
490
+ ? "connected"
491
+ : /Failed|✗/.test(statusRaw)
492
+ ? "failed"
493
+ : "unknown";
494
+
495
+ mcps.push({ name, plugin, fullName, status, command });
234
496
  }
235
497
  return mcps;
236
498
  }
237
499
 
238
- // ---------- Paths ----------
239
- export function pluginCacheDir(pluginName = "curdx-flow", marketplace = "curdx-flow-marketplace") {
240
- return `${process.env.HOME}/.claude/plugins/cache/${marketplace}/${pluginName}`;
241
- }
242
-
243
500
  // ---------- Runtime PATH guards (bun / uv) ----------
244
501
  // claude-mem hard-codes `command: "bun"` in its .mcp.json, but bun installs to
245
502
  // ~/.bun/bin which is not on PATH when Claude Code spawns MCP servers
@@ -247,10 +504,13 @@ export function pluginCacheDir(pluginName = "curdx-flow", marketplace = "curdx-f
247
504
  // detection + self-healing: create a symlink to the user-level bun install
248
505
  // in a PATH-visible directory.
249
506
 
250
- import { existsSync, mkdirSync, symlinkSync, lstatSync, unlinkSync, readlinkSync } from "node:fs";
251
- // `join` already imported at the top of this file.
507
+ // Note: existsSync, mkdirSync, symlinkSync, lstatSync, unlinkSync, readlinkSync, homedir, join
508
+ // are already imported at the top of this file.
252
509
 
253
- const HOME = process.env.HOME || "";
510
+ // os.homedir() is sourced from the OS-level user record and works even
511
+ // when $HOME is empty (non-login shells, some CI containers). See the
512
+ // same rationale in cli/protocols.js.
513
+ const HOME = homedir();
254
514
 
255
515
  /** Candidate bun install locations (priority order) */
256
516
  const BUN_CANDIDATES = [
@@ -15,7 +15,7 @@ Execute spec tasks per tasks.md. Select the best execution strategy based on arg
15
15
  ## Step 1: Preflight Checks
16
16
 
17
17
  ```bash
18
- [ ! -d ".flow" ] && { echo " Not a CurDX-Flow project. Run /curdx-flow:init first"; exit 1; }
18
+ [ ! -d ".flow" ] && { echo " Not a CurDX-Flow project. Run /curdx-flow:init first"; exit 1; }
19
19
 
20
20
  ARGS="$ARGUMENTS"
21
21
  SPEC_NAME=""
@@ -35,10 +35,10 @@ for arg in $ARGS; do
35
35
  done
36
36
 
37
37
  [ -z "$SPEC_NAME" ] && SPEC_NAME=$(cat .flow/.active-spec 2>/dev/null)
38
- [ -z "$SPEC_NAME" ] && { echo " No active spec. Run /curdx-flow:start first"; exit 1; }
38
+ [ -z "$SPEC_NAME" ] && { echo " No active spec. Run /curdx-flow:start first"; exit 1; }
39
39
 
40
40
  DIR=".flow/specs/$SPEC_NAME"
41
- [ ! -f "$DIR/tasks.md" ] && { echo " Missing tasks.md. Run /curdx-flow:spec first (or /curdx-flow:spec --phase=tasks to rebuild just the tasks phase)"; exit 1; }
41
+ [ ! -f "$DIR/tasks.md" ] && { echo " Missing tasks.md. Run /curdx-flow:spec first (or /curdx-flow:spec --phase=tasks to rebuild just the tasks phase)"; exit 1; }
42
42
  ```
43
43
 
44
44
  ## Step 2: Parse Task Characteristics from tasks.md
package/commands/init.md CHANGED
@@ -71,9 +71,20 @@ Append (if not already present):
71
71
 
72
72
  ### Step 5: Health Check
73
73
 
74
- Run `npx @curdx/flow doctor` (or inline its checks) to verify:
75
- - 2 bundled MCPs started (context7 / sequential-thinking)
76
- - Recommended plugins status (pua / claude-mem / frontend-design / chrome-devtools-mcp)
74
+ Do NOT shell out to a new terminal for this step — you are already inside
75
+ Claude Code. Verify inline via the information the plugin already has:
76
+
77
+ - Read `~/.claude/plugins/data/curdx-flow/.deps-checked` (optional — the
78
+ SessionStart hook already refreshes this once per day).
79
+ - If the user asks for the full report, suggest they run
80
+ `npx @curdx/flow doctor` in a separate terminal — don't try to spawn
81
+ it from inside the Claude Code session (output won't render cleanly
82
+ and the user has to alt-tab to see it).
83
+
84
+ Items the CLI doctor covers (for user reference):
85
+ - 2 bundled MCPs (context7 / sequential-thinking) — visible in `claude mcp list`
86
+ - 4 recommended plugins (pua / claude-mem / frontend-design / chrome-devtools-mcp)
87
+ - Runtime PATH guards for `bun` / `uv` (relevant only when claude-mem is installed)
77
88
 
78
89
  ### Step 6: Prompt Next Steps
79
90
 
package/commands/start.md CHANGED
@@ -32,18 +32,40 @@ Entry point for every feature. Works in four modes depending on flags and existi
32
32
 
33
33
  ## Flag parsing
34
34
 
35
- ```bash
36
- FLAG_RESUME=$(echo "$ARGUMENTS" | grep -q -- '--resume' && echo 1 || echo 0)
37
- FLAG_LIST=$(echo "$ARGUMENTS" | grep -q -- '--list' && echo 1 || echo 0)
38
- FLAG_MODE=$(echo "$ARGUMENTS" | grep -oP -- '--mode=\K[^\s]+' || echo "standard")
39
-
40
- # Strip flags from ARGUMENTS to leave the positional args
41
- POS=$(echo "$ARGUMENTS" | sed -E 's/--[a-z-]+(=[^ ]+)?//g' | xargs)
42
- SPEC_NAME=$(echo "$POS" | awk '{print $1}')
43
- GOAL=$(echo "$POS" | awk '{$1=""; print $0}' | sed 's/^"//; s/"$//' | xargs)
44
- ```
45
-
46
- Mode must be `fast`, `standard`, or `enterprise`. Invalid → default to `standard` with a warning.
35
+ **Do not shell-split `$ARGUMENTS`.** It is a user-supplied string that may
36
+ contain quoted substrings with spaces, `$`-signs, or embedded quotes.
37
+ `xargs`, naive `awk`, and `sed`-based quote stripping all mis-parse at
38
+ least one of those cases (e.g. `my-feature "Fix user's login bug"` breaks
39
+ `xargs: unmatched quote`). Parse the string as a model task instead:
40
+
41
+ 1. **Flags** (order-independent, each is self-delimited):
42
+ - `--resume` / `--list` boolean presence
43
+ - `--mode=<fast|standard|enterprise>` value after `=`
44
+ Detect each with a single regex over the full `$ARGUMENTS` string and
45
+ remove the matched span from your working copy. Flags not in the list
46
+ above are errors surface them to the user.
47
+
48
+ 2. **Positional args** (after flags removed):
49
+ - First whitespace-separated token → `SPEC_NAME` (kebab-case `[a-z0-9-]+`).
50
+ - Remainder of the string, trimmed and with one layer of outer `"..."`
51
+ or `'...'` quotes stripped → `GOAL`. Preserve inner quotes as-is.
52
+
53
+ 3. If `SPEC_NAME` does not match `^[a-z0-9][a-z0-9-]*$` (per
54
+ `schemas/spec-state.schema.json`), stop and ask the user to pick a
55
+ valid kebab-case name.
56
+
57
+ Mode must be `fast`, `standard`, or `enterprise`. Invalid → default to
58
+ `standard` with a warning.
59
+
60
+ Example inputs and their parse:
61
+
62
+ | `$ARGUMENTS` | SPEC_NAME | GOAL | flags |
63
+ |-------------------------------------------------|--------------|-------------------------------|---------------|
64
+ | `my-feature "Add JWT auth"` | `my-feature` | `Add JWT auth` | — |
65
+ | `my-feature --mode=fast "Add JWT auth"` | `my-feature` | `Add JWT auth` | mode=fast |
66
+ | `my-feature "Fix user's login bug"` | `my-feature` | `Fix user's login bug` | — |
67
+ | `--list` | — | — | list=true |
68
+ | `--resume` | — | — | resume=true |
47
69
 
48
70
  ## Branch logic
49
71
 
package/hooks/hooks.json CHANGED
@@ -8,10 +8,9 @@
8
8
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/session-start.sh"
9
9
  }
10
10
  ]
11
- }
12
- ],
13
- "InstructionsLoaded": [
11
+ },
14
12
  {
13
+ "matcher": "startup|clear|compact",
15
14
  "hooks": [
16
15
  {
17
16
  "type": "command",
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env bash
2
- # CurDX-Flow InstructionsLoaded Hook
2
+ # CurDX-Flow SessionStart baseline injection
3
3
  # Injects the L1 baseline (Karpathy 4 principles + mandatory tool rules + 3 red lines)
4
- # into every session after CLAUDE.md is loaded.
4
+ # as additionalContext at every session boot (startup, /clear, post-compact).
5
5
  #
6
- # This is what makes the baseline "always on" — it doesn't rely on CLAUDE.md being
7
- # present in every project, and it survives context compaction.
6
+ # Wired under SessionStart rather than InstructionsLoaded because Claude Code's
7
+ # InstructionsLoaded event is observability-only its hook schema rejects
8
+ # hookSpecificOutput / additionalContext. SessionStart with matcher
9
+ # "startup|clear|compact" gives the same "baseline is always on, even after
10
+ # compaction" property while staying within the supported schema.
8
11
 
9
12
  set -u
10
13
 
@@ -46,7 +49,7 @@ CONTEXT='## CurDX-Flow Mind Baseline (L1 — always on)
46
49
  # Emit JSON with safe encoding
47
50
  if command -v python3 >/dev/null 2>&1; then
48
51
  ESCAPED="$(printf '%s' "$CONTEXT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')"
49
- printf '{"hookSpecificOutput":{"hookEventName":"InstructionsLoaded","additionalContext":%s}}\n' "$ESCAPED"
52
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}\n' "$ESCAPED"
50
53
  fi
51
54
 
52
55
  exit 0
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "2.0.0-beta.9",
3
+ "version": "2.0.0",
4
4
  "description": "CLI installer for CurDX-Flow — AI engineering workflow meta-framework for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
7
- "flow": "bin/curdx-flow.js",
8
7
  "curdx-flow": "bin/curdx-flow.js"
9
8
  },
10
9
  "scripts": {
11
- "prepublishOnly": "node bin/curdx-flow.js --version"
10
+ "test": "node --test test/*.test.js",
11
+ "prepublishOnly": "node --test test/*.test.js && node bin/curdx-flow.js --version"
12
12
  },
13
13
  "files": [
14
14
  "bin/",
@@ -44,5 +44,9 @@
44
44
  "installer",
45
45
  "ai-engineering",
46
46
  "curdx-flow"
47
- ]
47
+ ],
48
+ "dependencies": {
49
+ "@clack/prompts": "^0.8.2",
50
+ "picocolors": "^1.1.1"
51
+ }
48
52
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: brownfield-index
3
- description: Invoke when the user is new to an unfamiliar / legacy / brownfield codebase and wants a structural understanding — module map, component inventory, API surface, data flow. Triggers on "legacy code", "brownfield", "unfamiliar", "new to this code", "new to this project", "just joined", "inherited codebase", "explore codebase", "understand structure", "index code", "map modules", "tour", "onboard", "what is this project", "老代码", "棕地", "不熟悉", "新接手", "新项目", "代码探索", "结构理解", "索引", "模块地图", "先了解下", "先熟悉一下".
3
+ description: Invoke when the user is new to an unfamiliar / legacy / brownfield codebase and wants a structural understanding — module map, component inventory, API surface, data flow. Triggers on "legacy code", "brownfield", "unfamiliar", "new to this code", "new to this project", "just joined", "inherited codebase", "explore codebase", "understand structure", "index code", "map modules", "tour", "onboard", "what is this project".
4
4
  allowed-tools: [Read, Grep, Glob, Bash]
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: browser-qa
3
- description: Invoke when the user wants to test a UI/frontend in a real browser — accessibility, performance, console errors, network traffic, visual regression. Triggers on "browser test", "test in browser", "UI test", "e2e test", "frontend test", "accessibility", "a11y", "WCAG", "lighthouse", "performance audit", "console error", "network request", "cross-browser", "responsive", "mobile test", "visual regression", "screenshot", "浏览器测试", "UI 测试", "可访问性", "性能", "控制台错误", "截图", "移动端", "兼容性", "在浏览器里跑", "看看 console".
3
+ description: Invoke when the user wants to test a UI/frontend in a real browser — accessibility, performance, console errors, network traffic, visual regression. Triggers on "browser test", "test in browser", "UI test", "e2e test", "frontend test", "accessibility", "a11y", "WCAG", "lighthouse", "performance audit", "console error", "network request", "cross-browser", "responsive", "mobile test", "visual regression", "screenshot".
4
4
  allowed-tools: [Read, Write, Bash, Grep, Glob, WebFetch]
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: epic
3
- description: Invoke when user wants to break a large feature into multiple smaller specs with a dependency graph. Triggers on "epic", "big feature", "too big", "decompose", "break down", "break into", "split into", "multi-spec", "multiple features", "sub-features", "vertical slice", "parent feature", "large scope", "大功能", "太大", "拆分", "拆解", "分解", "多个规格", "子功能", "垂直切片", "史诗级", "分几个做", "一个 sprint 做不完", "需要拆".
3
+ description: Invoke when user wants to break a large feature into multiple smaller specs with a dependency graph. Triggers on "epic", "big feature", "too big", "decompose", "break down", "break into", "split into", "multi-spec", "multiple features", "sub-features", "vertical slice", "parent feature", "large scope", "won't fit in one sprint", "needs splitting".
4
4
  allowed-tools: [Read, Write, Grep, Glob, Bash]
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: security-audit
3
- description: Invoke when the user wants a security review — OWASP Top 10, STRIDE threat modeling, credential handling, injection, secrets, sensitive data handling. Triggers on "security", "auth", "authentication", "credential", "password", "secret", "API key", "token", "OWASP", "STRIDE", "CVE", "vulnerability", "injection", "XSS", "CSRF", "SSRF", "SQL injection", "hardcoded secret", "sensitive data", "leak", "安全", "认证", "凭证", "密码", "密钥", "令牌", "漏洞", "注入", "硬编码", "敏感数据", "泄露", "API key 会不会泄", "有没有安全问题".
3
+ description: Invoke when the user wants a security review — OWASP Top 10, STRIDE threat modeling, credential handling, injection, secrets, sensitive data handling. Triggers on "security", "auth", "authentication", "credential", "password", "secret", "API key", "token", "OWASP", "STRIDE", "CVE", "vulnerability", "injection", "XSS", "CSRF", "SSRF", "SQL injection", "hardcoded secret", "sensitive data", "leak", "will my API key leak", "is this safe".
4
4
  allowed-tools: [Read, Grep, Glob, Bash, WebSearch]
5
5
  ---
6
6