@claude-flow/cli 3.45.0 → 3.46.1

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.
Files changed (77) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/README.md +2 -0
  3. package/bin/cli.js +13 -0
  4. package/bin/mcp-server.js +16 -0
  5. package/catalog-manifest.json +2 -2
  6. package/dist/src/commands/doctor.d.ts +3 -0
  7. package/dist/src/commands/doctor.js +131 -2
  8. package/dist/src/commands/embeddings.js +50 -7
  9. package/dist/src/commands/init.js +5 -2
  10. package/dist/src/commands/mcp.js +27 -4
  11. package/dist/src/mcp-server.d.ts +50 -6
  12. package/dist/src/mcp-server.js +267 -33
  13. package/dist/src/mcp-tools/agent-execute-core.js +7 -0
  14. package/dist/src/mcp-tools/agentdb-tools.js +20 -8
  15. package/dist/src/mcp-tools/browser-tools.js +59 -48
  16. package/dist/src/mcp-tools/memory-tools.js +9 -4
  17. package/dist/src/mcp-tools/metaharness-tools.js +1 -1
  18. package/dist/src/mcp-tools/policy-enforcer.d.ts +4 -4
  19. package/dist/src/mcp-tools/policy-enforcer.js +4 -4
  20. package/dist/src/mcp-tools/terminal-tools.js +11 -1
  21. package/dist/src/mcp-tools/workflow-tools.js +1 -1
  22. package/dist/src/memory/ewc-consolidation.d.ts +16 -1
  23. package/dist/src/memory/ewc-consolidation.js +63 -13
  24. package/dist/src/memory/intelligence.d.ts +6 -2
  25. package/dist/src/memory/intelligence.js +18 -11
  26. package/dist/src/memory/memory-bridge.js +84 -0
  27. package/dist/src/runtime/browser-command.d.ts +29 -0
  28. package/dist/src/runtime/browser-command.js +101 -0
  29. package/dist/src/ruvector/model-router.js +19 -1
  30. package/dist/src/services/distill-oracle.js +2 -1
  31. package/dist/src/services/git-workspace-identity.js +5 -8
  32. package/dist/src/services/policy-runtime.js +114 -4
  33. package/dist/src/services/worker-daemon.js +8 -3
  34. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  35. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.d.ts.map +1 -1
  36. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.js +2 -6
  37. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.js.map +1 -1
  38. package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
  39. package/node_modules/@claude-flow/mcp/dist/transport/http.js +14 -1
  40. package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
  41. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts +40 -0
  42. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts.map +1 -0
  43. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.js +100 -0
  44. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.js.map +1 -0
  45. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  46. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts +1 -0
  47. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts.map +1 -1
  48. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.js +1 -0
  49. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.js.map +1 -1
  50. package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -0
  51. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  52. package/node_modules/@claude-flow/security/dist/safe-git.d.ts +22 -0
  53. package/node_modules/@claude-flow/security/dist/safe-git.d.ts.map +1 -0
  54. package/node_modules/@claude-flow/security/dist/safe-git.js +63 -0
  55. package/node_modules/@claude-flow/security/dist/safe-git.js.map +1 -0
  56. package/package.json +5 -5
  57. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +1 -1
  58. package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +1 -1
  59. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +84 -12
  60. package/plugins/ruflo-metaharness/scripts/_harness.mjs +10 -4
  61. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +120 -6
  62. package/plugins/ruflo-metaharness/scripts/_redblue.mjs +33 -6
  63. package/plugins/ruflo-metaharness/scripts/audit-list.mjs +11 -10
  64. package/plugins/ruflo-metaharness/scripts/audit-trend.mjs +8 -7
  65. package/plugins/ruflo-metaharness/scripts/drift-from-history.mjs +2 -3
  66. package/plugins/ruflo-metaharness/scripts/oia-audit.mjs +8 -7
  67. package/plugins/ruflo-metaharness/scripts/similarity.mjs +9 -8
  68. package/plugins/ruflo-metaharness/scripts/smoke.sh +31 -0
  69. package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +319 -0
  70. package/plugins/ruflo-metaharness/scripts/test-pin-alignment.mjs +182 -0
  71. package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +4 -2
  72. package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
  73. package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +1 -1
  74. package/plugins/ruflo-metaharness/skills/harness-mcp-scan/SKILL.md +1 -1
  75. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
  76. package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +3 -1
  77. package/plugins/ruflo-metaharness/skills/harness-threat-model/SKILL.md +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.45.0",
3
+ "version": "3.46.1",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "85fe05c757421c52137c0bc8545a0896bab6b4714538c2a11d1d0835bfcc8c1c",
6
6
  "hook-handler.cjs": "209d9fafe10e17d1be0866727f6f9cf9ac66f9a0793f1c793a4f58319e8e4583",
@@ -9,6 +9,6 @@
9
9
  "router.js": "b6998397e7883191b62229ccdc66fb87f1ccca1d5b2039ecb8c1e686d1ad81f8"
10
10
  }
11
11
  },
12
- "signature": "IO42tJDeyKIl8l7ewYgoPdlZqQdsESvjIJr3KoAHDYHTi7gMCElwEyah4jx/RC6+Lq2zXv8XSOd/cgHI2U5zAg==",
12
+ "signature": "F1SDoeGrsywBpjrcev0ITXmm1FHHi2PuKGrZGudnoH96qyOhbYfvxhvlQIeusWqN7theLd6k3PQ6/67nCjCKAw==",
13
13
  "algorithm": "ed25519"
14
14
  }
package/README.md CHANGED
@@ -19,6 +19,8 @@
19
19
 
20
20
  # Ruflo
21
21
 
22
+ [English](README.md) · [简体中文](README.zh-CN.md)
23
+
22
24
  **An agent meta-harness for Claude Code and Codex.**
23
25
 
24
26
  [![RuFlo Explained — build an AI team that plans, remembers, tests, and improves](docs/assets/ruflo-explained/ch14.jpg)](docs/ruflo-explained.md)
package/bin/cli.js CHANGED
@@ -164,6 +164,8 @@ const isMCPMode = !process.stdin.isTTY
164
164
  if (isMCPMode) {
165
165
  // Run MCP server mode
166
166
  const { listMCPTools, callMCPTool, hasTool } = await import('../dist/src/mcp-client.js');
167
+ const { isPolicyEnforcementEnabled, loadMcpPolicy, evaluateToolCall } =
168
+ await import('../dist/src/mcp-tools/policy-enforcer.js');
167
169
 
168
170
  const VERSION = '3.0.0';
169
171
  const sessionId = `mcp-${Date.now()}-${randomUUID().slice(0, 8)}`;
@@ -287,6 +289,17 @@ if (isMCPMode) {
287
289
  };
288
290
  }
289
291
 
292
+ if (isPolicyEnforcementEnabled()) {
293
+ const check = evaluateToolCall(loadMcpPolicy(), sessionId, toolName);
294
+ if (!check.allowed) {
295
+ return {
296
+ jsonrpc: '2.0',
297
+ id: message.id,
298
+ error: { code: -32001, message: `Policy denied: ${check.reason}` },
299
+ };
300
+ }
301
+ }
302
+
290
303
  try {
291
304
  const result = await callMCPTool(toolName, toolParams, { sessionId });
292
305
  return {
package/bin/mcp-server.js CHANGED
@@ -28,6 +28,11 @@ console.log = (...args) => {
28
28
  };
29
29
 
30
30
  import { listMCPTools, callMCPTool, hasTool } from '../dist/src/mcp-client.js';
31
+ import {
32
+ isPolicyEnforcementEnabled,
33
+ loadMcpPolicy,
34
+ evaluateToolCall,
35
+ } from '../dist/src/mcp-tools/policy-enforcer.js';
31
36
 
32
37
  const VERSION = '3.0.0';
33
38
  const sessionId = `mcp-${Date.now()}-${randomUUID().slice(0, 8)}`;
@@ -173,6 +178,17 @@ async function handleMessage(message) {
173
178
  };
174
179
  }
175
180
 
181
+ if (isPolicyEnforcementEnabled()) {
182
+ const check = evaluateToolCall(loadMcpPolicy(), sessionId, toolName);
183
+ if (!check.allowed) {
184
+ return {
185
+ jsonrpc: '2.0',
186
+ id: message.id,
187
+ error: { code: -32001, message: `Policy denied: ${check.reason}` },
188
+ };
189
+ }
190
+ }
191
+
176
192
  try {
177
193
  const result = await callMCPTool(toolName, toolParams, { sessionId });
178
194
  return {
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 6,
4
- "generatedAt": "2026-09-24T22:48:10.625Z",
5
- "gitSha": "aab1d625",
4
+ "generatedAt": "2026-09-26T23:25:25.296Z",
5
+ "gitSha": "a5adb5aa",
6
6
  "catalog": {
7
7
  "agents": 167,
8
8
  "tools": 418,
@@ -11,6 +11,9 @@ interface HealthCheck {
11
11
  message: string;
12
12
  fix?: string;
13
13
  }
14
+ /** ADR-122 Phase 0: probe only the CLI version, without launching a browser. */
15
+ export declare function evaluateAgentBrowserVersion(rawOutput: string): HealthCheck;
16
+ export declare function checkAgentBrowserVersion(probe?: () => Promise<string>): Promise<HealthCheck>;
14
17
  export declare function checkMemoryPersistenceDriver(): Promise<HealthCheck>;
15
18
  /**
16
19
  * #3392: pure verdict for "does the @claude-flow/memory the CLI loads satisfy
@@ -9,7 +9,7 @@ import { existsSync, readFileSync, statSync, openSync, readSync, closeSync } fro
9
9
  import { join, dirname } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { createHash } from 'crypto';
12
- import { execSync, exec } from 'child_process';
12
+ import { execSync, exec, execFile } from 'child_process';
13
13
  import { promisify } from 'util';
14
14
  import { decodeKey, isEncryptionEnabled } from '../encryption/vault.js';
15
15
  import { isEncryptedBlob } from '../encryption/vault.js';
@@ -17,6 +17,7 @@ import * as semver from 'semver';
17
17
  import { resolveMemoryPackageFromProject, resolveMemoryPackageFromCli, readMemoryPackageVersion, recordMemoryPackagePath, } from '../init/memory-package-resolver.js';
18
18
  // Promisified exec with proper shell and env inheritance for cross-platform support
19
19
  const execAsync = promisify(exec);
20
+ const execFileAsync = promisify(execFile);
20
21
  /**
21
22
  * Execute command asynchronously with proper environment inheritance
22
23
  * Critical for Windows where PATH may not be inherited properly
@@ -62,6 +63,39 @@ async function checkNpmVersion() {
62
63
  return { name: 'npm Version', status: 'fail', message: 'npm not found', fix: 'Install Node.js from https://nodejs.org' };
63
64
  }
64
65
  }
66
+ /** ADR-122 Phase 0: probe only the CLI version, without launching a browser. */
67
+ export function evaluateAgentBrowserVersion(rawOutput) {
68
+ const name = 'agent-browser CLI (ADR-122)';
69
+ const fix = 'npm install -g agent-browser@latest';
70
+ const versionOutput = rawOutput.trim().replace(/\x1b\[[0-9;]*m/g, '');
71
+ const match = /^(?:agent-browser(?:\s+version)?\s+)?v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\s|$)/i.exec(versionOutput);
72
+ const version = match ? semver.parse(match[1]) : null;
73
+ if (!version) {
74
+ return { name, status: 'warn', message: `Unable to parse agent-browser version${versionOutput ? `: ${versionOutput.slice(0, 120)}` : ' (empty output)'}. Browser MCP tools may be unavailable.`, fix };
75
+ }
76
+ if (semver.lt(version, '0.27.0')) {
77
+ return { name, status: 'warn', message: `v${version.version} is below the ADR-122 v0.27.0 minimum for browser MCP tools`, fix };
78
+ }
79
+ return { name, status: 'pass', message: `v${version.version} meets the ADR-122 v0.27.0 minimum for browser MCP tools` };
80
+ }
81
+ export async function checkAgentBrowserVersion(probe = async () => {
82
+ const { stdout } = await execFileAsync('agent-browser', ['--version'], {
83
+ encoding: 'utf8', timeout: 3000, maxBuffer: 16 * 1024, windowsHide: true,
84
+ });
85
+ return String(stdout);
86
+ }) {
87
+ try {
88
+ return evaluateAgentBrowserVersion(await probe());
89
+ }
90
+ catch (error) {
91
+ const name = 'agent-browser CLI (ADR-122)';
92
+ const fix = 'npm install -g agent-browser@latest';
93
+ const code = error.code;
94
+ return code === 'ENOENT'
95
+ ? { name, status: 'warn', message: 'Not found on PATH; @claude-flow/browser and browser MCP tools need agent-browser v0.27.0 or newer', fix }
96
+ : { name, status: 'warn', message: `Version check failed: ${error instanceof Error ? error.message : String(error)}`, fix };
97
+ }
98
+ }
65
99
  // Check config file
66
100
  async function checkConfigFile() {
67
101
  // JSON configs (parse-validated). The first three are LEGACY shapes from
@@ -306,6 +340,22 @@ async function resolveMemoryDbPath() {
306
340
  return p;
307
341
  return null;
308
342
  }
343
+ /** Native AgentDB writes to a plaintext sibling of the sql.js memory file.
344
+ * Probe only stores that already exist: projects without the native bridge
345
+ * should not acquire an extra warning just by running doctor (#3195). */
346
+ async function existingNativeAgentDbPaths() {
347
+ const candidates = new Set();
348
+ try {
349
+ const { getMemoryRoot, resolveDbPath } = await import('../memory/memory-initializer.js');
350
+ candidates.add(join(getMemoryRoot(), 'agentdb-memory.db'));
351
+ // Explicit CLAUDE_FLOW_DB_PATH callers may put the sibling elsewhere.
352
+ candidates.add(join(dirname(resolveDbPath()), 'agentdb-memory.db'));
353
+ }
354
+ catch {
355
+ candidates.add(join(process.cwd(), '.swarm', 'agentdb-memory.db'));
356
+ }
357
+ return [...candidates].filter((path) => existsSync(path));
358
+ }
309
359
  /** Open a sql.js Database over an on-disk file, returning null when the
310
360
  * file can't be opened as a SQLite database (encrypted / corrupted / not
311
361
  * a database). Callers decide whether that's warn or fail. */
@@ -507,6 +557,79 @@ async function checkMemoryStructuralIntegrity() {
507
557
  catch { /* best-effort */ }
508
558
  }
509
559
  }
560
+ /** Check the AgentDB authority independently from the legacy memory.db.
561
+ * Opening read-only with SQLite keeps WAL commits visible without mutating
562
+ * either store; the existing legacy health checks retain their own scope. */
563
+ async function checkNativeAgentDbStructuralIntegrity(dbPath) {
564
+ const NAME = 'Native AgentDB Structural Integrity (quick_check)';
565
+ if (!existsSync(dbPath)) {
566
+ return { name: NAME, status: 'warn', message: `${dbPath} disappeared before the native check could run` };
567
+ }
568
+ if (isMemoryDbEncryptedAtRest(dbPath)) {
569
+ return {
570
+ name: NAME,
571
+ status: 'fail',
572
+ message: `${dbPath} — RFE1-encrypted; native AgentDB requires a plaintext SQLite file`,
573
+ fix: `back up ${dbPath} and restore a usable native AgentDB database`,
574
+ };
575
+ }
576
+ let Database;
577
+ try {
578
+ Database = (await import('better-sqlite3')).default;
579
+ }
580
+ catch {
581
+ return {
582
+ name: NAME,
583
+ status: 'warn',
584
+ message: `${dbPath} — better-sqlite3 not installed; native AgentDB structural health is unverified`,
585
+ fix: 'install better-sqlite3 with npm install scripts enabled, then rerun this check',
586
+ };
587
+ }
588
+ let db;
589
+ try {
590
+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
591
+ }
592
+ catch (error) {
593
+ if (isNativeSqliteBindingUnavailable(error)) {
594
+ return nativeBindingUnavailableCheck(NAME, dbPath, error, '[native AgentDB structural health unverified]');
595
+ }
596
+ const message = error instanceof Error ? error.message : String(error);
597
+ return {
598
+ name: NAME,
599
+ status: 'fail',
600
+ message: `${dbPath} — better-sqlite3 failed to open: ${message} [native AgentDB]`,
601
+ fix: `back up ${dbPath} and its WAL sidecars, then restore a known-good native AgentDB database`,
602
+ };
603
+ }
604
+ try {
605
+ const rows = db.pragma('quick_check');
606
+ const values = rows.map((row) => String(Object.values(row)[0]));
607
+ if (values.length === 1 && values[0] === 'ok') {
608
+ return { name: NAME, status: 'pass', message: `${dbPath} — PRAGMA quick_check: ok [native AgentDB, WAL-aware]` };
609
+ }
610
+ return {
611
+ name: NAME,
612
+ status: 'fail',
613
+ message: `${dbPath} — PRAGMA quick_check: ${values.slice(0, 3).join('; ')}${values.length > 3 ? ` (+${values.length - 3} more)` : ''} [native AgentDB]`,
614
+ fix: `back up ${dbPath} and its WAL sidecars, then restore a known-good native AgentDB database`,
615
+ };
616
+ }
617
+ catch (error) {
618
+ const message = error instanceof Error ? error.message : String(error);
619
+ return {
620
+ name: NAME,
621
+ status: 'fail',
622
+ message: `${dbPath} — quick_check probe threw: ${message} [native AgentDB]`,
623
+ fix: `back up ${dbPath} and its WAL sidecars, then restore a known-good native AgentDB database`,
624
+ };
625
+ }
626
+ finally {
627
+ try {
628
+ db.close();
629
+ }
630
+ catch { /* best-effort */ }
631
+ }
632
+ }
510
633
  // #2968/#3321 — read-only native SQLite capability probe. A skipped
511
634
  // postinstall can leave the wrapper importable but its binding unavailable.
512
635
  // Schema size cannot identify the runtime driver or the database's history:
@@ -2337,7 +2460,7 @@ export const doctorCommand = {
2337
2460
  {
2338
2461
  name: 'component',
2339
2462
  short: 'c',
2340
- description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, mcp-overhead, claude, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, typesafe, metaharness)',
2463
+ description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, mcp-overhead, claude, browser, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, typesafe, metaharness)',
2341
2464
  type: 'string'
2342
2465
  },
2343
2466
  {
@@ -2444,11 +2567,14 @@ export const doctorCommand = {
2444
2567
  output.writeln(output.dim('System diagnostics and health check'));
2445
2568
  output.writeln(output.dim('─'.repeat(50)));
2446
2569
  output.writeln();
2570
+ const nativeAgentDbChecks = (await existingNativeAgentDbPaths())
2571
+ .map((dbPath) => () => checkNativeAgentDbStructuralIntegrity(dbPath));
2447
2572
  const allChecks = [
2448
2573
  checkVersionFreshness,
2449
2574
  checkNodeVersion,
2450
2575
  checkNpmVersion,
2451
2576
  checkClaudeCode,
2577
+ checkAgentBrowserVersion, // ADR-122 Phase 0 — browser runtime compatibility
2452
2578
  checkGit,
2453
2579
  checkGitRepo,
2454
2580
  checkConfigFile,
@@ -2456,6 +2582,7 @@ export const doctorCommand = {
2456
2582
  checkDaemonStatus,
2457
2583
  checkMemoryDatabase,
2458
2584
  checkMemoryStructuralIntegrity, // #2737 — bounded, native quick_check on every default run
2585
+ ...nativeAgentDbChecks, // #3195 — verify each present native authority file
2459
2586
  checkMemoryPersistenceDriver, // #2968/#3321 — read-only native capability probe
2460
2587
  checkLearningBridge, // #2545 — can the auto-memory hook actually load @claude-flow/memory?
2461
2588
  checkMemoryPackageVersion, // #3392 — loaded @claude-flow/memory must satisfy the CLI's declared range
@@ -2489,12 +2616,14 @@ export const doctorCommand = {
2489
2616
  'node': checkNodeVersion,
2490
2617
  'npm': checkNpmVersion,
2491
2618
  'claude': checkClaudeCode,
2619
+ 'browser': checkAgentBrowserVersion,
2492
2620
  'config': checkConfigFile,
2493
2621
  'stale-settings': checkStaleSettingsNpx, // #2448
2494
2622
  'daemon': checkDaemonStatus,
2495
2623
  'memory': [
2496
2624
  checkMemoryDatabase, // existing: exists + statable (unchanged)
2497
2625
  checkMemoryIntegrity, // #2677 check 1: sql.js open + PRAGMA integrity_check
2626
+ ...nativeAgentDbChecks, // #3195: present AgentDB authority, checked independently
2498
2627
  checkMemoryPersistenceDriver, // #2968/#3321: read-only native capability probe
2499
2628
  checkMemoryPackageVersion, // #3392: loaded memory package satisfies the declared range
2500
2629
  checkMemoryContent, // #2677 check 2: memory_entries content coverage
@@ -679,7 +679,12 @@ const initCommand = {
679
679
  if (!fs.existsSync(modelDir)) {
680
680
  fs.mkdirSync(modelDir, { recursive: true });
681
681
  }
682
- // Download model if requested
682
+ // Download model if requested. #3376: record what actually happened
683
+ // instead of assuming the download ran — `getEmbeddings()` returns null
684
+ // whenever the optional @claude-flow/embeddings package is absent, which
685
+ // is every plain `npm i -g ruflo` install.
686
+ let modelDownloaded = false;
687
+ let modelSkipReason = null;
683
688
  if (download) {
684
689
  spinner.setText(`Downloading ONNX model: ${model}...`);
685
690
  const embeddings = await getEmbeddings();
@@ -687,13 +692,19 @@ const initCommand = {
687
692
  await embeddings.downloadEmbeddingModel(model, modelDir, (p) => {
688
693
  spinner.setText(`Downloading ${model}... ${p.percent.toFixed(0)}%`);
689
694
  });
695
+ modelDownloaded = true;
690
696
  }
691
697
  else {
692
- // Embeddings package not available — skip download
693
- await new Promise(r => setTimeout(r, 500));
694
- output.writeln(output.dim(' (Skipped — @claude-flow/embeddings not installed)'));
698
+ // No download happened. That is the outcome of this command, not an
699
+ // aside — reported below via spinner.fail + a warning, never as a
700
+ // dimmed note on a success path. No fake progress delay: there is
701
+ // nothing in progress.
702
+ modelSkipReason = '@claude-flow/embeddings is not installed';
695
703
  }
696
704
  }
705
+ else {
706
+ modelSkipReason = 'download disabled by --no-download';
707
+ }
697
708
  // Write embeddings config
698
709
  spinner.setText('Writing configuration...');
699
710
  const dimension = model.includes('mpnet') ? 768 : 384;
@@ -702,6 +713,12 @@ const initCommand = {
702
713
  modelPath: modelDir,
703
714
  dimension,
704
715
  cacheSize,
716
+ // #3376: additive, optional fields. `modelPath` keeps its old meaning
717
+ // (the directory the model belongs in), but it no longer implies a
718
+ // model is in it — `modelDownloaded` says whether one is. Readers that
719
+ // predate these keys are unaffected; they ignore unknown properties.
720
+ modelDownloaded,
721
+ ...(modelSkipReason ? { modelSkipReason } : {}),
705
722
  hyperbolic: {
706
723
  enabled: hyperbolic,
707
724
  curvature,
@@ -716,7 +733,15 @@ const initCommand = {
716
733
  initialized: new Date().toISOString(),
717
734
  };
718
735
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
719
- spinner.succeed('Embedding subsystem initialized');
736
+ // A download that was asked for and did not happen is a failed init:
737
+ // the only substantive step of `embeddings init` did not run. #3376.
738
+ const downloadSkipped = download && !modelDownloaded;
739
+ if (downloadSkipped) {
740
+ spinner.fail('Embedding model NOT downloaded — embedding subsystem is not ready');
741
+ }
742
+ else {
743
+ spinner.succeed('Embedding subsystem initialized');
744
+ }
720
745
  output.writeln();
721
746
  output.printTable({
722
747
  columns: [
@@ -725,6 +750,7 @@ const initCommand = {
725
750
  ],
726
751
  data: [
727
752
  { setting: 'Model', value: model },
753
+ { setting: 'Model Status', value: modelDownloaded ? output.success('Downloaded') : output.error('NOT downloaded') },
728
754
  { setting: 'Dimension', value: String(dimension) },
729
755
  { setting: 'Cache Size', value: String(cacheSize) + ' entries' },
730
756
  { setting: 'Hyperbolic', value: hyperbolic ? `${output.success('Enabled')} (c=${curvature})` : output.dim('Disabled') },
@@ -733,6 +759,20 @@ const initCommand = {
733
759
  { setting: 'Config', value: configPath },
734
760
  ],
735
761
  });
762
+ if (downloadSkipped) {
763
+ output.writeln();
764
+ output.printWarning(`No embedding model was installed: ${modelSkipReason}.`);
765
+ output.printWarning('Until a real embedder is present, embeddings fall back to hash vectors, which carry no semantic meaning.');
766
+ output.printInfo(`Configuration was still written to ${configPath}, recording "modelDownloaded": false.`);
767
+ output.writeln();
768
+ output.writeln(output.dim('To finish initializing:'));
769
+ output.printList([
770
+ 'npm install @claude-flow/embeddings - the package this command downloads with; then: embeddings init --force',
771
+ 'npm install ruvector - the other real embedder ruflo accepts; needs no download step',
772
+ 'embeddings init --no-download --force - keep this configuration without a model (exits 0)',
773
+ ]);
774
+ return { success: false, exitCode: 1, data: config };
775
+ }
736
776
  output.writeln();
737
777
  if (hyperbolic) {
738
778
  output.printBox([
@@ -1182,8 +1222,11 @@ const modelsCommand = {
1182
1222
  }
1183
1223
  }
1184
1224
  else {
1185
- await new Promise(r => setTimeout(r, 500));
1186
- spinner.succeed(`Download skipped — @claude-flow/embeddings not installed`);
1225
+ // #3376, same class as `init`: nothing was downloaded, so this is a
1226
+ // failure, not a success with a caveat. No fake progress delay either.
1227
+ spinner.fail(`Download skipped — @claude-flow/embeddings is not installed`);
1228
+ output.printWarning(`"${download}" was not downloaded. Install @claude-flow/embeddings to enable model downloads.`);
1229
+ return { success: false, exitCode: 1 };
1187
1230
  }
1188
1231
  return { success: true };
1189
1232
  }
@@ -113,7 +113,9 @@ export function runCodexInitializerCli(cwd, options) {
113
113
  // init) and scripted `--format json` output.
114
114
  async function maybeAutoDetectCodex(ctx, options) {
115
115
  try {
116
- if (ctx.flags['no-codex-detect'] === true)
116
+ // The CLI parser turns `--no-codex-detect` into `codexDetect: false`.
117
+ // Retain the literal key for programmatic CommandContext callers.
118
+ if (ctx.flags.codexDetect === false || ctx.flags['no-codex-detect'] === true)
117
119
  return;
118
120
  if (ctx.flags.format === 'json')
119
121
  return; // scripted output stays pure
@@ -295,7 +297,8 @@ function isBloatedRufloSkillDir(dir) {
295
297
  }
296
298
  async function maybeInstallSkillsSh(ctx) {
297
299
  try {
298
- if (ctx.flags['no-skills-sh'] === true)
300
+ // The CLI parser turns `--no-skills-sh` into `skillsSh: false`.
301
+ if (ctx.flags.skillsSh === false || ctx.flags['no-skills-sh'] === true)
299
302
  return;
300
303
  if (ctx.flags.format === 'json')
301
304
  return;
@@ -66,6 +66,11 @@ const startCommand = {
66
66
  description: 'Tools to advertise (comma-separated categories, prefixes, exact names, or "all")',
67
67
  type: 'string'
68
68
  },
69
+ {
70
+ name: 'request-timeout-ms',
71
+ description: 'HTTP request timeout in milliseconds (default 30000; env RUFLO_MCP_REQUEST_TIMEOUT_MS)',
72
+ type: 'number'
73
+ },
69
74
  {
70
75
  name: 'daemon',
71
76
  short: 'd',
@@ -84,6 +89,7 @@ const startCommand = {
84
89
  examples: [
85
90
  { command: 'claude-flow mcp start', description: 'Start with defaults (stdio)' },
86
91
  { command: 'claude-flow mcp start -p 8080 -t http', description: 'Start HTTP server' },
92
+ { command: 'claude-flow mcp start -t http --request-timeout-ms 120000', description: 'Allow slower cold-start HTTP tools' },
87
93
  { command: 'claude-flow mcp start -d', description: 'Start as daemon' },
88
94
  { command: 'claude-flow mcp start -f', description: 'Force restart (kill existing)' }
89
95
  ],
@@ -94,6 +100,15 @@ const startCommand = {
94
100
  const tools = ctx.flags.tools
95
101
  || process.env.CLAUDE_FLOW_MCP_TOOLS
96
102
  || 'all';
103
+ const timeoutInput = ctx.flags.requestTimeoutMs
104
+ ?? ctx.flags['request-timeout-ms']
105
+ ?? process.env.RUFLO_MCP_REQUEST_TIMEOUT_MS;
106
+ const requestTimeoutMs = timeoutInput === undefined ? undefined : Number(timeoutInput);
107
+ if (requestTimeoutMs !== undefined &&
108
+ (!Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 1 || requestTimeoutMs > 3_600_000)) {
109
+ output.printError('Request timeout must be an integer from 1 to 3600000 milliseconds');
110
+ return { success: false, exitCode: 1 };
111
+ }
97
112
  const daemon = ctx.flags.daemon ?? false;
98
113
  const force = ctx.flags.force ?? false;
99
114
  output.writeln();
@@ -104,10 +119,17 @@ const startCommand = {
104
119
  // PID file exists, which would cause us to SIGKILL ourselves)
105
120
  const existingStatus = await getMCPServerStatus();
106
121
  const isSelfDetected = existingStatus.pid === process.pid;
107
- if (existingStatus.running && !isSelfDetected) {
108
- // For stdio transport, always force restart since we can't health check it
109
- // For other transports, check health unless --force is specified
110
- const shouldForceRestart = force || transport === 'stdio';
122
+ // #3364: only a port-bound transport (http/websocket) is single-instance.
123
+ // A stdio server belongs to the client that spawned it, over that client's
124
+ // own pipes, and any number run side by side. The PID file is a single
125
+ // slot per os.tmpdir(), so for stdio "already running" only ever meant
126
+ // "some other server is recorded" — and force-restarting SIGKILLed it
127
+ // (another terminal's stdio server, or a live http server). Stdio now
128
+ // kills a recorded server only when explicitly asked with --force.
129
+ const isSingleInstance = transport !== 'stdio';
130
+ if (existingStatus.running && !isSelfDetected && (isSingleInstance || force)) {
131
+ // --force always restarts; otherwise check health (http/websocket)
132
+ const shouldForceRestart = force;
111
133
  if (!shouldForceRestart) {
112
134
  // Verify the server is actually healthy/responsive
113
135
  const manager = getServerManager();
@@ -143,6 +165,7 @@ const startCommand = {
143
165
  host,
144
166
  port,
145
167
  tools: !tools || tools === 'all' ? 'all' : tools.split(','),
168
+ ...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
146
169
  daemonize: daemon,
147
170
  };
148
171
  try {
@@ -29,6 +29,7 @@ export interface MCPServerOptions {
29
29
  tools?: string[] | 'all';
30
30
  daemonize?: boolean;
31
31
  timeout?: number;
32
+ requestTimeoutMs?: number;
32
33
  }
33
34
  /**
34
35
  * MCP Server status
@@ -85,6 +86,21 @@ export declare class MCPServerManager extends EventEmitter {
85
86
  private startTime?;
86
87
  private healthCheckInterval?;
87
88
  private mcpServers;
89
+ /**
90
+ * This manager's own lifecycle (#3364). A stdio server keeps no PID record,
91
+ * so nothing on disk can answer "is this manager running?" — only the
92
+ * manager can. `idle` is a manager that has never started, and only there
93
+ * does the #2934 fallback in getStatus() ("assume a client-launched stdio
94
+ * server") apply; `stopped` is a handle on a server that is gone, and it
95
+ * says so rather than reporting whatever PID the shared file happens to hold.
96
+ */
97
+ private lifecycle;
98
+ /**
99
+ * The exact bytes this manager wrote to the PID file, while they are still
100
+ * there (#3364). stop() retracts the record only while it is byte for byte
101
+ * the one we wrote: the slot may have changed hands in between.
102
+ */
103
+ private ownedRecord;
88
104
  constructor(options?: MCPServerOptions);
89
105
  /**
90
106
  * Start the MCP server
@@ -136,15 +152,43 @@ export declare class MCPServerManager extends EventEmitter {
136
152
  */
137
153
  private startHealthMonitoring;
138
154
  /**
139
- * Write PID file
155
+ * Write the PID record.
156
+ *
157
+ * Line 1 is the bare PID, byte for byte what this file has always held, so
158
+ * every existing reader keeps working: an older ruflo's
159
+ * `parseInt(content.trim(), 10)` stops at the newline, and
160
+ * `v3/scripts/start-mcp.sh` reads the first line. Line 2 is the durable
161
+ * identity of the instance that wrote it (#3364). Returns the bytes written,
162
+ * which stop() uses to retract only its own record.
140
163
  */
141
164
  private writePidFile;
142
165
  /**
143
- * Read PID file
144
- */
145
- private readPidFile;
146
- /**
147
- * Remove PID file
166
+ * Read the PID record.
167
+ *
168
+ * A bare-integer file — an older ruflo, `start-mcp.sh --daemon`, or a
169
+ * hand-written one — parses to a record with no identity, which keeps
170
+ * exactly the old behaviour: the PID is checked for liveness and nothing
171
+ * more. An identity line is only believed for the PID it names.
172
+ */
173
+ private readPidRecord;
174
+ /**
175
+ * Is the recorded server still the instance the record names? (#3364)
176
+ *
177
+ * `kill -0`, and isProcessRunning()'s process-name check on top of it, only
178
+ * answer "something with this number is alive". Durable identity answers
179
+ * "it is still the one we wrote down":
180
+ * - another host, OS, kernel boot or PID namespace issues its own PIDs, so
181
+ * the number says nothing here. On Linux this is the ordinary stale case,
182
+ * because /tmp commonly survives a reboot;
183
+ * - within one boot the OS reuses a PID once the process is reaped, and the
184
+ * owner's start time is what tells the two apart.
185
+ * Where the start time cannot be read — Windows — the answer falls back to
186
+ * isProcessRunning(), i.e. exactly the evidence used today.
187
+ */
188
+ private recordedServerIsLive;
189
+ /**
190
+ * Remove PID file. With `ownedRecord`, only while the file still holds
191
+ * exactly those bytes: the slot may have changed hands (#3364).
148
192
  */
149
193
  private removePidFile;
150
194
  /**