claude-flow 3.32.1 → 3.32.8

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 (51) hide show
  1. package/.claude/helpers/statusline.cjs +24 -17
  2. package/.claude-plugin/scripts/ruflo-hook.cjs +2 -2
  3. package/.claude-plugin/scripts/ruflo-hook.sh +17 -2
  4. package/package.json +1 -1
  5. package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
  6. package/v3/@claude-flow/cli/dist/src/auth/client.d.ts +89 -0
  7. package/v3/@claude-flow/cli/dist/src/auth/client.js +242 -0
  8. package/v3/@claude-flow/cli/dist/src/auth/constants.d.ts +7 -0
  9. package/v3/@claude-flow/cli/dist/src/auth/constants.js +7 -0
  10. package/v3/@claude-flow/cli/dist/src/auth/scopes.d.ts +14 -0
  11. package/v3/@claude-flow/cli/dist/src/auth/scopes.js +21 -0
  12. package/v3/@claude-flow/cli/dist/src/auth/security-bridge.d.ts +36 -0
  13. package/v3/@claude-flow/cli/dist/src/auth/security-bridge.js +42 -0
  14. package/v3/@claude-flow/cli/dist/src/auth/session.d.ts +20 -0
  15. package/v3/@claude-flow/cli/dist/src/auth/session.js +32 -0
  16. package/v3/@claude-flow/cli/dist/src/auth/state.d.ts +19 -0
  17. package/v3/@claude-flow/cli/dist/src/auth/state.js +53 -0
  18. package/v3/@claude-flow/cli/dist/src/auth/types.d.ts +27 -0
  19. package/v3/@claude-flow/cli/dist/src/auth/types.js +11 -0
  20. package/v3/@claude-flow/cli/dist/src/commands/auth.d.ts +15 -0
  21. package/v3/@claude-flow/cli/dist/src/commands/auth.js +244 -0
  22. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +211 -4
  23. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +20 -9
  24. package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
  25. package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.d.ts +20 -0
  26. package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.js +277 -0
  27. package/v3/@claude-flow/cli/dist/src/commands/proxy.js +97 -4
  28. package/v3/@claude-flow/cli/dist/src/commands/security.js +16 -11
  29. package/v3/@claude-flow/cli/dist/src/funnel/insights.d.ts +1 -0
  30. package/v3/@claude-flow/cli/dist/src/funnel/insights.js +4 -4
  31. package/v3/@claude-flow/cli/dist/src/funnel/local-signals.d.ts +6 -1
  32. package/v3/@claude-flow/cli/dist/src/funnel/local-signals.js +29 -20
  33. package/v3/@claude-flow/cli/dist/src/init/executor.js +2 -2
  34. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +13 -1
  35. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +11 -3
  36. package/v3/@claude-flow/cli/dist/src/proxy/install.d.ts +29 -0
  37. package/v3/@claude-flow/cli/dist/src/proxy/install.js +135 -0
  38. package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.d.ts +61 -0
  39. package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.js +249 -0
  40. package/v3/@claude-flow/cli/dist/src/proxy/paths.d.ts +34 -0
  41. package/v3/@claude-flow/cli/dist/src/proxy/paths.js +70 -0
  42. package/v3/@claude-flow/cli/dist/src/proxy/release.d.ts +47 -0
  43. package/v3/@claude-flow/cli/dist/src/proxy/release.js +138 -0
  44. package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.d.ts +5 -0
  45. package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.js +61 -0
  46. package/v3/@claude-flow/cli/dist/src/proxy/verify.d.ts +44 -0
  47. package/v3/@claude-flow/cli/dist/src/proxy/verify.js +68 -0
  48. package/v3/@claude-flow/cli/dist/src/security/builtin-aidefence.d.ts +34 -0
  49. package/v3/@claude-flow/cli/dist/src/security/builtin-aidefence.js +86 -0
  50. package/v3/@claude-flow/cli/dist/src/services/fable-harness.d.ts +1 -0
  51. package/v3/@claude-flow/cli/package.json +2 -2
@@ -23,6 +23,8 @@ import { clearRateLimitStatus, readRateLimitStatus } from '../funnel/rate-limit-
23
23
  import { clearQuotaLowStatus, readQuotaLowStatus } from '../funnel/power-saver-notifier.js';
24
24
  import { getInstalledCliVersion } from '../init/helper-refresh.js';
25
25
  import * as path from 'path';
26
+ import { proxyLifecycleSubcommands, printProxyConsoleGuidance } from './proxy-lifecycle.js';
27
+ import { getProxyStatus } from '../proxy/lifecycle.js';
26
28
  const PROXY_CONFIG_FILE = 'proxy-config.toml';
27
29
  /**
28
30
  * Minimal hand-rolled TOML writer — this config has exactly one boolean
@@ -40,12 +42,12 @@ function readProxyConfigRaw() {
40
42
  return '';
41
43
  }
42
44
  }
43
- function writeConsentMirrorLine(field, value) {
45
+ function writeConfigLine(field, rawValue) {
44
46
  const dir = funnelStateDir();
45
47
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
46
48
  const target = path.join(dir, PROXY_CONFIG_FILE);
47
49
  const raw = readProxyConfigRaw();
48
- const line = `${field} = ${value}`;
50
+ const line = `${field} = ${rawValue}`;
49
51
  const pattern = new RegExp(`^${field}\\s*=.*$`, 'm');
50
52
  let next;
51
53
  if (pattern.test(raw)) {
@@ -58,6 +60,9 @@ function writeConsentMirrorLine(field, value) {
58
60
  fs.writeFileSync(tmp, next, { encoding: 'utf-8', mode: 0o600 });
59
61
  fs.renameSync(tmp, target);
60
62
  }
63
+ function writeConsentMirrorLine(field, value) {
64
+ writeConfigLine(field, String(value));
65
+ }
61
66
  function writeSponsoredConsentMirror(granted) {
62
67
  writeConsentMirrorLine('sponsored_consent_granted', granted);
63
68
  }
@@ -67,6 +72,78 @@ function writePowerSaverConsentMirror(granted) {
67
72
  function writeTrainingShareConsentMirror(granted) {
68
73
  writeConsentMirrorLine('training_share_consent_granted', granted);
69
74
  }
75
+ /**
76
+ * `default_data_plane` — the ADR-304 cloud-routing toggle. Values confirmed
77
+ * against meta-proxy's actual `DataPlane` enum (`src/config.rs`,
78
+ * `#[serde(rename_all = "snake_case")]`): "local" | "cloud" | "sponsored" |
79
+ * "passthrough" (the last two are not written by this command — sponsored
80
+ * is ADR-313's own consent flag, passthrough is the proxy's own default).
81
+ */
82
+ function readDataPlane() {
83
+ const raw = readProxyConfigRaw();
84
+ const match = raw.match(/^default_data_plane\s*=\s*"([^"]*)"/m);
85
+ return match ? match[1] : 'passthrough'; // matches the Rust struct's own default
86
+ }
87
+ function writeDataPlane(plane) {
88
+ writeConfigLine('default_data_plane', `"${plane}"`);
89
+ }
90
+ const CLOUD_ROUTING_DISCLOSURE = [
91
+ 'Enabling cloud routing.',
92
+ '',
93
+ 'With cloud routing ON, prompts for cloud-tier requests are sent to',
94
+ 'api.cognitum.one and forwarded to the selected provider',
95
+ '(Claude / GPT / Gemini / DeepSeek / OpenRouter).',
96
+ '',
97
+ 'Requests routed to local backends never leave this machine.',
98
+ '',
99
+ 'Disable anytime: ruflo proxy config --local-only',
100
+ ].join('\n');
101
+ const configSub = {
102
+ name: 'config',
103
+ description: 'Toggle cloud routing (ADR-304) — local backends only by default',
104
+ options: [
105
+ { name: 'cloud', description: 'Enable cloud routing (requires cloud-routing consent)', type: 'boolean', default: false },
106
+ { name: 'local-only', description: 'Disable cloud routing, revert to local-only routing', type: 'boolean', default: false },
107
+ { name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
108
+ ],
109
+ action: async (ctx) => {
110
+ const wantCloud = Boolean(ctx.flags.cloud);
111
+ const wantLocalOnly = Boolean(ctx.flags.localOnly ?? ctx.flags['local-only']);
112
+ if (wantCloud && wantLocalOnly) {
113
+ output.printError('Pass either --cloud or --local-only, not both.');
114
+ return { success: false, exitCode: 1 };
115
+ }
116
+ if (!wantCloud && !wantLocalOnly) {
117
+ const plane = readDataPlane();
118
+ output.writeln(`Current data plane: ${plane}`);
119
+ output.writeln(plane === 'cloud'
120
+ ? 'Cloud routing is ON — cloud-tier requests go to api.cognitum.one.'
121
+ : 'Cloud routing is OFF — requests never leave this machine (or use your own Claude subscription on Passthrough).');
122
+ return { success: true, data: { plane } };
123
+ }
124
+ if (wantLocalOnly) {
125
+ writeDataPlane('local');
126
+ revokeConsent('cloud-routing', 'proxy-config-local-only');
127
+ output.printSuccess('Cloud routing disabled — reverted to local-only routing.');
128
+ return { success: true, data: { plane: 'local' } };
129
+ }
130
+ // wantCloud
131
+ if (!hasConsent('cloud-routing')) {
132
+ output.writeln(CLOUD_ROUTING_DISCLOSURE);
133
+ output.writeln('');
134
+ if (!ctx.flags.yes) {
135
+ output.writeln('Re-run with --yes to confirm: ruflo proxy config --cloud --yes');
136
+ return { success: true, data: { confirmed: false } };
137
+ }
138
+ recordConsent('cloud-routing', true, 'proxy-config-cloud');
139
+ }
140
+ writeDataPlane('cloud');
141
+ output.printSuccess('Cloud routing enabled.');
142
+ output.writeln(' Requests routed to local backends still never leave this machine.');
143
+ output.writeln(' Disable anytime: ruflo proxy config --local-only');
144
+ return { success: true, data: { plane: 'cloud' } };
145
+ },
146
+ };
70
147
  const SPONSOR_DISCLOSURE = [
71
148
  'Enabling sponsored downtime mode.',
72
149
  '',
@@ -292,19 +369,35 @@ const trainingShareStatusSub = {
292
369
  };
293
370
  export const proxyCommand = {
294
371
  name: 'proxy',
295
- description: 'Meta LLM Proxy — sponsored downtime + power saver + training-data sharing (ADR-304/307/313/314/315)',
372
+ description: 'Meta LLM Proxy — install/lifecycle + sponsored downtime + power saver + training-data sharing (ADR-304/307/313/314/315)',
296
373
  subcommands: [
374
+ ...proxyLifecycleSubcommands,
375
+ configSub,
297
376
  sponsorEnableSub, sponsorDisableSub, sponsorStatusSub, sponsorClearSub,
298
377
  powerSaverEnableSub, powerSaverDisableSub, powerSaverStatusSub, powerSaverClearSub,
299
378
  trainingShareEnableSub, trainingShareDisableSub, trainingShareStatusSub,
300
379
  ],
301
380
  examples: [
381
+ { command: 'ruflo proxy install --yes', description: 'Install the signed Meta-Proxy v0.4.0 binary' },
382
+ { command: 'ruflo proxy start', description: 'Start meta-proxy in the foreground' },
383
+ { command: 'ruflo proxy status', description: 'Show install + process status' },
384
+ { command: 'ruflo proxy config --cloud --yes', description: 'Enable cloud routing (ADR-304)' },
385
+ { command: 'ruflo proxy config --local-only', description: 'Revert to local-only routing' },
302
386
  { command: 'ruflo proxy sponsor-status', description: 'Show current sponsored-mode state' },
303
387
  { command: 'ruflo proxy sponsor-enable --yes', description: 'Opt into sponsored downtime capacity' },
304
388
  { command: 'ruflo proxy power-saver-enable --yes', description: 'Opt into power saver mode' },
305
389
  { command: 'ruflo proxy training-share-enable --yes', description: 'Opt into training-data sharing (ADR-315)' },
306
390
  ],
307
- action: sponsorStatusSub.action,
391
+ action: async () => {
392
+ const status = getProxyStatus();
393
+ output.writeln('Meta Proxy');
394
+ output.writeln(` Installation: ${status.installed ? 'ready' : 'not installed'}`);
395
+ output.writeln(` Process: ${status.running ? `running (pid ${status.pid})` : 'not running'}`);
396
+ if (status.stalePidFile)
397
+ output.writeln(' (a stale PID file was found and will be cleared on next start)');
398
+ printProxyConsoleGuidance(status);
399
+ return { success: true, data: status };
400
+ },
308
401
  };
309
402
  export default proxyCommand;
310
403
  //# sourceMappingURL=proxy.js.map
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { output } from '../output.js';
8
8
  import { execSync } from 'node:child_process';
9
+ import { createBuiltinAIDefence } from '../security/builtin-aidefence.js';
9
10
  // Scan subcommand
10
11
  const scanCommand = {
11
12
  name: 'scan',
@@ -861,16 +862,17 @@ const defendCommand = {
861
862
  output.writeln(output.bold('🛡️ AIDefence - AI Manipulation Defense System'));
862
863
  output.writeln(output.dim('─'.repeat(55)));
863
864
  // Dynamic import of aidefence (allows package to be optional)
864
- let createAIDefence;
865
+ let defender;
865
866
  try {
866
867
  const aidefence = await import('@claude-flow/aidefence');
867
- createAIDefence = aidefence.createAIDefence;
868
+ defender = aidefence.createAIDefence({ enableLearning });
868
869
  }
869
870
  catch {
870
- output.error('AIDefence package not installed. Run: npm install @claude-flow/aidefence');
871
- return { success: false, message: 'AIDefence not available' };
871
+ // Keep cold npx startup lean (#2561): the full learning engine remains
872
+ // user-installable, while the CLI always ships a deterministic scanner.
873
+ defender = createBuiltinAIDefence();
874
+ output.writeln(output.dim('Using built-in defense engine (install @claude-flow/aidefence for adaptive learning)'));
872
875
  }
873
- const defender = createAIDefence({ enableLearning });
874
876
  // Show stats mode
875
877
  if (showStats) {
876
878
  const stats = await defender.getStats();
@@ -893,8 +895,8 @@ const defendCommand = {
893
895
  output.writeln(output.dim(`Reading file: ${filePath}`));
894
896
  }
895
897
  catch (err) {
896
- output.error(`Failed to read file: ${filePath}`);
897
- return { success: false, message: 'File not found' };
898
+ output.printError(`Failed to read file: ${filePath}`);
899
+ return { success: false, exitCode: 2, message: 'File not found' };
898
900
  }
899
901
  }
900
902
  if (!textToScan) {
@@ -914,8 +916,9 @@ const defendCommand = {
914
916
  spinner.start();
915
917
  // Perform scan
916
918
  const startTime = performance.now();
917
- const result = quickMode
918
- ? { ...defender.quickScan(textToScan), threats: [], piiFound: false, detectionTimeMs: 0, inputHash: '', safe: !defender.quickScan(textToScan).threat }
919
+ const quickResult = quickMode ? defender.quickScan(textToScan) : undefined;
920
+ const result = quickResult
921
+ ? { ...quickResult, threats: [], piiFound: false, detectionTimeMs: 0, inputHash: '', safe: !quickResult.threat }
919
922
  : await defender.detect(textToScan);
920
923
  const scanTime = performance.now() - startTime;
921
924
  spinner.stop();
@@ -927,7 +930,8 @@ const defendCommand = {
927
930
  piiFound: result.piiFound,
928
931
  detectionTimeMs: scanTime,
929
932
  }, null, 2));
930
- return { success: true };
933
+ const safe = result.safe && !result.piiFound;
934
+ return { success: safe, exitCode: safe ? 0 : 1 };
931
935
  }
932
936
  // Text output
933
937
  output.writeln();
@@ -969,7 +973,8 @@ const defendCommand = {
969
973
  }
970
974
  }
971
975
  output.writeln(output.dim(`Detection time: ${scanTime.toFixed(3)}ms`));
972
- return { success: result.safe };
976
+ const safe = result.safe && !result.piiFound;
977
+ return { success: safe, exitCode: safe ? 0 : 1 };
973
978
  },
974
979
  };
975
980
  // Main security command
@@ -30,6 +30,7 @@ export interface LocalInsight {
30
30
  export interface LocalInsightContext {
31
31
  security?: {
32
32
  status: string;
33
+ findings?: number;
33
34
  cvesFixed: number;
34
35
  totalCves: number;
35
36
  };
@@ -42,11 +42,11 @@ function securityInsight(ctx) {
42
42
  const s = ctx.security;
43
43
  if (!s)
44
44
  return null;
45
- const pending = s.totalCves - s.cvesFixed;
46
- if (pending > 0) {
45
+ const findings = Math.max(0, s.findings ?? 0);
46
+ if (s.status === 'ISSUES' || findings > 0) {
47
47
  return {
48
- id: 'insight-cves-pending',
49
- text: `⚠ ${pending} CVE${pending === 1 ? '' : 's'} pending Run ruflo security scan --depth full`,
48
+ id: 'insight-security-findings',
49
+ text: `⚠ ${findings} security finding${findings === 1 ? '' : 's'} — Review the latest ruflo security scan`,
50
50
  priority: 90,
51
51
  };
52
52
  }
@@ -1,6 +1,11 @@
1
1
  export interface SecurityStatus {
2
- status: 'CLEAN' | 'IN_PROGRESS' | 'PENDING';
2
+ status: 'CLEAN' | 'ISSUES' | 'PENDING';
3
+ /** Generic code-pattern findings from `ruflo security scan` (not CVEs). */
4
+ findings: number;
5
+ scannedAt?: string;
6
+ /** @deprecated Retained as a zero-valued compatibility field. */
3
7
  cvesFixed: number;
8
+ /** @deprecated Retained as a zero-valued compatibility field. */
4
9
  totalCves: number;
5
10
  }
6
11
  export declare function getSecurityStatus(cwd?: string): SecurityStatus;
@@ -11,30 +11,39 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import { execSync } from 'child_process';
13
13
  export function getSecurityStatus(cwd = process.cwd()) {
14
+ const empty = {
15
+ status: 'PENDING', findings: 0, cvesFixed: 0, totalCves: 0,
16
+ };
14
17
  const scanResultsPath = path.join(cwd, '.claude', 'security-scans');
15
- let cvesFixed = 0;
16
- const totalCves = 3;
17
- if (fs.existsSync(scanResultsPath)) {
18
- try {
19
- const scans = fs.readdirSync(scanResultsPath).filter((f) => f.endsWith('.json'));
20
- cvesFixed = Math.min(totalCves, scans.length);
21
- }
22
- catch {
23
- // Ignore
18
+ if (!fs.existsSync(scanResultsPath))
19
+ return empty;
20
+ try {
21
+ const files = fs.readdirSync(scanResultsPath).filter((f) => f.endsWith('.json'));
22
+ if (files.length === 0)
23
+ return empty;
24
+ let newest = files[0];
25
+ let newestMtime = -1;
26
+ for (const f of files) {
27
+ const st = fs.statSync(path.join(scanResultsPath, f));
28
+ if (st.mtimeMs > newestMtime) {
29
+ newestMtime = st.mtimeMs;
30
+ newest = f;
31
+ }
24
32
  }
33
+ const scan = JSON.parse(fs.readFileSync(path.join(scanResultsPath, newest), 'utf-8'));
34
+ const rawFindings = scan.summary?.total ?? scan.totalFindings ?? scan.findings?.length ?? 0;
35
+ const findings = Math.max(0, Number.isFinite(rawFindings) ? rawFindings : 0);
36
+ return {
37
+ status: findings > 0 ? 'ISSUES' : 'CLEAN',
38
+ findings,
39
+ scannedAt: typeof scan.timestamp === 'string' ? scan.timestamp : undefined,
40
+ cvesFixed: 0,
41
+ totalCves: 0,
42
+ };
25
43
  }
26
- const auditPath = path.join(cwd, '.swarm', 'security');
27
- if (fs.existsSync(auditPath)) {
28
- try {
29
- const audits = fs.readdirSync(auditPath).filter((f) => f.includes('audit'));
30
- cvesFixed = Math.min(totalCves, Math.max(cvesFixed, audits.length));
31
- }
32
- catch {
33
- // Ignore
34
- }
44
+ catch {
45
+ return empty;
35
46
  }
36
- const status = cvesFixed >= totalCves ? 'CLEAN' : cvesFixed > 0 ? 'IN_PROGRESS' : 'PENDING';
37
- return { status, cvesFixed, totalCves };
38
47
  }
39
48
  export function getSwarmStatus() {
40
49
  let activeAgents = 0;
@@ -619,7 +619,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
619
619
  initialized: new Date().toISOString(),
620
620
  status: 'PENDING',
621
621
  cvesFixed: 0,
622
- totalCves: 3,
622
+ totalCves: 0,
623
623
  lastScan: null,
624
624
  _note: 'Run: npx @claude-flow/cli@latest security scan'
625
625
  };
@@ -1541,7 +1541,7 @@ async function writeInitialMetrics(targetDir, options, result) {
1541
1541
  initialized: new Date().toISOString(),
1542
1542
  status: 'PENDING',
1543
1543
  cvesFixed: 0,
1544
- totalCves: 3,
1544
+ totalCves: 0,
1545
1545
  lastScan: null,
1546
1546
  _note: 'Run: npx @claude-flow/cli@latest security scan'
1547
1547
  };
@@ -2010,8 +2010,9 @@ export const hooksSessionEnd = {
2010
2010
  }
2011
2011
  // Phase 5: Wire ReflexionMemory session end + NightlyLearner consolidation via bridge
2012
2012
  let sessionPersistence = null;
2013
+ let bridge = null;
2013
2014
  try {
2014
- const bridge = await import('../memory/memory-bridge.js');
2015
+ bridge = await import('../memory/memory-bridge.js');
2015
2016
  const result = await bridge.bridgeSessionEnd({
2016
2017
  sessionId,
2017
2018
  summary: saveState ? 'Session ended with state saved' : 'Session ended',
@@ -2028,6 +2029,17 @@ export const hooksSessionEnd = {
2028
2029
  catch {
2029
2030
  // Bridge not available
2030
2031
  }
2032
+ finally {
2033
+ // Release AgentDB/ONNX resources after one-shot session persistence.
2034
+ // A partially initialized native pool can otherwise keep Node alive
2035
+ // after the command has completed all logical work (#2691).
2036
+ try {
2037
+ await bridge?.shutdownBridge();
2038
+ }
2039
+ catch {
2040
+ // Cleanup is best-effort and must not fail session-end.
2041
+ }
2042
+ }
2031
2043
  return {
2032
2044
  sessionId,
2033
2045
  duration: 3600000, // 1 hour in ms
@@ -389,7 +389,7 @@ export const memoryTools = [
389
389
  type: 'object',
390
390
  properties: {
391
391
  query: { type: 'string', description: 'Search query (semantic similarity)' },
392
- namespace: { type: 'string', description: 'Namespace to search (default: "default")' },
392
+ namespace: { type: 'string', description: 'Namespace to search (default: all namespaces — omit to search across every namespace)' },
393
393
  limit: { type: 'number', description: 'Maximum results (default: 10)' },
394
394
  threshold: { type: 'number', description: 'Minimum similarity threshold 0-1 (default: 0.3)' },
395
395
  smart: { type: 'boolean', description: 'Enable SmartRetrieval pipeline — query expansion, RRF fusion, recency boost, MMR diversity (default: false)' },
@@ -400,10 +400,18 @@ export const memoryTools = [
400
400
  await ensureInitialized();
401
401
  const { searchEntries } = await getMemoryFunctions();
402
402
  const query = input.query;
403
- const namespace = input.namespace || 'default';
403
+ // #2646 (3rd occurrence of #1123/#1131 shape): do NOT coerce an omitted
404
+ // namespace to the literal string 'default' here. Both searchEntries()
405
+ // and bridgeSearchEntries() already resolve an omitted/undefined
406
+ // namespace to 'all' (fan out across every namespace) — but 'default'
407
+ // is truthy, so passing it defeats that fallback and silently scopes
408
+ // the search to a namespace that is usually empty. Leave namespace
409
+ // undefined when not provided so the underlying `|| 'all'` fallback
410
+ // in the search layer actually fires.
411
+ const namespace = input.namespace;
404
412
  const limit = input.limit ?? 10;
405
413
  const threshold = input.threshold ?? 0.3;
406
- validateMemoryInput(undefined, undefined, query);
414
+ validateMemoryInput(undefined, undefined, query, namespace);
407
415
  const startTime = performance.now();
408
416
  try {
409
417
  // #1846: feature-detect smartSearch on the resolved memory package.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `ruflo proxy install`/`update` orchestration (ADR-307): download -> verify
3
+ * -> extract -> place -> record. The per-user bearer token
4
+ * (`~/.ruflo/proxy-token`) is NOT generated here — confirmed empirically
5
+ * (2026-07-16) that the meta-proxy binary itself creates it on first launch
6
+ * (`load_or_create_token()`), so this module's job ends at a verified binary
7
+ * on disk plus an install manifest doctor can check against.
8
+ *
9
+ * @module proxy/install
10
+ */
11
+ export declare class ExtractionError extends Error {
12
+ constructor(message: string);
13
+ }
14
+ export interface InstallOptions {
15
+ version: string;
16
+ log?: (line: string) => void;
17
+ }
18
+ export interface InstallResult {
19
+ version: string;
20
+ binaryPath: string;
21
+ sha256: string;
22
+ }
23
+ /**
24
+ * Full install pipeline. Refuses (throws) on any verification failure —
25
+ * never writes a partially-verified binary into the live install path.
26
+ */
27
+ export declare function installProxy(opts: InstallOptions): Promise<InstallResult>;
28
+ export declare function uninstallProxy(): Promise<boolean>;
29
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `ruflo proxy install`/`update` orchestration (ADR-307): download -> verify
3
+ * -> extract -> place -> record. The per-user bearer token
4
+ * (`~/.ruflo/proxy-token`) is NOT generated here — confirmed empirically
5
+ * (2026-07-16) that the meta-proxy binary itself creates it on first launch
6
+ * (`load_or_create_token()`), so this module's job ends at a verified binary
7
+ * on disk plus an install manifest doctor can check against.
8
+ *
9
+ * @module proxy/install
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as os from 'node:os';
13
+ import * as path from 'node:path';
14
+ import { fetchReleaseAssets, detectTargetTriple, releaseArchiveExtension, releaseAssetFilename } from './release.js';
15
+ import { verifyRelease, sha256Hex, PROXY_RELEASE_PUBKEY_PEM } from './verify.js';
16
+ import { proxyBinaryPath, proxyInstallManifestPath } from './paths.js';
17
+ export class ExtractionError extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = 'ExtractionError';
21
+ }
22
+ }
23
+ function binaryNameInArchive() {
24
+ return process.platform === 'win32' ? 'meta-proxy.exe' : 'meta-proxy';
25
+ }
26
+ /**
27
+ * Extracts the archive via the OS's own tools — `tar` for `.tar.gz`
28
+ * (present on macOS/Linux/Windows 10+), PowerShell `Expand-Archive`
29
+ * specifically for `.zip` on Windows (not tar's bsdtar zip support — not
30
+ * reliable enough to lean on). Zero new archive-parsing dependency, matching
31
+ * this repo's existing taste for shelling out over adding a parser dep.
32
+ */
33
+ async function extractArchive(archivePath, extractDir, ext) {
34
+ const { SafeExecutor } = await import('@claude-flow/security');
35
+ fs.mkdirSync(extractDir, { recursive: true });
36
+ if (ext === 'tar.gz') {
37
+ const exec = new SafeExecutor({ allowedCommands: ['tar'], timeout: 60_000 });
38
+ const result = await exec.execute('tar', ['xzf', archivePath, '-C', extractDir]);
39
+ if (result.exitCode !== 0) {
40
+ throw new ExtractionError(`tar extraction failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
41
+ }
42
+ return;
43
+ }
44
+ // .zip — PowerShell Expand-Archive, single-quoted literal paths (doubling
45
+ // any embedded single quote per PowerShell string-literal escaping) passed
46
+ // as ONE argv element to -Command. shell:false means no OS shell ever
47
+ // tokenizes this string — only powershell.exe's own parser does.
48
+ const escape = (p) => p.replace(/'/g, "''");
49
+ const command = `Expand-Archive -LiteralPath '${escape(archivePath)}' -DestinationPath '${escape(extractDir)}' -Force`;
50
+ const exec = new SafeExecutor({ allowedCommands: ['powershell', 'powershell.exe'], timeout: 60_000 });
51
+ const result = await exec.execute('powershell', ['-NoProfile', '-NonInteractive', '-Command', command]);
52
+ if (result.exitCode !== 0) {
53
+ throw new ExtractionError(`Expand-Archive failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
54
+ }
55
+ }
56
+ /**
57
+ * Full install pipeline. Refuses (throws) on any verification failure —
58
+ * never writes a partially-verified binary into the live install path.
59
+ */
60
+ export async function installProxy(opts) {
61
+ const log = opts.log ?? (() => { });
62
+ const triple = detectTargetTriple();
63
+ const archiveFilename = releaseAssetFilename(opts.version, triple);
64
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruflo-proxy-install-'));
65
+ try {
66
+ log(`Fetching meta-proxy ${opts.version} (${triple})...`);
67
+ const assets = await fetchReleaseAssets(opts.version, triple, workDir, log);
68
+ log('Verifying release signature and checksum...');
69
+ const { sha256 } = verifyRelease({
70
+ sumsBytes: assets.sumsBytes,
71
+ sigBase64: assets.sigBase64,
72
+ assetBytes: assets.archiveBytes,
73
+ assetFilename: assets.archiveFilename,
74
+ });
75
+ log(`Verified — sha256 ${sha256.slice(0, 16)}…`);
76
+ // fetchReleaseAssets's dev (gh) path already wrote the archive to workDir
77
+ // under archiveFilename; ensure it's there regardless of source so
78
+ // extraction always has a real file to operate on.
79
+ const archivePath = path.join(workDir, archiveFilename);
80
+ if (!fs.existsSync(archivePath)) {
81
+ fs.writeFileSync(archivePath, assets.archiveBytes);
82
+ }
83
+ const extractDir = path.join(workDir, 'extracted');
84
+ await extractArchive(archivePath, extractDir, releaseArchiveExtension(triple));
85
+ const extractedBinaryPath = path.join(extractDir, binaryNameInArchive());
86
+ if (!fs.existsSync(extractedBinaryPath)) {
87
+ throw new ExtractionError(`archive did not contain the expected binary at its root: ${binaryNameInArchive()}`);
88
+ }
89
+ // Defense in depth: confirm the extracted binary genuinely resolves
90
+ // inside extractDir (catches a symlink swap or similar), even though
91
+ // we only ever read one specific expected relative path, never an
92
+ // archive-listed one (so "zip slip" via arbitrary archive paths isn't
93
+ // reachable here in the first place).
94
+ const { PathValidator } = await import('@claude-flow/security');
95
+ const validator = new PathValidator({ allowedPrefixes: [extractDir] });
96
+ const validation = await validator.validate(extractedBinaryPath);
97
+ if (!validation.isValid) {
98
+ throw new ExtractionError(`extracted binary path failed validation: ${validation.errors.join('; ') || 'unknown'}`);
99
+ }
100
+ const finalPath = proxyBinaryPath();
101
+ fs.mkdirSync(path.dirname(finalPath), { recursive: true, mode: 0o700 });
102
+ const tmp = `${finalPath}.tmp`;
103
+ fs.copyFileSync(extractedBinaryPath, tmp);
104
+ fs.chmodSync(tmp, 0o755);
105
+ fs.renameSync(tmp, finalPath);
106
+ const liveSha = sha256Hex(fs.readFileSync(finalPath));
107
+ const manifest = {
108
+ version: opts.version,
109
+ sha256: liveSha,
110
+ verifiedAt: new Date().toISOString(),
111
+ pubkeyFingerprint: sha256Hex(Buffer.from(PROXY_RELEASE_PUBKEY_PEM)).slice(0, 16),
112
+ };
113
+ fs.mkdirSync(path.dirname(proxyInstallManifestPath()), { recursive: true });
114
+ fs.writeFileSync(proxyInstallManifestPath(), JSON.stringify(manifest, null, 2), { mode: 0o600 });
115
+ log(`meta-proxy ${opts.version} installed at ${finalPath}`);
116
+ return { version: opts.version, binaryPath: finalPath, sha256: liveSha };
117
+ }
118
+ finally {
119
+ fs.rmSync(workDir, { recursive: true, force: true });
120
+ }
121
+ }
122
+ export async function uninstallProxy() {
123
+ const binPath = proxyBinaryPath();
124
+ const existed = fs.existsSync(binPath);
125
+ if (existed)
126
+ fs.unlinkSync(binPath);
127
+ try {
128
+ fs.unlinkSync(proxyInstallManifestPath());
129
+ }
130
+ catch {
131
+ /* absent — fine */
132
+ }
133
+ return existed;
134
+ }
135
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * meta-proxy process lifecycle (ADR-307) — start/stop/status/logs.
3
+ *
4
+ * Adapts daemon.ts's proven pattern (PID file, O_EXCL lockfile for atomic
5
+ * check-then-start, signal-0 liveness, SIGTERM->1000ms->SIGKILL) to a
6
+ * native binary instead of a forked Node process. The binary itself takes
7
+ * no CLI flags — confirmed empirically (2026-07-16): `meta-proxy.exe` has
8
+ * no `--version`/`--help`, and any invocation just starts the server reading
9
+ * its own config file — so `spawn()` here passes zero arguments, always.
10
+ *
11
+ * Foreground `start` (the ADR-307 default) uses `stdio: 'inherit'` and
12
+ * blocks directly — simplest and safest, no log-file redirection needed.
13
+ * `start --service` needs REAL file-descriptor redirection
14
+ * (`stdio: ['ignore', fd, fd]`) + `detached: true` + `unref()` via
15
+ * `child_process.spawn()` directly — `SafeExecutor.executeStreaming()`
16
+ * buffers output in-process, which is wrong for a process meant to outlive
17
+ * the `ruflo` invocation that started it.
18
+ *
19
+ * @module proxy/lifecycle
20
+ */
21
+ import * as fs from 'node:fs';
22
+ export declare class ProxyNotInstalledError extends Error {
23
+ constructor();
24
+ }
25
+ export declare class ProxyAlreadyRunningError extends Error {
26
+ readonly pid: number;
27
+ constructor(pid: number);
28
+ }
29
+ export interface ProxyStatus {
30
+ installed: boolean;
31
+ running: boolean;
32
+ pid: number | null;
33
+ stalePidFile: boolean;
34
+ }
35
+ export declare function getProxyStatus(): ProxyStatus;
36
+ /**
37
+ * Foreground start (ADR-307 default) — blocks the caller until the process
38
+ * exits or is interrupted. `stdio: 'inherit'` passes the proxy's own output
39
+ * straight through to the terminal; signals (Ctrl+C) propagate naturally to
40
+ * the child, no manual forwarding needed.
41
+ */
42
+ export declare function startForeground(supervised?: boolean): Promise<never>;
43
+ /**
44
+ * Background/`--service` start — detaches so the process outlives this
45
+ * `ruflo` invocation, redirecting stdout/stderr to a real log file (not
46
+ * buffered in-process). Returns once the child's PID is confirmed written,
47
+ * without waiting for the process to exit.
48
+ */
49
+ export declare function startBackground(): Promise<{
50
+ pid: number;
51
+ }>;
52
+ export interface StopResult {
53
+ wasRunning: boolean;
54
+ pid: number | null;
55
+ }
56
+ /** SIGTERM -> 1000ms -> SIGKILL if still alive, mirroring daemon.ts's killBackgroundDaemon. */
57
+ export declare function stopProxy(): Promise<StopResult>;
58
+ export declare function readProxyLogTail(maxBytes?: number): string;
59
+ /** Streams new log lines as they're appended, starting from the current end of file. */
60
+ export declare function watchProxyLog(onLine: (line: string) => void): fs.FSWatcher;
61
+ //# sourceMappingURL=lifecycle.d.ts.map