@curdx/flow 2.0.18 → 2.0.20

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/uninstall.js CHANGED
@@ -12,17 +12,27 @@ import {
12
12
  resultLastLine,
13
13
  resultOutput,
14
14
  confirm,
15
- multiSelect,
16
- claudeVersion,
17
15
  listPlugins,
18
16
  } from "./utils.js";
19
- import { removeGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
20
17
  import { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
21
18
  import {
22
19
  removeMcp,
23
20
  removePluginMarketplace,
24
21
  uninstallPlugin,
25
22
  } from "./lib/claude-ops.js";
23
+ import {
24
+ createUninstallContext,
25
+ ensureClaudeCliAvailableForUninstall,
26
+ getInstalledTargets,
27
+ getManagedMarketplaceIds,
28
+ printUninstallSummary,
29
+ removeProtocolsStep,
30
+ selectRecommendedPluginsToRemove,
31
+ shouldKeepBundledMcps,
32
+ shouldKeepRequiredPlugins,
33
+ UNINSTALL_STEP_COUNT,
34
+ confirmUninstallStep,
35
+ } from "./uninstall-workflow.js";
26
36
 
27
37
  const HOME = homedir();
28
38
 
@@ -36,45 +46,30 @@ const MANAGED_SYMLINKS = [
36
46
  ];
37
47
 
38
48
  export async function uninstall(args = []) {
39
- const yes = args.includes("--yes") || args.includes("-y");
40
- const purge = args.includes("--purge");
41
- const keepRecommended = args.includes("--keep-recommended");
49
+ const context = createUninstallContext(args);
42
50
 
43
51
  log.title("🗑️ CurdX-Flow Uninstaller");
44
52
 
45
- const cv = claudeVersion();
46
- if (!cv) {
47
- log.err("claude CLI not found, cannot uninstall plugin.");
48
- process.exit(1);
49
- }
53
+ ensureClaudeCliAvailableForUninstall();
50
54
 
51
- if (!(await confirmUninstall({ yes }))) {
52
- log.info("Cancelled");
55
+ if (!(await confirmUninstallStep(context))) {
53
56
  return;
54
57
  }
55
58
 
56
59
  await uninstallCurdxFlowPlugin();
57
- await maybeUninstallRecommendedPlugins({ yes, keepRecommended });
58
- await maybeRemoveBundledMcps({ yes, keepRecommended });
59
- await maybeUninstallRequiredPlugins({ yes });
60
- await maybePurgeRuntimeArtifacts({ purge });
61
- removeProtocols();
62
- await maybeRemoveProjectState({ yes });
63
-
64
- printSummary({ purge });
65
- }
66
-
67
- async function confirmUninstall({ yes }) {
68
- if (yes) return true;
69
- return confirm(
70
- `This will uninstall the ${color.bold("curdx-flow")} plugin. Continue?`,
71
- false
72
- );
60
+ await maybeUninstallRecommendedPlugins(context);
61
+ await maybeRemoveBundledMcps(context);
62
+ await maybeUninstallRequiredPlugins(context);
63
+ await maybePurgeRuntimeArtifacts(context);
64
+ removeProtocolsStep();
65
+ await maybeRemoveProjectState(context);
66
+
67
+ printUninstallSummary(context);
73
68
  }
74
69
 
75
70
  async function uninstallCurdxFlowPlugin() {
76
71
  log.blank();
77
- log.step(1, 4, "Uninstalling curdx-flow plugin...");
72
+ log.step(1, UNINSTALL_STEP_COUNT, "Uninstalling curdx-flow plugin...");
78
73
  const curdx = listPlugins().find((plugin) => plugin.name === "curdx-flow");
79
74
  if (!curdx) {
80
75
  log.info("curdx-flow not installed, skipping");
@@ -95,22 +90,19 @@ async function uninstallCurdxFlowPlugin() {
95
90
 
96
91
  async function maybeUninstallRecommendedPlugins({ yes, keepRecommended }) {
97
92
  log.blank();
98
- log.step(2, 4, "Recommended plugins");
93
+ log.step(2, UNINSTALL_STEP_COUNT, "Recommended plugins");
99
94
  if (keepRecommended) {
100
95
  log.info("Keeping recommended plugins (--keep-recommended)");
101
96
  return;
102
97
  }
103
98
 
104
- const currentlyInstalled = listPlugins();
105
- const present = RECOMMENDED.filter((entry) =>
106
- currentlyInstalled.some((plugin) => plugin.name === entry.name)
107
- );
99
+ const present = getInstalledTargets(RECOMMENDED);
108
100
  if (present.length === 0) {
109
101
  log.info("No installed recommended plugins");
110
102
  return;
111
103
  }
112
104
 
113
- const selected = await selectPluginsToRemove({ yes, present });
105
+ const selected = await selectRecommendedPluginsToRemove({ yes, present });
114
106
  for (const name of selected) {
115
107
  const entry = present.find((plugin) => plugin.name === name);
116
108
  if (!entry) continue;
@@ -118,26 +110,6 @@ async function maybeUninstallRecommendedPlugins({ yes, keepRecommended }) {
118
110
  }
119
111
  }
120
112
 
121
- async function selectPluginsToRemove({ yes, present }) {
122
- if (yes) {
123
- log.info(
124
- color.dim("--yes mode: keeping recommended plugins (use --purge to remove them)")
125
- );
126
- return [];
127
- }
128
-
129
- const choices = present.map((entry) => ({
130
- label: color.bold(entry.name),
131
- value: entry.name,
132
- hint: "",
133
- }));
134
- return multiSelect(
135
- "Which recommended plugins to also uninstall? (default: none)",
136
- choices,
137
- []
138
- );
139
- }
140
-
141
113
  async function uninstallNamedPlugin(entry) {
142
114
  log.blank();
143
115
  console.log(` ${color.cyan("▸")} Uninstalling ${color.bold(entry.name)}...`);
@@ -155,7 +127,7 @@ async function uninstallNamedPlugin(entry) {
155
127
  async function maybeRemoveBundledMcps({ yes, keepRecommended }) {
156
128
  log.blank();
157
129
  log.info("Required MCP servers (context7, sequential-thinking)");
158
- if (keepRecommended || yes) {
130
+ if (shouldKeepBundledMcps({ yes, keepRecommended })) {
159
131
  log.info(
160
132
  color.dim("--yes or --keep-recommended: keeping user-level MCPs (remove manually with `claude mcp remove <name>`)")
161
133
  );
@@ -184,7 +156,7 @@ async function maybeRemoveBundledMcps({ yes, keepRecommended }) {
184
156
  async function maybeUninstallRequiredPlugins({ yes }) {
185
157
  log.blank();
186
158
  log.info("Required companion plugins");
187
- if (yes) {
159
+ if (shouldKeepRequiredPlugins({ yes })) {
188
160
  log.info(
189
161
  color.dim("--yes mode: keeping required companion plugins (use --purge to remove them)")
190
162
  );
@@ -212,7 +184,7 @@ async function maybeUninstallRequiredPlugins({ yes }) {
212
184
 
213
185
  async function maybePurgeRuntimeArtifacts({ purge }) {
214
186
  log.blank();
215
- log.step(3, 4, "Runtime symlinks and marketplaces");
187
+ log.step(3, UNINSTALL_STEP_COUNT, "Runtime symlinks and marketplaces");
216
188
  if (!purge) {
217
189
  log.info(
218
190
  color.dim("Keeping ~/.local/bin/bun, ~/.local/bin/uv (use --purge to remove)")
@@ -228,14 +200,7 @@ async function maybePurgeRuntimeArtifacts({ purge }) {
228
200
  }
229
201
 
230
202
  async function purgeManagedMarketplaces() {
231
- const marketplaceIds = [
232
- ...new Set(
233
- RECOMMENDED
234
- .concat(REQUIRED)
235
- .map((entry) => entry.marketplaceId)
236
- .filter((id) => id && id !== "claude-plugins-official")
237
- ),
238
- ];
203
+ const marketplaceIds = getManagedMarketplaceIds(RECOMMENDED.concat(REQUIRED));
239
204
 
240
205
  for (const marketplaceId of marketplaceIds) {
241
206
  const result = await removePluginMarketplace(marketplaceId);
@@ -269,26 +234,9 @@ function removeManagedSymlinks() {
269
234
  }
270
235
  }
271
236
 
272
- function removeProtocols() {
273
- log.blank();
274
- console.log(color.dim("Removing global protocols from ~/.claude/CLAUDE.md..."));
275
- try {
276
- const result = removeGlobalProtocols();
277
- if (result.action === "removed") {
278
- log.ok(`Global protocols removed ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
279
- } else if (result.action === "not-present") {
280
- log.info("Global protocols not present, skipping");
281
- } else {
282
- log.info("~/.claude/CLAUDE.md does not exist, skipping");
283
- }
284
- } catch (err) {
285
- log.warn(`Protocol removal failed: ${err.message}`);
286
- }
287
- }
288
-
289
237
  async function maybeRemoveProjectState({ yes }) {
290
238
  log.blank();
291
- log.step(4, 4, "Project state directory");
239
+ log.step(4, UNINSTALL_STEP_COUNT, "Project state directory");
292
240
  const flowDir = join(process.cwd(), ".flow");
293
241
  if (!existsSync(flowDir)) {
294
242
  log.info(".flow/ does not exist, skipping");
@@ -319,27 +267,6 @@ async function maybeRemoveProjectState({ yes }) {
319
267
  }
320
268
  }
321
269
 
322
- function printSummary({ purge }) {
323
- log.blank();
324
- console.log(color.bold("✅ Uninstall complete"));
325
- if (purge) return;
326
-
327
- console.log(
328
- color.dim(
329
- `\nArtifacts kept:\n` +
330
- ` - ~/.local/bin/bun, ~/.local/bin/uv (symlinks; use --purge to remove)\n` +
331
- ` - bun/uv binaries themselves (~/.bun/bin/bun, ~/.local/bin/uv real installs)\n` +
332
- ` - claude-mem data (~/.claude-mem/)\n` +
333
- ` - claude marketplace cache`
334
- )
335
- );
336
- console.log(
337
- color.dim(
338
- `\nFully purge: ${color.cyan("curdx-flow uninstall --purge")}`
339
- )
340
- );
341
- }
342
-
343
270
  function toUninstallTarget(entry) {
344
271
  return {
345
272
  name: entry.name,
@@ -0,0 +1,80 @@
1
+ import { claudeVersion, color, listPlugins, log, resultLastLine } from "./utils.js";
2
+
3
+ export const UPGRADE_STEP_COUNT = 2;
4
+
5
+ export function ensureClaudeCliAvailableForUpgrade(
6
+ { claudeVersionImpl = claudeVersion, logImpl = log, exitImpl = process.exit } = {}
7
+ ) {
8
+ const version = claudeVersionImpl();
9
+ if (!version) {
10
+ logImpl.err("claude CLI not found");
11
+ exitImpl(1);
12
+ return null;
13
+ }
14
+
15
+ return version;
16
+ }
17
+
18
+ export function getInstalledPluginNames({ listPluginsImpl = listPlugins } = {}) {
19
+ return new Set(listPluginsImpl().map((plugin) => plugin.name));
20
+ }
21
+
22
+ export function getPluginNameFromSpec(spec) {
23
+ return String(spec).split("@")[0];
24
+ }
25
+
26
+ export function classifyPluginUpdateResult(result) {
27
+ if (result.code !== 0) {
28
+ return {
29
+ status: "failed",
30
+ message: resultLastLine(result),
31
+ };
32
+ }
33
+
34
+ if (!result.stdout.includes("updated from")) {
35
+ return {
36
+ status: "unchanged",
37
+ message: "already up to date",
38
+ };
39
+ }
40
+
41
+ const match = result.stdout.match(/updated from (\S+) to (\S+)/);
42
+ if (!match) {
43
+ return {
44
+ status: "updated",
45
+ message: "updated",
46
+ };
47
+ }
48
+
49
+ return {
50
+ status: "updated",
51
+ from: match[1],
52
+ to: match[2],
53
+ message: color.dim(`${match[1]} → ${match[2]}`),
54
+ };
55
+ }
56
+
57
+ export async function refreshMarketplaces(
58
+ marketplaceIds,
59
+ { updatePluginMarketplaceImpl, logImpl = log } = {}
60
+ ) {
61
+ logImpl.step(1, UPGRADE_STEP_COUNT, "Refreshing marketplaces...");
62
+
63
+ for (const marketplaceId of marketplaceIds) {
64
+ const result = await updatePluginMarketplaceImpl(marketplaceId);
65
+ if (result.code === 0) {
66
+ logImpl.ok(` ${marketplaceId}`);
67
+ continue;
68
+ }
69
+
70
+ if (!result.stderr.includes("not found")) {
71
+ logImpl.warn(` ${marketplaceId}: ${resultLastLine(result)}`);
72
+ }
73
+ }
74
+ }
75
+
76
+ export function printUpgradeSummary({ logImpl = log } = {}) {
77
+ logImpl.blank();
78
+ logImpl.ok("Upgrade complete");
79
+ console.log(color.dim(" Some changes require a Claude Code restart to take effect"));
80
+ }
package/cli/upgrade.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * upgrade command — update curdx-flow + recommended plugins to latest.
3
3
  */
4
4
 
5
- import { color, log, listPlugins, claudeVersion, resultLastLine } from "./utils.js";
5
+ import { log } from "./utils.js";
6
6
  import {
7
7
  PLUGINS_TO_UPDATE,
8
8
  MARKETPLACES_TO_REFRESH,
@@ -11,61 +11,53 @@ import {
11
11
  updatePlugin,
12
12
  updatePluginMarketplace,
13
13
  } from "./lib/claude-ops.js";
14
+ import {
15
+ classifyPluginUpdateResult,
16
+ ensureClaudeCliAvailableForUpgrade,
17
+ getInstalledPluginNames,
18
+ getPluginNameFromSpec,
19
+ printUpgradeSummary,
20
+ refreshMarketplaces,
21
+ UPGRADE_STEP_COUNT,
22
+ } from "./upgrade-workflow.js";
14
23
 
15
24
  export async function upgrade(args = []) {
16
25
  log.title("⬆️ CurdX-Flow upgrade");
17
26
 
18
- if (!claudeVersion()) {
19
- log.err("claude CLI not found");
20
- process.exit(1);
21
- }
27
+ ensureClaudeCliAvailableForUpgrade();
22
28
 
23
29
  // Refresh marketplaces first (derived from cli/registry.js)
24
- log.step(1, 2, "Refreshing marketplaces...");
25
- for (const mp of MARKETPLACES_TO_REFRESH) {
26
- const r = await updatePluginMarketplace(mp);
27
- if (r.code === 0) {
28
- log.ok(` ${mp}`);
29
- } else {
30
- // Not a fatal — might not be added
31
- if (!r.stderr.includes("not found")) {
32
- log.warn(` ${mp}: ${resultLastLine(r)}`);
33
- }
34
- }
35
- }
30
+ await refreshMarketplaces(MARKETPLACES_TO_REFRESH, {
31
+ updatePluginMarketplaceImpl: updatePluginMarketplace,
32
+ });
36
33
 
37
34
  // Update each plugin
38
35
  log.blank();
39
- log.step(2, 2, "Updating installed plugins...");
40
- const installed = listPlugins();
41
- const installedNames = new Set(installed.map((p) => p.name));
36
+ log.step(2, UPGRADE_STEP_COUNT, "Updating installed plugins...");
37
+ const installedNames = getInstalledPluginNames();
42
38
 
43
39
  for (const spec of PLUGINS_TO_UPDATE) {
44
- const pluginName = spec.split("@")[0];
40
+ const pluginName = getPluginNameFromSpec(spec);
45
41
  if (!installedNames.has(pluginName)) {
46
42
  log.info(` ${pluginName.padEnd(22)} not installed, skipping`);
47
43
  continue;
48
44
  }
49
45
 
50
- const r = await updatePlugin(spec);
51
- if (r.code === 0) {
52
- const updated = r.stdout.includes("updated from");
53
- if (updated) {
54
- const m = r.stdout.match(/updated from (\S+) to (\S+)/);
55
- if (m) {
56
- log.ok(` ${pluginName.padEnd(22)} ${color.dim(`${m[1]} → ${m[2]}`)}`);
57
- } else {
58
- log.ok(` ${pluginName.padEnd(22)} updated`);
59
- }
60
- } else {
61
- log.info(` ${pluginName.padEnd(22)} already up to date`);
62
- }
63
- } else {
64
- log.warn(` ${pluginName.padEnd(22)} ${resultLastLine(r)}`);
46
+ const result = await updatePlugin(spec);
47
+ const status = classifyPluginUpdateResult(result);
48
+
49
+ if (status.status === "updated") {
50
+ log.ok(` ${pluginName.padEnd(22)} ${status.message}`);
51
+ continue;
65
52
  }
53
+
54
+ if (status.status === "unchanged") {
55
+ log.info(` ${pluginName.padEnd(22)} ${status.message}`);
56
+ continue;
57
+ }
58
+
59
+ log.warn(` ${pluginName.padEnd(22)} ${status.message}`);
66
60
  }
67
61
 
68
- log.blank();
69
- log.ok("Upgrade complete");
70
- console.log(color.dim(" Some changes require a Claude Code restart to take effect"));
62
+ printUpgradeSummary();
71
63
  }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+
3
+ has_python3() {
4
+ command -v python3 >/dev/null 2>&1
5
+ }
6
+
7
+ json_escape() {
8
+ local value="${1:-}"
9
+
10
+ if has_python3; then
11
+ printf '%s' "$value" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'
12
+ return
13
+ fi
14
+
15
+ printf '%s' "$value" \
16
+ | sed 's/\\/\\\\/g; s/"/\\"/g' \
17
+ | awk 'BEGIN{printf "\""} {printf "%s\\n", $0} END{printf "\""}'
18
+ }
19
+
20
+ emit_session_start_context() {
21
+ local context="${1:-}"
22
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}\n' \
23
+ "$(json_escape "$context")"
24
+ }
25
+
26
+ emit_pretooluse_deny() {
27
+ local reason="${1:-}"
28
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\n' \
29
+ "$(json_escape "$reason")"
30
+ }
31
+
32
+ emit_stop_block() {
33
+ local reason="${1:-}"
34
+ printf '{"decision":"block","reason":%s}\n' "$(json_escape "$reason")"
35
+ }
@@ -3,14 +3,14 @@
3
3
  # Injects the L1 baseline (Karpathy 4 principles + mandatory tool rules + 3 red lines)
4
4
  # as additionalContext at every session boot (startup, /clear, post-compact).
5
5
  #
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.
6
+ # Wired under SessionStart with matcher "startup|clear|compact" so the
7
+ # baseline survives startup, /clear, and post-compact resume.
11
8
 
12
9
  set -u
13
10
 
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ . "$SCRIPT_DIR/common.sh"
13
+
14
14
  CONTEXT='## CurDX-Flow Mind Baseline (L1 — always on)
15
15
 
16
16
  ### 1. Think Before Coding
@@ -46,10 +46,8 @@ CONTEXT='## CurDX-Flow Mind Baseline (L1 — always on)
46
46
  2. **Fact-driven**: verify before saying "probably". An unverified attribution is blame-shifting
47
47
  3. **Exhaust everything**: before saying "I cannot", complete the systematic 4-stage debugging'
48
48
 
49
- # Emit JSON with safe encoding
50
- if command -v python3 >/dev/null 2>&1; then
51
- ESCAPED="$(printf '%s' "$CONTEXT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')"
52
- printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}\n' "$ESCAPED"
49
+ if has_python3; then
50
+ emit_session_start_context "$CONTEXT"
53
51
  fi
54
52
 
55
53
  exit 0
@@ -8,6 +8,9 @@
8
8
 
9
9
  set -u
10
10
 
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ . "$SCRIPT_DIR/common.sh"
13
+
11
14
  # Read input (Claude sends JSON on stdin for PreToolUse)
12
15
  INPUT=$(cat 2>/dev/null || echo "{}")
13
16
 
@@ -58,8 +61,7 @@ except Exception:
58
61
  if [ "$QUICK_MODE" = "true" ]; then
59
62
  # Block and inject guidance
60
63
  MSG="[CurDX-Flow quick-mode-guard] Active spec '$ACTIVE' is in quick mode or autonomous mode — AskUserQuestion is forbidden. Decide autonomously based on user preferences in .flow/CONTEXT.md plus the most reasonable assumption, and record your assumption in .progress.md."
61
- ESCAPED=$(printf '%s' "$MSG" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')
62
- printf '{"decision":"block","reason":%s}\n' "$ESCAPED"
64
+ emit_pretooluse_deny "$MSG"
63
65
  exit 0
64
66
  fi
65
67
 
@@ -11,6 +11,9 @@
11
11
 
12
12
  set -u
13
13
 
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ . "$SCRIPT_DIR/common.sh"
16
+
14
17
  DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.claude/plugins/data/curdx-flow}"
15
18
  MARKER="$DATA_DIR/.deps-checked"
16
19
  TODAY="$(date +%Y-%m-%d)"
@@ -63,14 +66,7 @@ fi
63
66
 
64
67
  # ---------- 3. Emit hook output ----------
65
68
  if [ -n "$ADDITIONAL_CONTEXT" ]; then
66
- # Use python3 for safe JSON encoding (handles newlines, quotes)
67
- if command -v python3 >/dev/null 2>&1; then
68
- ESCAPED="$(printf '%s' "$ADDITIONAL_CONTEXT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')"
69
- else
70
- # Fallback: naive escape (only newlines and quotes)
71
- ESCAPED="$(printf '%s' "$ADDITIONAL_CONTEXT" | sed 's/\\/\\\\/g; s/"/\\"/g' | awk 'BEGIN{printf "\""} {printf "%s\\n", $0} END{printf "\""}')"
72
- fi
73
- printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}\n' "$ESCAPED"
69
+ emit_session_start_context "$ADDITIONAL_CONTEXT"
74
70
  fi
75
71
 
76
72
  exit 0
@@ -15,6 +15,9 @@
15
15
 
16
16
  set -u
17
17
 
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ . "$SCRIPT_DIR/common.sh"
20
+
18
21
  DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.claude/plugins/data/curdx-flow}"
19
22
 
20
23
  # ---------- helper: exit with "allow stop" ----------
@@ -26,15 +29,7 @@ allow_stop() {
26
29
  # ---------- helper: block and inject continuation ----------
27
30
  block_continue() {
28
31
  local reason="$1"
29
- # Safely JSON-encode via python3 if available
30
- if command -v python3 >/dev/null 2>&1; then
31
- printf '{"decision":"block","reason":%s}\n' \
32
- "$(printf '%s' "$reason" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')"
33
- else
34
- # Fallback: escape quotes and newlines naively
35
- local escaped=$(printf '%s' "$reason" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
36
- printf '{"decision":"block","reason":"%s"}\n' "$escaped"
37
- fi
32
+ emit_stop_block "$reason"
38
33
  exit 0
39
34
  }
40
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "2.0.18",
3
+ "version": "2.0.20",
4
4
  "description": "CLI installer for CurdX-Flow — AI engineering workflow meta-framework for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {