@curdx/flow 3.2.0 → 3.3.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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to `@curdx/flow` are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/) and the project follows [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## 3.3.0 — 2026-04-27
6
+
7
+ ### Added
8
+
9
+ - **CLAUDE.md sync** — every successful `install` / `update` / `uninstall` now rewrites a small managed block in `~/.claude/CLAUDE.md` so Claude Code has session-start knowledge of which tools are installed and when to use each. The block lives between `<!-- BEGIN @curdx/flow v1 -->` / `<!-- END @curdx/flow v1 -->` markers; everything outside is preserved verbatim. Uninstalling all managed items removes the block entirely.
10
+ - **`Pkg.whenToUse` and `Pkg.slashNamespace`** — two new optional registry fields. `whenToUse` is the English trigger fragment shown in the CLAUDE.md "Available tools/plugins" list (e.g. "auto-fires on 2+ failures..."). `slashNamespace` is the slash invocation pattern (e.g. `/pua:*`) — only set on plugins that expose one. Both populated for the six bundled items, sourced from each upstream's own documentation.
11
+ - **Conditional Rules section** — the block's `Rules:` lines are emitted only for currently-installed tools, so the block never advises Claude to use a tool that isn't there. The "plan first" rule names whichever planners (`sequential-thinking`, `claude-mem`) are installed.
12
+ - **`--no-claude-md` flag and `CURDX_FLOW_NO_CLAUDE_MD` env var** — opt out of the CLAUDE.md sync (CI, locked-down filesystems, or users who prefer to manage CLAUDE.md by hand).
13
+
14
+ ### Notes
15
+
16
+ - Sync is **safe by default**: writes are atomic (tmp + `fs.rename`), partial CLAUDE.md changes are impossible, and a failed sync prints a warning but never aborts a successful install.
17
+ - Forward-compatible: the BEGIN/END regex matches any `v\d+` suffix, so a future `v2` block format will silently replace any pre-existing `v1` block.
18
+ - Block content is always English regardless of `--lang`. CLAUDE.md's audience is Claude itself; English keeps instructions stable and avoids diff churn from alternating language runs.
19
+
5
20
  ## 3.2.0 — 2026-04-26
6
21
 
7
22
  ### Added
package/README.md CHANGED
@@ -34,6 +34,28 @@ npx @curdx/flow --lang en # override language
34
34
  | `sequential-thinking` | mcp | `@modelcontextprotocol/server-sequential-thinking` |
35
35
  | `context7` | mcp | HTTP — `https://mcp.context7.com/mcp` (optional API key) |
36
36
 
37
+ ## What it writes to your filesystem
38
+
39
+ After every successful `install` / `update` / `uninstall`, flow keeps a short managed block in your global `~/.claude/CLAUDE.md` so Claude Code knows at session start which tools are installed and when to use them. The block looks like:
40
+
41
+ ```
42
+ <!-- BEGIN @curdx/flow v1 -->
43
+ ## Tool Usage
44
+
45
+ Available tools/plugins:
46
+ - pua (v3.0.0) — `/pua:*` — auto-fires on 2+ failures or user frustration; ...
47
+ - ...
48
+
49
+ Rules:
50
+ - Do not call every tool by default; ...
51
+ - ...
52
+
53
+ Run `npx @curdx/flow` to install / update / uninstall.
54
+ <!-- END @curdx/flow v1 -->
55
+ ```
56
+
57
+ Anything outside the BEGIN/END markers is preserved verbatim — flow only ever rewrites or removes the block itself. Uninstalling all managed items removes the block entirely. Pass `--no-claude-md` (or set `CURDX_FLOW_NO_CLAUDE_MD=1`) to opt out.
58
+
37
59
  ## Requirements
38
60
 
39
61
  - Node.js >= 20.12
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import * as p8 from "@clack/prompts";
4
+ import * as p9 from "@clack/prompts";
5
5
  import { defineCommand, runMain } from "citty";
6
6
 
7
7
  // src/ui/language.ts
@@ -66,7 +66,12 @@ var messages = {
66
66
  "chrome.prereqNode": "\u9700\u8981 Node.js >= 20.19\uFF0C\u5F53\u524D\u7248\u672C {current}",
67
67
  "chrome.prereqChrome": "\u9700\u8981\u672C\u673A\u5DF2\u5B89\u88C5 Chrome\uFF08chrome-devtools-mcp \u4F1A\u8C03\u7528\u672C\u5730\u6D4F\u89C8\u5668\uFF09",
68
68
  "reinstall.uninstalling": "\u5148\u5378\u8F7D\u65E7\u7248\u672C\u2026",
69
- "reinstall.installing": "\u5B89\u88C5\u65B0\u7248\u672C\u2026"
69
+ "reinstall.installing": "\u5B89\u88C5\u65B0\u7248\u672C\u2026",
70
+ "claudeMd.synced": "CLAUDE.md \u5DF2\u66F4\u65B0\uFF08{path}\uFF09",
71
+ "claudeMd.unchanged": "CLAUDE.md \u5DF2\u662F\u6700\u65B0",
72
+ "claudeMd.removed": "\u5DF2\u4ECE CLAUDE.md \u79FB\u9664 @curdx/flow \u533A\u5757",
73
+ "claudeMd.skipped": "\u5DF2\u8DF3\u8FC7 CLAUDE.md \u540C\u6B65\uFF08--no-claude-md\uFF09",
74
+ "claudeMd.failed": "CLAUDE.md \u540C\u6B65\u5931\u8D25\uFF1A{error}"
70
75
  };
71
76
  var zh_default = messages;
72
77
 
@@ -129,7 +134,12 @@ var messages2 = {
129
134
  "chrome.prereqNode": "Requires Node.js >= 20.19 (current: {current})",
130
135
  "chrome.prereqChrome": "Requires Chrome installed locally (chrome-devtools-mcp drives the local browser)",
131
136
  "reinstall.uninstalling": "Uninstalling old version\u2026",
132
- "reinstall.installing": "Installing new version\u2026"
137
+ "reinstall.installing": "Installing new version\u2026",
138
+ "claudeMd.synced": "CLAUDE.md updated ({path})",
139
+ "claudeMd.unchanged": "CLAUDE.md already up to date",
140
+ "claudeMd.removed": "Removed @curdx/flow block from CLAUDE.md",
141
+ "claudeMd.skipped": "Skipped CLAUDE.md sync (--no-claude-md)",
142
+ "claudeMd.failed": "CLAUDE.md sync failed: {error}"
133
143
  };
134
144
  var en_default = messages2;
135
145
 
@@ -178,10 +188,10 @@ async function initLanguage(override) {
178
188
  }
179
189
 
180
190
  // src/ui/menu.ts
181
- import * as p7 from "@clack/prompts";
191
+ import * as p8 from "@clack/prompts";
182
192
 
183
193
  // src/flows/install.ts
184
- import * as p3 from "@clack/prompts";
194
+ import * as p4 from "@clack/prompts";
185
195
  import pc from "picocolors";
186
196
 
187
197
  // src/runner/state.ts
@@ -200,15 +210,15 @@ async function run(cmd, args) {
200
210
  stderr: result.stderr
201
211
  };
202
212
  }
203
- async function runStreaming(cmd, args, log4) {
204
- log4.message(`$ ${cmd} ${args.join(" ")}`);
213
+ async function runStreaming(cmd, args, log5) {
214
+ log5.message(`$ ${cmd} ${args.join(" ")}`);
205
215
  const proc = x(cmd, args, { throwOnError: false });
206
216
  let stdout = "";
207
217
  for await (const line of proc) {
208
218
  const trimmed = line.replace(/\r?\n$/, "");
209
219
  if (trimmed.length > 0) {
210
220
  stdout += trimmed + "\n";
211
- log4.message(trimmed);
221
+ log5.message(trimmed);
212
222
  }
213
223
  }
214
224
  const finished = await proc;
@@ -251,15 +261,15 @@ async function listPlugins(force = false) {
251
261
  }
252
262
  try {
253
263
  const arr = JSON.parse(res.stdout);
254
- pluginCache = arr.map((p9) => {
255
- const [name = p9.id, marketplace = ""] = p9.id.split("@");
264
+ pluginCache = arr.map((p10) => {
265
+ const [name = p10.id, marketplace = ""] = p10.id.split("@");
256
266
  return {
257
- id: p9.id,
267
+ id: p10.id,
258
268
  name,
259
269
  marketplace,
260
- version: p9.version,
261
- scope: p9.scope,
262
- enabled: p9.enabled
270
+ version: p10.version,
271
+ scope: p10.scope,
272
+ enabled: p10.enabled
263
273
  };
264
274
  });
265
275
  } catch {
@@ -305,7 +315,7 @@ async function listMcp(force = false) {
305
315
  }
306
316
  async function isPluginInstalled(id) {
307
317
  const list = await listPlugins();
308
- return list.some((p9) => p9.id === id);
318
+ return list.some((p10) => p10.id === id);
309
319
  }
310
320
  async function isMarketplaceAdded(name) {
311
321
  const list = await listMarketplaces();
@@ -317,7 +327,7 @@ async function isMcpInstalled(name) {
317
327
  }
318
328
  async function findPlugin(id) {
319
329
  const list = await listPlugins();
320
- return list.find((p9) => p9.id === id);
330
+ return list.find((p10) => p10.id === id);
321
331
  }
322
332
  var marketplaceJsonCache = /* @__PURE__ */ new Map();
323
333
  function marketplaceDir(name) {
@@ -339,7 +349,7 @@ async function readMarketplaceJson(name) {
339
349
  async function getMarketplacePluginVersion(marketplaceName, pluginName) {
340
350
  const m = await readMarketplaceJson(marketplaceName);
341
351
  if (!m?.plugins) return null;
342
- const entry = m.plugins.find((p9) => p9.name === pluginName);
352
+ const entry = m.plugins.find((p10) => p10.name === pluginName);
343
353
  return entry?.version ?? null;
344
354
  }
345
355
  var REFRESH_TTL_MS = 60 * 60 * 1e3;
@@ -399,11 +409,13 @@ var pua = {
399
409
  name: "pua",
400
410
  description: "tanweai/pua \u2014 Chinese Claude Code skills bundle",
401
411
  type: "plugin",
412
+ slashNamespace: "/pua:*",
413
+ whenToUse: "auto-fires on 2+ failures or user frustration; sub-modes p7 / p9 / pro / loop. Skip on first-attempt failures or when a known fix is executing.",
402
414
  marketplaces: () => [MARKETPLACE_NAME],
403
415
  isInstalled: () => isPluginInstalled(PLUGIN_ID),
404
416
  installedVersion: async () => {
405
- const p9 = await findPlugin(PLUGIN_ID);
406
- const v = p9?.version;
417
+ const p10 = await findPlugin(PLUGIN_ID);
418
+ const v = p10?.version;
407
419
  return v && v !== "unknown" ? v : null;
408
420
  },
409
421
  latestVersion: () => getMarketplacePluginVersion(MARKETPLACE_NAME, PLUGIN_NAME),
@@ -426,11 +438,13 @@ var claudeMem = {
426
438
  name: "claude-mem",
427
439
  description: "thedotmack/claude-mem \u2014 persistent cross-session memory for Claude Code",
428
440
  type: "plugin",
441
+ slashNamespace: "/claude-mem:*",
442
+ whenToUse: 'for cross-session memory search ("did we solve this before?"), phased planning (`make-plan`), or phased execution (`do`).',
429
443
  marketplaces: () => [MARKETPLACE_NAME2],
430
444
  isInstalled: () => isPluginInstalled(PLUGIN_ID2),
431
445
  installedVersion: async () => {
432
- const p9 = await findPlugin(PLUGIN_ID2);
433
- const v = p9?.version;
446
+ const p10 = await findPlugin(PLUGIN_ID2);
447
+ const v = p10?.version;
434
448
  return v && v !== "unknown" ? v : null;
435
449
  },
436
450
  latestVersion: () => getMarketplacePluginVersion(MARKETPLACE_NAME2, PLUGIN_NAME2),
@@ -461,6 +475,7 @@ var chromeDevtoolsMcp = {
461
475
  name: "chrome-devtools-mcp",
462
476
  description: "ChromeDevTools/chrome-devtools-mcp \u2014 drive a real Chrome from Claude Code",
463
477
  type: "plugin",
478
+ whenToUse: "when debugging code that runs in a browser: perf traces, network / console inspection, DOM / CSS issues. Prefer snapshot over screenshot.",
464
479
  prereqCheck: async (t2) => {
465
480
  const major = Number(process.versions.node.split(".")[0] ?? "0");
466
481
  const minor = Number(process.versions.node.split(".")[1] ?? "0");
@@ -489,6 +504,7 @@ var frontendDesign = {
489
504
  name: "frontend-design",
490
505
  description: "Anthropic official \u2014 UI/frontend design helpers",
491
506
  type: "plugin",
507
+ whenToUse: "auto-fires when building UI / web components / pages. Best where visual personality matters (landing, marketing, portfolio).",
492
508
  isInstalled: () => isPluginInstalled(PLUGIN_ID4),
493
509
  install: (ctx) => installPluginById(PLUGIN_ID4, ctx),
494
510
  uninstall: (ctx) => uninstallPluginById(PLUGIN_ID4, ctx),
@@ -503,6 +519,7 @@ var sequentialThinking = {
503
519
  name: "sequential-thinking",
504
520
  description: "modelcontextprotocol/server-sequential-thinking \u2014 structured reasoning helper",
505
521
  type: "mcp",
522
+ whenToUse: "for complex multi-step problems where assumptions may shift (architecture comparison, risk-assessed migrations, prod-only debugging). Skip for simple queries.",
506
523
  isInstalled: () => isMcpInstalled(MCP_NAME),
507
524
  install: async (ctx) => {
508
525
  const r = await runStreaming(
@@ -543,6 +560,7 @@ var context7 = {
543
560
  name: "context7",
544
561
  description: "upstash/context7 \u2014 up-to-date docs from any library (HTTP MCP, optional API key)",
545
562
  type: "mcp",
563
+ whenToUse: "for any library / SDK / framework / API / Claude Code docs lookup. Use instead of web search.",
546
564
  isInstalled: () => isMcpInstalled(MCP_NAME2),
547
565
  configPrompts: async ({ t: t2 }) => {
548
566
  p2.note(`${t2("context7.dashboardHint")}
@@ -595,7 +613,181 @@ var PKGS = [
595
613
  context7_default
596
614
  ];
597
615
  function findPkg(id) {
598
- return PKGS.find((p9) => p9.id === id);
616
+ return PKGS.find((p10) => p10.id === id);
617
+ }
618
+
619
+ // src/runner/claudeMd.ts
620
+ import { promises as fs2 } from "fs";
621
+ import path2 from "path";
622
+ import os2 from "os";
623
+ import * as p3 from "@clack/prompts";
624
+ var BEGIN_MARKER = "<!-- BEGIN @curdx/flow v1 -->";
625
+ var END_MARKER = "<!-- END @curdx/flow v1 -->";
626
+ var BLOCK_RE = /<!-- BEGIN @curdx\/flow v\d+[^>]*-->[\s\S]*?<!-- END @curdx\/flow v\d+ -->/;
627
+ function claudeMdPath() {
628
+ return path2.join(os2.homedir(), ".claude", "CLAUDE.md");
629
+ }
630
+ function renderItemLine(item) {
631
+ let line = `- ${item.name}`;
632
+ if (item.version) line += ` (v${item.version})`;
633
+ if (item.slashNamespace) line += ` \u2014 \`${item.slashNamespace}\``;
634
+ if (item.whenToUse) line += ` \u2014 ${item.whenToUse}`;
635
+ return line;
636
+ }
637
+ var ALWAYS_ON_RULES = [
638
+ "Do not call every tool by default; pick by the trigger condition above.",
639
+ "For first-attempt failures or simple edits, skip extra tools."
640
+ ];
641
+ function buildConditionalRules(installedIds) {
642
+ const out = [];
643
+ const planners = [];
644
+ if (installedIds.has("sequential-thinking")) planners.push("sequential-thinking");
645
+ if (installedIds.has("claude-mem")) planners.push("claude-mem `make-plan`");
646
+ if (planners.length > 0) {
647
+ out.push(`For complex / risky changes, plan first (${planners.join(" or ")}).`);
648
+ }
649
+ if (installedIds.has("context7")) {
650
+ out.push("For library / SDK lookups, prefer context7 over web search.");
651
+ }
652
+ if (installedIds.has("chrome-devtools-mcp")) {
653
+ out.push("For browser-rendered behavior, verify in chrome-devtools-mcp instead of guessing.");
654
+ }
655
+ return out;
656
+ }
657
+ function renderBlock(items) {
658
+ const installedIds = new Set(items.map((i) => i.id));
659
+ const rules = [...ALWAYS_ON_RULES, ...buildConditionalRules(installedIds)];
660
+ return [
661
+ BEGIN_MARKER,
662
+ "## Tool Usage",
663
+ "",
664
+ "Available tools/plugins:",
665
+ ...items.map(renderItemLine),
666
+ "",
667
+ "Rules:",
668
+ ...rules.map((r) => `- ${r}`),
669
+ "",
670
+ "Run `npx @curdx/flow` to install / update / uninstall.",
671
+ END_MARKER
672
+ ].join("\n");
673
+ }
674
+ function withEol(s, eol) {
675
+ return eol === "\n" ? s : s.split("\n").join(eol);
676
+ }
677
+ function ensureSingleTrailingNewline(s, eol) {
678
+ if (s.length === 0) return s;
679
+ return s.replace(/[\r\n]+$/, "") + eol;
680
+ }
681
+ function upsertBlock(existing, blockBody, eol) {
682
+ const block = withEol(blockBody, eol);
683
+ if (BLOCK_RE.test(existing)) {
684
+ return existing.replace(BLOCK_RE, block);
685
+ }
686
+ if (existing.length === 0) {
687
+ return block + eol;
688
+ }
689
+ const trimmed = existing.replace(/[\r\n\s]+$/, "");
690
+ return trimmed + eol + eol + block + eol;
691
+ }
692
+ function removeBlock(existing, eol) {
693
+ if (!BLOCK_RE.test(existing)) return existing;
694
+ let next = existing.replace(BLOCK_RE, "");
695
+ const tripleEol = new RegExp(`(?:\\r?\\n){3,}`, "g");
696
+ next = next.replace(tripleEol, eol + eol);
697
+ if (next.replace(/[\s\r\n]/g, "").length === 0) return "";
698
+ return ensureSingleTrailingNewline(next, eol);
699
+ }
700
+ async function pkgToItem(pkg) {
701
+ let version;
702
+ if (pkg.installedVersion) {
703
+ const v = await pkg.installedVersion();
704
+ if (v) version = v;
705
+ }
706
+ return {
707
+ id: pkg.id,
708
+ name: pkg.name,
709
+ type: pkg.type,
710
+ version,
711
+ whenToUse: pkg.whenToUse,
712
+ slashNamespace: pkg.slashNamespace
713
+ };
714
+ }
715
+ async function collectInstalledItems() {
716
+ await Promise.all([listPlugins(true), listMcp(true)]);
717
+ const items = [];
718
+ for (const pkg of PKGS) {
719
+ if (await pkg.isInstalled()) {
720
+ items.push(await pkgToItem(pkg));
721
+ }
722
+ }
723
+ items.sort((a, b) => {
724
+ if (a.type !== b.type) return a.type === "plugin" ? -1 : 1;
725
+ return a.name.localeCompare(b.name);
726
+ });
727
+ return items;
728
+ }
729
+ async function syncClaudeMd(opts) {
730
+ const file = claudeMdPath();
731
+ if (opts?.skip) return { status: "skipped", path: file };
732
+ try {
733
+ const items = await collectInstalledItems();
734
+ let existing = "";
735
+ let existed = true;
736
+ try {
737
+ existing = await fs2.readFile(file, "utf8");
738
+ } catch (err) {
739
+ if (err.code === "ENOENT") {
740
+ existed = false;
741
+ } else {
742
+ throw err;
743
+ }
744
+ }
745
+ const eol = existing.includes("\r\n") ? "\r\n" : "\n";
746
+ const hadBlock = BLOCK_RE.test(existing);
747
+ let next;
748
+ if (items.length === 0) {
749
+ if (!hadBlock) {
750
+ return { status: "unchanged", path: file };
751
+ }
752
+ next = removeBlock(existing, eol);
753
+ } else {
754
+ next = upsertBlock(existing, renderBlock(items), eol);
755
+ }
756
+ if (next === existing) {
757
+ return { status: "unchanged", path: file };
758
+ }
759
+ await fs2.mkdir(path2.dirname(file), { recursive: true });
760
+ const tmp = `${file}.tmp.${process.pid}`;
761
+ await fs2.writeFile(tmp, next, "utf8");
762
+ await fs2.rename(tmp, file);
763
+ if (!existed) return { status: "created", path: file };
764
+ if (hadBlock && items.length === 0) return { status: "removed", path: file };
765
+ return { status: "updated", path: file };
766
+ } catch (err) {
767
+ const msg = err instanceof Error ? err.message : String(err);
768
+ return { status: "failed", path: file, error: msg };
769
+ }
770
+ }
771
+ async function syncFromState(opts) {
772
+ const r = await syncClaudeMd(opts);
773
+ switch (r.status) {
774
+ case "skipped":
775
+ p3.log.info(t("claudeMd.skipped"));
776
+ return;
777
+ case "unchanged":
778
+ p3.log.info(t("claudeMd.unchanged"));
779
+ return;
780
+ case "created":
781
+ case "updated":
782
+ p3.log.info(t("claudeMd.synced", { path: r.path }));
783
+ return;
784
+ case "removed":
785
+ p3.log.info(t("claudeMd.removed"));
786
+ return;
787
+ case "failed":
788
+ p3.log.warn(t("claudeMd.failed", { error: r.error ?? "unknown" }));
789
+ return;
790
+ }
599
791
  }
600
792
 
601
793
  // src/flows/install.ts
@@ -634,13 +826,13 @@ async function selectInteractive(states) {
634
826
  const s = states.get(pkg.id);
635
827
  return s.kind === "not_installed" || s.kind === "update_available";
636
828
  }).map((pkg) => pkg.id);
637
- const picked = await p3.multiselect({
829
+ const picked = await p4.multiselect({
638
830
  message: t("install.selectPrompt"),
639
831
  options,
640
832
  initialValues,
641
833
  required: false
642
834
  });
643
- if (p3.isCancel(picked)) return null;
835
+ if (p4.isCancel(picked)) return null;
644
836
  return picked.map((id) => findPkg(id)).filter((x2) => Boolean(x2));
645
837
  }
646
838
  function selectFromIds(opts) {
@@ -650,7 +842,7 @@ function selectFromIds(opts) {
650
842
  for (const id of opts.ids) {
651
843
  const pkg = findPkg(id);
652
844
  if (pkg) found.push(pkg);
653
- else p3.log.warn(`Unknown id: ${id}`);
845
+ else p4.log.warn(`Unknown id: ${id}`);
654
846
  }
655
847
  return found;
656
848
  }
@@ -662,11 +854,11 @@ async function runOne(pkg, state, opts) {
662
854
  mode = "update";
663
855
  } else {
664
856
  if (!opts.yes) {
665
- const ans = await p3.confirm({
857
+ const ans = await p4.confirm({
666
858
  message: t("install.confirmReinstall", { name: pkg.name }),
667
859
  initialValue: false
668
860
  });
669
- if (p3.isCancel(ans) || ans === false) {
861
+ if (p4.isCancel(ans) || ans === false) {
670
862
  return { id: pkg.id, status: "skip", message: t("install.skippedReinstall", { name: pkg.name }) };
671
863
  }
672
864
  }
@@ -675,7 +867,7 @@ async function runOne(pkg, state, opts) {
675
867
  if (pkg.prereqCheck) {
676
868
  const r = await pkg.prereqCheck(t);
677
869
  if (!r.ok) {
678
- p3.log.warn(t("install.prereqFail", { name: pkg.name, reason: r.reason }));
870
+ p4.log.warn(t("install.prereqFail", { name: pkg.name, reason: r.reason }));
679
871
  return { id: pkg.id, status: "skip", message: r.reason };
680
872
  }
681
873
  }
@@ -690,30 +882,30 @@ async function runOne(pkg, state, opts) {
690
882
  if (mode === "update" && state.kind === "update_available") {
691
883
  titleVars["version"] = state.latest;
692
884
  }
693
- const log4 = p3.taskLog({ title: t(titleKey, titleVars) });
885
+ const log5 = p4.taskLog({ title: t(titleKey, titleVars) });
694
886
  try {
695
887
  if (mode === "reinstall") {
696
- log4.message(t("reinstall.uninstalling"));
697
- await pkg.uninstall({ log: log4, config, t });
698
- log4.message(t("reinstall.installing"));
699
- await pkg.install({ log: log4, config, t });
888
+ log5.message(t("reinstall.uninstalling"));
889
+ await pkg.uninstall({ log: log5, config, t });
890
+ log5.message(t("reinstall.installing"));
891
+ await pkg.install({ log: log5, config, t });
700
892
  } else if (mode === "update") {
701
893
  if (pkg.update) {
702
- await pkg.update({ log: log4, config, t });
894
+ await pkg.update({ log: log5, config, t });
703
895
  } else {
704
- log4.message(t("reinstall.uninstalling"));
705
- await pkg.uninstall({ log: log4, config, t });
706
- log4.message(t("reinstall.installing"));
707
- await pkg.install({ log: log4, config, t });
896
+ log5.message(t("reinstall.uninstalling"));
897
+ await pkg.uninstall({ log: log5, config, t });
898
+ log5.message(t("reinstall.installing"));
899
+ await pkg.install({ log: log5, config, t });
708
900
  }
709
901
  } else {
710
- await pkg.install({ log: log4, config, t });
902
+ await pkg.install({ log: log5, config, t });
711
903
  }
712
- log4.success(t("install.success", { name: pkg.name }));
904
+ log5.success(t("install.success", { name: pkg.name }));
713
905
  return { id: pkg.id, status: "ok" };
714
906
  } catch (err) {
715
907
  const msg = err instanceof Error ? err.message : String(err);
716
- log4.error(`${t("install.failed", { name: pkg.name })}
908
+ log5.error(`${t("install.failed", { name: pkg.name })}
717
909
  ${msg}`);
718
910
  return { id: pkg.id, status: "fail", message: msg };
719
911
  }
@@ -731,7 +923,7 @@ function summarize(results) {
731
923
  ...skip.map((r) => ` ${pc.yellow("-")} ${r.id}${r.message ? pc.dim(` (${r.message})`) : ""}`),
732
924
  ...fail.map((r) => ` ${pc.red("\u2717")} ${r.id}${r.message ? pc.dim(` (${r.message.split("\n")[0]})`) : ""}`)
733
925
  ];
734
- p3.note(lines.join("\n"), t("install.summaryTitle"));
926
+ p4.note(lines.join("\n"), t("install.summaryTitle"));
735
927
  }
736
928
  async function maybeRefreshMarketplaces(opts) {
737
929
  if (opts.noRefresh) return;
@@ -740,7 +932,7 @@ async function maybeRefreshMarketplaces(opts) {
740
932
  if (pkg.marketplaces) for (const n of pkg.marketplaces()) names.add(n);
741
933
  }
742
934
  if (names.size === 0) return;
743
- const sp = p3.spinner();
935
+ const sp = p4.spinner();
744
936
  sp.start(t("marketplace.refreshing"));
745
937
  const refreshed = await refreshMarketplaces([...names]);
746
938
  sp.stop(
@@ -752,7 +944,7 @@ async function installFlow(opts = {}) {
752
944
  const explicit = opts.all || opts.ids && opts.ids.length > 0;
753
945
  const candidates = explicit ? selectFromIds(opts) : [...PKGS];
754
946
  if (candidates.length === 0) {
755
- p3.log.info(t("install.nothingSelected"));
947
+ p4.log.info(t("install.nothingSelected"));
756
948
  return;
757
949
  }
758
950
  const stateMap = /* @__PURE__ */ new Map();
@@ -767,13 +959,13 @@ async function installFlow(opts = {}) {
767
959
  } else {
768
960
  const picked = await selectInteractive(stateMap);
769
961
  if (picked === null) {
770
- p3.cancel(t("app.cancelled"));
962
+ p4.cancel(t("app.cancelled"));
771
963
  return;
772
964
  }
773
965
  targets = picked;
774
966
  }
775
967
  if (targets.length === 0) {
776
- p3.log.info(t("install.nothingSelected"));
968
+ p4.log.info(t("install.nothingSelected"));
777
969
  return;
778
970
  }
779
971
  const results = [];
@@ -782,10 +974,11 @@ async function installFlow(opts = {}) {
782
974
  results.push(await runOne(pkg, state, opts));
783
975
  }
784
976
  summarize(results);
977
+ await syncFromState({ skip: opts.noClaudeMd });
785
978
  }
786
979
 
787
980
  // src/flows/uninstall.ts
788
- import * as p4 from "@clack/prompts";
981
+ import * as p5 from "@clack/prompts";
789
982
  import pc2 from "picocolors";
790
983
  async function getInstalled() {
791
984
  const states = await Promise.all(PKGS.map(async (pkg) => ({ pkg, installed: await pkg.isInstalled() })));
@@ -799,21 +992,21 @@ async function uninstallFlow(opts = {}) {
799
992
  for (const id of opts.ids) {
800
993
  const pkg = findPkg(id);
801
994
  if (!pkg) {
802
- p4.log.warn(`Unknown id: ${id}`);
995
+ p5.log.warn(`Unknown id: ${id}`);
803
996
  continue;
804
997
  }
805
998
  if (!installed.some((x2) => x2.id === pkg.id)) {
806
- p4.log.warn(`${pkg.name}: ${t("pkg.notInstalled")}`);
999
+ p5.log.warn(`${pkg.name}: ${t("pkg.notInstalled")}`);
807
1000
  continue;
808
1001
  }
809
1002
  targets.push(pkg);
810
1003
  }
811
1004
  } else {
812
1005
  if (installed.length === 0) {
813
- p4.log.info(t("uninstall.noneInstalled"));
1006
+ p5.log.info(t("uninstall.noneInstalled"));
814
1007
  return;
815
1008
  }
816
- const picked = await p4.multiselect({
1009
+ const picked = await p5.multiselect({
817
1010
  message: t("uninstall.selectPrompt"),
818
1011
  options: installed.map((pkg) => ({
819
1012
  value: pkg.id,
@@ -822,53 +1015,54 @@ async function uninstallFlow(opts = {}) {
822
1015
  })),
823
1016
  required: false
824
1017
  });
825
- if (p4.isCancel(picked)) {
826
- p4.cancel(t("app.cancelled"));
1018
+ if (p5.isCancel(picked)) {
1019
+ p5.cancel(t("app.cancelled"));
827
1020
  return;
828
1021
  }
829
1022
  targets = picked.map((id) => findPkg(id)).filter((x2) => Boolean(x2));
830
1023
  }
831
1024
  if (targets.length === 0) {
832
- p4.log.info(t("install.nothingSelected"));
1025
+ p5.log.info(t("install.nothingSelected"));
833
1026
  return;
834
1027
  }
835
1028
  if (!opts.yes) {
836
- const ok2 = await p4.confirm({
1029
+ const ok2 = await p5.confirm({
837
1030
  message: t("uninstall.confirm", { count: targets.length }),
838
1031
  initialValue: false
839
1032
  });
840
- if (p4.isCancel(ok2) || ok2 === false) {
841
- p4.cancel(t("app.cancelled"));
1033
+ if (p5.isCancel(ok2) || ok2 === false) {
1034
+ p5.cancel(t("app.cancelled"));
842
1035
  return;
843
1036
  }
844
1037
  }
845
1038
  const results = [];
846
1039
  for (const pkg of targets) {
847
- const log4 = p4.taskLog({ title: t("uninstall.starting", { name: pkg.name }) });
1040
+ const log5 = p5.taskLog({ title: t("uninstall.starting", { name: pkg.name }) });
848
1041
  try {
849
- await pkg.uninstall({ log: log4, config: {}, t });
850
- log4.success(t("uninstall.success", { name: pkg.name }));
1042
+ await pkg.uninstall({ log: log5, config: {}, t });
1043
+ log5.success(t("uninstall.success", { name: pkg.name }));
851
1044
  results.push({ id: pkg.id, status: "ok" });
852
1045
  } catch (err) {
853
1046
  const msg = err instanceof Error ? err.message : String(err);
854
- log4.error(`${t("uninstall.failed", { name: pkg.name })}
1047
+ log5.error(`${t("uninstall.failed", { name: pkg.name })}
855
1048
  ${msg}`);
856
1049
  results.push({ id: pkg.id, status: "fail", message: msg });
857
1050
  }
858
1051
  }
859
1052
  const ok = results.filter((r) => r.status === "ok").length;
860
1053
  const fail = results.filter((r) => r.status === "fail").length;
861
- p4.note(
1054
+ p5.note(
862
1055
  [
863
1056
  pc2.green(t("install.summaryOk", { count: ok })),
864
1057
  pc2.red(t("install.summaryFail", { count: fail }))
865
1058
  ].join("\n"),
866
1059
  t("install.summaryTitle")
867
1060
  );
1061
+ await syncFromState({ skip: opts.noClaudeMd });
868
1062
  }
869
1063
 
870
1064
  // src/flows/update.ts
871
- import * as p5 from "@clack/prompts";
1065
+ import * as p6 from "@clack/prompts";
872
1066
  import pc3 from "picocolors";
873
1067
  async function getInstalled2() {
874
1068
  const states = await Promise.all(PKGS.map(async (pkg) => ({ pkg, installed: await pkg.isInstalled() })));
@@ -877,7 +1071,7 @@ async function getInstalled2() {
877
1071
  async function updateFlow(opts = {}) {
878
1072
  const installed = await getInstalled2();
879
1073
  if (installed.length === 0) {
880
- p5.log.info(t("update.noneInstalled"));
1074
+ p6.log.info(t("update.noneInstalled"));
881
1075
  return;
882
1076
  }
883
1077
  let targets;
@@ -888,17 +1082,17 @@ async function updateFlow(opts = {}) {
888
1082
  for (const id of opts.ids) {
889
1083
  const pkg = findPkg(id);
890
1084
  if (!pkg) {
891
- p5.log.warn(`Unknown id: ${id}`);
1085
+ p6.log.warn(`Unknown id: ${id}`);
892
1086
  continue;
893
1087
  }
894
1088
  if (!installed.some((x2) => x2.id === pkg.id)) {
895
- p5.log.warn(`${pkg.name}: ${t("pkg.notInstalled")}`);
1089
+ p6.log.warn(`${pkg.name}: ${t("pkg.notInstalled")}`);
896
1090
  continue;
897
1091
  }
898
1092
  targets.push(pkg);
899
1093
  }
900
1094
  } else {
901
- const picked = await p5.multiselect({
1095
+ const picked = await p6.multiselect({
902
1096
  message: t("update.selectPrompt"),
903
1097
  options: installed.map((pkg) => ({
904
1098
  value: pkg.id,
@@ -907,41 +1101,41 @@ async function updateFlow(opts = {}) {
907
1101
  })),
908
1102
  required: false
909
1103
  });
910
- if (p5.isCancel(picked)) {
911
- p5.cancel(t("app.cancelled"));
1104
+ if (p6.isCancel(picked)) {
1105
+ p6.cancel(t("app.cancelled"));
912
1106
  return;
913
1107
  }
914
1108
  targets = picked.map((id) => findPkg(id)).filter((x2) => Boolean(x2));
915
1109
  }
916
1110
  if (targets.length === 0) {
917
- p5.log.info(t("install.nothingSelected"));
1111
+ p6.log.info(t("install.nothingSelected"));
918
1112
  return;
919
1113
  }
920
1114
  const results = [];
921
1115
  for (const pkg of targets) {
922
1116
  if (pkg.id === "sequential-thinking") {
923
- p5.log.info(t("update.mcpAutoNote", { name: pkg.name }));
1117
+ p6.log.info(t("update.mcpAutoNote", { name: pkg.name }));
924
1118
  results.push({ id: pkg.id, status: "noop" });
925
1119
  continue;
926
1120
  }
927
1121
  if (pkg.id === "context7") {
928
- p5.log.info(t("update.context7Note"));
1122
+ p6.log.info(t("update.context7Note"));
929
1123
  results.push({ id: pkg.id, status: "noop" });
930
1124
  continue;
931
1125
  }
932
- const log4 = p5.taskLog({ title: t("update.starting", { name: pkg.name }) });
1126
+ const log5 = p6.taskLog({ title: t("update.starting", { name: pkg.name }) });
933
1127
  try {
934
1128
  if (pkg.update) {
935
- await pkg.update({ log: log4, config: {}, t });
1129
+ await pkg.update({ log: log5, config: {}, t });
936
1130
  } else {
937
- await pkg.uninstall({ log: log4, config: {}, t });
938
- await pkg.install({ log: log4, config: {}, t });
1131
+ await pkg.uninstall({ log: log5, config: {}, t });
1132
+ await pkg.install({ log: log5, config: {}, t });
939
1133
  }
940
- log4.success(t("update.success", { name: pkg.name }));
1134
+ log5.success(t("update.success", { name: pkg.name }));
941
1135
  results.push({ id: pkg.id, status: "ok" });
942
1136
  } catch (err) {
943
1137
  const msg = err instanceof Error ? err.message : String(err);
944
- log4.error(`${t("update.failed", { name: pkg.name })}
1138
+ log5.error(`${t("update.failed", { name: pkg.name })}
945
1139
  ${msg}`);
946
1140
  results.push({ id: pkg.id, status: "fail", message: msg });
947
1141
  }
@@ -949,7 +1143,7 @@ ${msg}`);
949
1143
  const ok = results.filter((r) => r.status === "ok").length;
950
1144
  const fail = results.filter((r) => r.status === "fail").length;
951
1145
  const noop = results.filter((r) => r.status === "noop").length;
952
- p5.note(
1146
+ p6.note(
953
1147
  [
954
1148
  pc3.green(t("install.summaryOk", { count: ok })),
955
1149
  pc3.red(t("install.summaryFail", { count: fail })),
@@ -957,10 +1151,11 @@ ${msg}`);
957
1151
  ].join("\n"),
958
1152
  t("install.summaryTitle")
959
1153
  );
1154
+ await syncFromState({ skip: opts.noClaudeMd });
960
1155
  }
961
1156
 
962
1157
  // src/flows/status.ts
963
- import * as p6 from "@clack/prompts";
1158
+ import * as p7 from "@clack/prompts";
964
1159
  import pc4 from "picocolors";
965
1160
  async function statusFlow(opts = {}) {
966
1161
  const states = await Promise.all(
@@ -993,12 +1188,12 @@ async function statusFlow(opts = {}) {
993
1188
  const rows = states.map(
994
1189
  (s) => `${s.name.padEnd(nameW)} ${s.type.padEnd(typeW)} ${s.installed ? pc4.green(`\u2713 ${t("pkg.installed")}`) : pc4.yellow(`\u2717 ${t("pkg.notInstalled")}`)}`
995
1190
  );
996
- p6.note([header, sep, ...rows].join("\n"), t("status.title"));
1191
+ p7.note([header, sep, ...rows].join("\n"), t("status.title"));
997
1192
  }
998
1193
 
999
1194
  // src/ui/menu.ts
1000
1195
  async function mainMenu() {
1001
- const action = await p7.select({
1196
+ const action = await p8.select({
1002
1197
  message: t("menu.title"),
1003
1198
  options: [
1004
1199
  { value: "install", label: t("menu.install") },
@@ -1008,8 +1203,8 @@ async function mainMenu() {
1008
1203
  { value: "exit", label: t("menu.exit") }
1009
1204
  ]
1010
1205
  });
1011
- if (p7.isCancel(action) || action === "exit") {
1012
- p7.cancel(t("app.cancelled"));
1206
+ if (p8.isCancel(action) || action === "exit") {
1207
+ p8.cancel(t("app.cancelled"));
1013
1208
  return;
1014
1209
  }
1015
1210
  switch (action) {
@@ -1032,8 +1227,16 @@ async function mainMenu() {
1032
1227
  function parseLang(v) {
1033
1228
  return v === "zh" || v === "en" ? v : void 0;
1034
1229
  }
1230
+ function noClaudeMdFromArgs(args) {
1231
+ if (args["no-claude-md"]) return true;
1232
+ return Boolean(process.env["CURDX_FLOW_NO_CLAUDE_MD"]);
1233
+ }
1035
1234
  var sharedArgs = {
1036
- lang: { type: "string", description: "Override language: zh or en" }
1235
+ lang: { type: "string", description: "Override language: zh or en" },
1236
+ "no-claude-md": {
1237
+ type: "boolean",
1238
+ description: "Skip syncing the @curdx/flow block in ~/.claude/CLAUDE.md"
1239
+ }
1037
1240
  };
1038
1241
  var installCmd = defineCommand({
1039
1242
  meta: { name: "install", description: "Install, reinstall, or update plugins / MCP servers" },
@@ -1046,15 +1249,16 @@ var installCmd = defineCommand({
1046
1249
  },
1047
1250
  async run({ args }) {
1048
1251
  await initLanguage(parseLang(args.lang));
1049
- p8.intro(t("app.intro"));
1252
+ p9.intro(t("app.intro"));
1050
1253
  const ids = collectPositional(args);
1051
1254
  await installFlow({
1052
1255
  ids,
1053
1256
  all: Boolean(args.all),
1054
1257
  yes: Boolean(args.yes),
1055
- noRefresh: Boolean(args["no-refresh"])
1258
+ noRefresh: Boolean(args["no-refresh"]),
1259
+ noClaudeMd: noClaudeMdFromArgs(args)
1056
1260
  });
1057
- p8.outro(t("app.outro"));
1261
+ p9.outro(t("app.outro"));
1058
1262
  }
1059
1263
  });
1060
1264
  var uninstallCmd = defineCommand({
@@ -1066,10 +1270,14 @@ var uninstallCmd = defineCommand({
1066
1270
  },
1067
1271
  async run({ args }) {
1068
1272
  await initLanguage(parseLang(args.lang));
1069
- p8.intro(t("app.intro"));
1273
+ p9.intro(t("app.intro"));
1070
1274
  const ids = collectPositional(args);
1071
- await uninstallFlow({ ids, yes: Boolean(args.yes) });
1072
- p8.outro(t("app.outro"));
1275
+ await uninstallFlow({
1276
+ ids,
1277
+ yes: Boolean(args.yes),
1278
+ noClaudeMd: noClaudeMdFromArgs(args)
1279
+ });
1280
+ p9.outro(t("app.outro"));
1073
1281
  }
1074
1282
  });
1075
1283
  var updateCmd = defineCommand({
@@ -1081,10 +1289,14 @@ var updateCmd = defineCommand({
1081
1289
  },
1082
1290
  async run({ args }) {
1083
1291
  await initLanguage(parseLang(args.lang));
1084
- p8.intro(t("app.intro"));
1292
+ p9.intro(t("app.intro"));
1085
1293
  const ids = collectPositional(args);
1086
- await updateFlow({ ids, all: Boolean(args.all) });
1087
- p8.outro(t("app.outro"));
1294
+ await updateFlow({
1295
+ ids,
1296
+ all: Boolean(args.all),
1297
+ noClaudeMd: noClaudeMdFromArgs(args)
1298
+ });
1299
+ p9.outro(t("app.outro"));
1088
1300
  }
1089
1301
  });
1090
1302
  var statusCmd = defineCommand({
@@ -1095,16 +1307,16 @@ var statusCmd = defineCommand({
1095
1307
  },
1096
1308
  async run({ args }) {
1097
1309
  await initLanguage(parseLang(args.lang));
1098
- if (!args.json) p8.intro(t("app.intro"));
1310
+ if (!args.json) p9.intro(t("app.intro"));
1099
1311
  await statusFlow({ json: Boolean(args.json) });
1100
- if (!args.json) p8.outro(t("app.outro"));
1312
+ if (!args.json) p9.outro(t("app.outro"));
1101
1313
  }
1102
1314
  });
1103
1315
  var SUBCOMMANDS = /* @__PURE__ */ new Set(["install", "uninstall", "update", "status"]);
1104
1316
  var root = defineCommand({
1105
1317
  meta: {
1106
1318
  name: "@curdx/flow",
1107
- version: "3.2.0",
1319
+ version: "3.3.0",
1108
1320
  description: "Interactive installer for Claude Code plugins and MCP servers"
1109
1321
  },
1110
1322
  args: sharedArgs,
@@ -1139,9 +1351,9 @@ async function runInteractive(argv2) {
1139
1351
  else if (argv2[i]?.startsWith("--lang=")) lang = parseLang(argv2[i].slice("--lang=".length));
1140
1352
  }
1141
1353
  await initLanguage(lang);
1142
- p8.intro(t("app.intro"));
1354
+ p9.intro(t("app.intro"));
1143
1355
  await mainMenu();
1144
- p8.outro(t("app.outro"));
1356
+ p9.outro(t("app.outro"));
1145
1357
  }
1146
1358
  var argv = process.argv.slice(2);
1147
1359
  var first = firstNonFlag(argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Interactive installer for Claude Code plugins and MCP servers",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.mjs",