claude-flow 3.6.26 → 3.6.28

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.
@@ -0,0 +1 @@
1
+ {"sessionId":"1dba3b8c-1f1f-4718-bab4-9ffba5f49578","pid":20633,"procStart":"Mon May 4 17:00:40 2026","acquiredAt":1777938041240}
package/README.md CHANGED
@@ -43,9 +43,17 @@ User --> Ruflo (CLI/MCP) --> Router --> Swarm --> Agents --> Memory --> LLM Prov
43
43
 
44
44
  ## Quick Start
45
45
 
46
- ### Claude Code Plugin (Recommended)
46
+ There are **two different install paths** with very different surface areas. Pick based on what you need (#1744):
47
47
 
48
- Install Ruflo as a native Claude Code plugin -- adds skills, commands, agents, and MCP tools directly:
48
+ | | **Claude Code Plugin** | **CLI install (`npx ruflo init`)** |
49
+ |---|---|---|
50
+ | What it gives you | Slash commands + a few skills + agent definitions per-plugin | Full Ruflo loop — 98 agents, 60+ commands, 30 skills, MCP server, hooks, daemon |
51
+ | Files in your workspace | **Zero** | `.claude/`, `.claude-flow/`, `CLAUDE.md`, helpers, settings |
52
+ | MCP server registered | **No** (`memory_store`, `swarm_init`, etc. unavailable to Claude) | Yes |
53
+ | Hooks installed | No | Yes |
54
+ | Best for | Try a single plugin's commands without committing to the full install | Production use — everything works as documented |
55
+
56
+ ### Path A — Claude Code Plugins (lite, slash commands only)
49
57
 
50
58
  ```bash
51
59
  # Add the marketplace
@@ -58,6 +66,8 @@ Install Ruflo as a native Claude Code plugin -- adds skills, commands, agents, a
58
66
  /plugin install ruflo-federation@ruflo
59
67
  ```
60
68
 
69
+ This adds slash commands and agent definitions only. The Ruflo MCP server is NOT registered, so `memory_store`, `swarm_init`, `agent_spawn`, etc. won't be callable from Claude. For the full loop, use Path B below.
70
+
61
71
  <details>
62
72
  <summary><strong>All 32 plugins</strong></summary>
63
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.6.26",
3
+ "version": "3.6.28",
4
4
  "description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -43,9 +43,17 @@ User --> Ruflo (CLI/MCP) --> Router --> Swarm --> Agents --> Memory --> LLM Prov
43
43
 
44
44
  ## Quick Start
45
45
 
46
- ### Claude Code Plugin (Recommended)
46
+ There are **two different install paths** with very different surface areas. Pick based on what you need (#1744):
47
47
 
48
- Install Ruflo as a native Claude Code plugin -- adds skills, commands, agents, and MCP tools directly:
48
+ | | **Claude Code Plugin** | **CLI install (`npx ruflo init`)** |
49
+ |---|---|---|
50
+ | What it gives you | Slash commands + a few skills + agent definitions per-plugin | Full Ruflo loop — 98 agents, 60+ commands, 30 skills, MCP server, hooks, daemon |
51
+ | Files in your workspace | **Zero** | `.claude/`, `.claude-flow/`, `CLAUDE.md`, helpers, settings |
52
+ | MCP server registered | **No** (`memory_store`, `swarm_init`, etc. unavailable to Claude) | Yes |
53
+ | Hooks installed | No | Yes |
54
+ | Best for | Try a single plugin's commands without committing to the full install | Production use — everything works as documented |
55
+
56
+ ### Path A — Claude Code Plugins (lite, slash commands only)
49
57
 
50
58
  ```bash
51
59
  # Add the marketplace
@@ -58,6 +66,8 @@ Install Ruflo as a native Claude Code plugin -- adds skills, commands, agents, a
58
66
  /plugin install ruflo-federation@ruflo
59
67
  ```
60
68
 
69
+ This adds slash commands and agent definitions only. The Ruflo MCP server is NOT registered, so `memory_store`, `swarm_init`, `agent_spawn`, etc. won't be callable from Claude. For the full loop, use Path B below.
70
+
61
71
  <details>
62
72
  <summary><strong>All 32 plugins</strong></summary>
63
73
 
@@ -145,6 +145,7 @@ const initAction = async (ctx) => {
145
145
  const full = ctx.flags.full;
146
146
  const skipClaude = ctx.flags['skip-claude'];
147
147
  const onlyClaude = ctx.flags['only-claude'];
148
+ const noGlobal = ctx.flags['no-global'];
148
149
  const codexMode = ctx.flags.codex;
149
150
  const dualMode = ctx.flags.dual;
150
151
  const cwd = ctx.cwd;
@@ -203,6 +204,12 @@ const initAction = async (ctx) => {
203
204
  if (onlyClaude) {
204
205
  options.components.runtime = false;
205
206
  }
207
+ // #1744 — opt-out of the user-global ~/.claude/CLAUDE.md "Ruflo Integration"
208
+ // pointer block. Default behavior (off) preserves current install for users
209
+ // who rely on it; opting in via --no-global keeps the global file pristine.
210
+ if (noGlobal) {
211
+ options.skipGlobalClaudeMd = true;
212
+ }
206
213
  // Create spinner
207
214
  const spinner = output.createSpinner({ text: 'Initializing...' });
208
215
  spinner.start();
@@ -919,6 +926,12 @@ export const initCommand = {
919
926
  type: 'boolean',
920
927
  default: false,
921
928
  },
929
+ {
930
+ name: 'no-global',
931
+ description: 'Skip the ~/.claude/CLAUDE.md "Ruflo Integration" pointer block (#1744)',
932
+ type: 'boolean',
933
+ default: false,
934
+ },
922
935
  {
923
936
  name: 'start-all',
924
937
  description: 'Auto-start daemon, memory, and swarm after init',
@@ -498,7 +498,7 @@ const optimizeCommand = {
498
498
  data: [
499
499
  { priority: output.error('P0'), area: 'Memory', recommendation: 'Enable HNSW index quantization', impact: '+50% reduction' },
500
500
  { priority: output.warning('P1'), area: 'CPU', recommendation: 'Enable WASM SIMD acceleration', impact: '+4x speedup' },
501
- { priority: output.warning('P1'), area: 'Latency', recommendation: 'Enable Flash Attention', impact: '+2.49x speedup' },
501
+ { priority: output.warning('P1'), area: 'Latency', recommendation: 'Flash Attention WASM (in progress, currently JS reference)', impact: '+2.49x target' },
502
502
  { priority: output.info('P2'), area: 'Cache', recommendation: 'Increase pattern cache size', impact: '+15% hit rate' },
503
503
  { priority: output.info('P2'), area: 'Network', recommendation: 'Enable request batching', impact: '-30% latency' },
504
504
  ],
@@ -536,7 +536,7 @@ const bottleneckCommand = {
536
536
  ],
537
537
  data: [
538
538
  { component: 'Vector Search', bottleneck: 'Linear scan O(n)', severity: output.error('High'), solution: 'Enable HNSW indexing' },
539
- { component: 'Neural Inference', bottleneck: 'Sequential attention', severity: output.warning('Medium'), solution: 'Enable Flash Attention' },
539
+ { component: 'Neural Inference', bottleneck: 'Sequential attention', severity: output.warning('Medium'), solution: 'Flash Attention WASM (in progress)' },
540
540
  { component: 'Memory Store', bottleneck: 'Lock contention', severity: output.info('Low'), solution: 'Use sharded storage' },
541
541
  ],
542
542
  });
@@ -571,7 +571,7 @@ export const performanceCommand = {
571
571
  output.writeln('Performance Targets:');
572
572
  output.printList([
573
573
  'HNSW Search: 150x-12,500x faster than brute force',
574
- 'Flash Attention: 2.49x-7.47x speedup',
574
+ 'Flash Attention: 2.49x-7.47x target (in progress; ships JS reference impl)',
575
575
  'Memory: 50-75% reduction with quantization',
576
576
  ]);
577
577
  output.writeln();
@@ -11,6 +11,9 @@ const PROVIDER_CATALOG = [
11
11
  { name: 'OpenAI', type: 'LLM', models: 'gpt-4o, gpt-4-turbo', envVar: 'OPENAI_API_KEY', configName: 'openai' },
12
12
  { name: 'OpenAI', type: 'Embedding', models: 'text-embedding-3-small/large', envVar: 'OPENAI_API_KEY', configName: 'openai' },
13
13
  { name: 'Google', type: 'LLM', models: 'gemini-pro, gemini-ultra', envVar: 'GOOGLE_API_KEY', configName: 'google' },
14
+ // #1725: Ollama Cloud — Tier-2 default per ADR-026 (~$100/mo flat-rate alternative
15
+ // to per-token pricing). OpenAI-compat API at https://ollama.com/v1/chat/completions.
16
+ { name: 'Ollama', type: 'LLM', models: 'gpt-oss:120b-cloud, llama3:70b-cloud, qwen2.5-coder:32b-cloud', envVar: 'OLLAMA_API_KEY', configName: 'ollama' },
14
17
  { name: 'Transformers.js', type: 'Embedding', models: 'Xenova/all-MiniLM-L6-v2' },
15
18
  { name: 'Agentic Flow', type: 'Embedding', models: 'ONNX optimized' },
16
19
  { name: 'Mock', type: 'All', models: 'mock-*' },
@@ -30,6 +33,7 @@ function resolveApiKey(providerName, configuredProviders) {
30
33
  anthropic: 'ANTHROPIC_API_KEY',
31
34
  openai: 'OPENAI_API_KEY',
32
35
  google: 'GOOGLE_API_KEY',
36
+ ollama: 'OLLAMA_API_KEY', // #1725 — Tier-2 routing
33
37
  };
34
38
  const envVar = envMapping[providerName.toLowerCase()];
35
39
  if (envVar && process.env[envVar]) {
@@ -60,6 +64,13 @@ async function testProviderConnectivity(providerName, apiKey) {
60
64
  url: `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`,
61
65
  headers: {},
62
66
  },
67
+ // #1725 — Ollama Cloud uses an OpenAI-compatible /v1 surface.
68
+ ollama: {
69
+ url: 'https://ollama.com/api/tags',
70
+ headers: {
71
+ 'Authorization': `Bearer ${apiKey}`,
72
+ },
73
+ },
63
74
  };
64
75
  const endpointConfig = endpoints[providerName.toLowerCase()];
65
76
  if (!endpointConfig) {
@@ -1662,9 +1662,11 @@ async function writeClaudeMd(targetDir, options, result) {
1662
1662
  fs.writeFileSync(claudeMdPath, content, 'utf-8');
1663
1663
  result.created.files.push('CLAUDE.md');
1664
1664
  }
1665
- // Also write/append global ~/.claude/CLAUDE.md so ruflo tools are used automatically (#1497)
1665
+ // Also write/append global ~/.claude/CLAUDE.md so ruflo tools are used automatically (#1497).
1666
+ // Opt-out via --no-global / options.skipGlobalClaudeMd (#1744 — keeps global rules file pristine
1667
+ // for users who don't want a per-machine pointer block).
1666
1668
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
1667
- if (homeDir) {
1669
+ if (homeDir && !options.skipGlobalClaudeMd) {
1668
1670
  const globalClaudeDir = path.join(homeDir, '.claude');
1669
1671
  const globalClaudeMd = path.join(globalClaudeDir, 'CLAUDE.md');
1670
1672
  const rufloBlock = [
@@ -1695,6 +1697,9 @@ async function writeClaudeMd(targetDir, options, result) {
1695
1697
  // Non-critical — global CLAUDE.md is best-effort
1696
1698
  }
1697
1699
  }
1700
+ else if (options.skipGlobalClaudeMd) {
1701
+ result.skipped.push('~/.claude/CLAUDE.md (--no-global)');
1702
+ }
1698
1703
  }
1699
1704
  /**
1700
1705
  * Find source directory for skills/commands/agents
@@ -8,8 +8,13 @@ import { detectPlatform } from './types.js';
8
8
  */
9
9
  export function generateSettings(options) {
10
10
  const settings = {};
11
- // Add hooks if enabled
12
- if (options.components.settings) {
11
+ // Add hooks if enabled. CRITICAL (#1744 #3): only emit the hooks block when
12
+ // the helpers directory will also be bundled. The hook commands point at
13
+ // .claude/helpers/hook-handler.cjs; if that file isn't created (as in
14
+ // --minimal where components.helpers=false), every hook fires and silently
15
+ // fails to find its handler. Either bundle the helpers OR drop the hooks —
16
+ // the option this fix takes is the latter (minimal stays minimal).
17
+ if (options.components.settings && options.components.helpers) {
13
18
  settings.hooks = generateHooksConfig(options.hooks);
14
19
  }
15
20
  // Add statusLine configuration if enabled
@@ -260,6 +260,12 @@ export interface InitOptions {
260
260
  runtime: RuntimeConfig;
261
261
  /** Embeddings configuration */
262
262
  embeddings: EmbeddingsConfig;
263
+ /**
264
+ * Skip the user-global ~/.claude/CLAUDE.md "Ruflo Integration" pointer block.
265
+ * Defaults to false (current behavior — block is appended once, idempotent).
266
+ * Set true via --no-global to keep the global Claude rules file pristine (#1744).
267
+ */
268
+ skipGlobalClaudeMd?: boolean;
263
269
  }
264
270
  /**
265
271
  * Default init options - full V3 setup
@@ -32,6 +32,7 @@ import { githubTools } from './mcp-tools/github-tools.js';
32
32
  import { daaTools } from './mcp-tools/daa-tools.js';
33
33
  import { coordinationTools } from './mcp-tools/coordination-tools.js';
34
34
  import { browserTools } from './mcp-tools/browser-tools.js';
35
+ import { browserSessionTools } from './mcp-tools/browser-session-tools.js';
35
36
  import { execFileSync } from 'node:child_process';
36
37
  // Phase 6: AgentDB v3 controller tools
37
38
  import { agentdbTools } from './mcp-tools/agentdb-tools.js';
@@ -54,6 +55,15 @@ function getBrowserTools() {
54
55
  }
55
56
  return _browserAvailable ? browserTools : [];
56
57
  }
58
+ /**
59
+ * Lifecycle MCP tools for ruflo-browser session-as-skill architecture
60
+ * (ADR-0001 ruflo-browser §7). Always registered: their handlers shell out
61
+ * to ruvector + agent-browser + claude-flow memory and degrade gracefully
62
+ * when those CLIs are missing.
63
+ */
64
+ function getBrowserSessionTools() {
65
+ return browserSessionTools;
66
+ }
57
67
  /**
58
68
  * MCP Tool Registry
59
69
  * Maps tool names to their handler functions
@@ -91,6 +101,7 @@ registerTools([
91
101
  ...daaTools,
92
102
  ...coordinationTools,
93
103
  ...getBrowserTools(),
104
+ ...getBrowserSessionTools(),
94
105
  // Phase 6: AgentDB v3 controller tools
95
106
  ...agentdbTools,
96
107
  // RuVector WASM tools
@@ -47,6 +47,12 @@ export interface AnthropicCallResult {
47
47
  * Generic Anthropic Messages API call. No agent registry coupling — used
48
48
  * by agent_execute (with the agent's configured model) and by the WASM
49
49
  * agent runtime (G4) when the bundled WASM only echoes input.
50
+ *
51
+ * #1725 — falls back to Ollama Cloud (Tier-2, OpenAI-compat) when
52
+ * ANTHROPIC_API_KEY is unset and OLLAMA_API_KEY is present, or when
53
+ * RUFLO_PROVIDER=ollama is explicitly set. Response shape is normalized
54
+ * to the Anthropic-flavored AnthropicCallResult so existing callers
55
+ * don't need to know which provider answered.
50
56
  */
51
57
  export declare function callAnthropicMessages(input: AnthropicCallInput): Promise<AnthropicCallResult>;
52
58
  /**
@@ -42,11 +42,26 @@ const MODEL_MAP = {
42
42
  * Generic Anthropic Messages API call. No agent registry coupling — used
43
43
  * by agent_execute (with the agent's configured model) and by the WASM
44
44
  * agent runtime (G4) when the bundled WASM only echoes input.
45
+ *
46
+ * #1725 — falls back to Ollama Cloud (Tier-2, OpenAI-compat) when
47
+ * ANTHROPIC_API_KEY is unset and OLLAMA_API_KEY is present, or when
48
+ * RUFLO_PROVIDER=ollama is explicitly set. Response shape is normalized
49
+ * to the Anthropic-flavored AnthropicCallResult so existing callers
50
+ * don't need to know which provider answered.
45
51
  */
46
52
  export async function callAnthropicMessages(input) {
47
- const apiKey = process.env.ANTHROPIC_API_KEY;
48
- if (!apiKey) {
49
- return { success: false, error: 'ANTHROPIC_API_KEY not set in environment' };
53
+ const explicitProvider = (process.env.RUFLO_PROVIDER || '').toLowerCase();
54
+ const ollamaKey = process.env.OLLAMA_API_KEY;
55
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
56
+ const useOllama = explicitProvider === 'ollama' || (!anthropicKey && !!ollamaKey);
57
+ if (useOllama && ollamaKey) {
58
+ return callOllamaCompat({ ...input, apiKey: ollamaKey });
59
+ }
60
+ if (!anthropicKey) {
61
+ return {
62
+ success: false,
63
+ error: 'No LLM provider configured. Set ANTHROPIC_API_KEY (Tier-3) or OLLAMA_API_KEY (Tier-2 Ollama Cloud — see issue #1725).',
64
+ };
50
65
  }
51
66
  const model = input.model || 'claude-3-5-sonnet-latest';
52
67
  const startedAt = Date.now();
@@ -56,7 +71,7 @@ export async function callAnthropicMessages(input) {
56
71
  const res = await fetch('https://api.anthropic.com/v1/messages', {
57
72
  method: 'POST',
58
73
  headers: {
59
- 'x-api-key': apiKey,
74
+ 'x-api-key': anthropicKey,
60
75
  'anthropic-version': '2023-06-01',
61
76
  'content-type': 'application/json',
62
77
  },
@@ -102,6 +117,98 @@ export async function callAnthropicMessages(input) {
102
117
  };
103
118
  }
104
119
  }
120
+ /**
121
+ * Ollama Cloud / OpenAI-compat provider — Tier-2 routing per ADR-026 + #1725.
122
+ *
123
+ * Endpoint: https://ollama.com/v1/chat/completions
124
+ * Auth: Authorization: Bearer <OLLAMA_API_KEY>
125
+ *
126
+ * Translates the Anthropic-flavored input shape onto OpenAI chat-completions
127
+ * and translates the response back so callers never see provider-specific
128
+ * fields. Logical model names are mapped to Ollama Cloud defaults:
129
+ * - 'haiku' / 'sonnet' → 'gpt-oss:120b-cloud' (sensible single default)
130
+ * - 'opus' → 'gpt-oss:120b-cloud' (no opus tier on Ollama)
131
+ * - explicit 'ollama:<model>' or bare provider-native name → passed through
132
+ */
133
+ async function callOllamaCompat(input) {
134
+ const model = resolveOllamaModel(input.model);
135
+ const startedAt = Date.now();
136
+ // OLLAMA_BASE_URL lets users point at local/self-hosted endpoints
137
+ // (e.g. http://ruvultra:11434, http://localhost:11434) instead of
138
+ // Ollama Cloud. Default is the public cloud endpoint.
139
+ const base = (process.env.OLLAMA_BASE_URL || 'https://ollama.com').replace(/\/+$/, '');
140
+ const url = `${base}/v1/chat/completions`;
141
+ // Self-hosted endpoints typically don't need an Authorization header
142
+ // (the daemon binds to 11434 with no auth by default), but Ollama Cloud
143
+ // does. Send the bearer when the key is non-empty AND looks cloud-shaped.
144
+ const sendAuth = input.apiKey && input.apiKey !== 'local';
145
+ try {
146
+ const controller = new AbortController();
147
+ const timer = setTimeout(() => controller.abort(), input.timeoutMs || 60000);
148
+ const res = await fetch(url, {
149
+ method: 'POST',
150
+ headers: {
151
+ ...(sendAuth ? { Authorization: `Bearer ${input.apiKey}` } : {}),
152
+ 'content-type': 'application/json',
153
+ },
154
+ body: JSON.stringify({
155
+ model,
156
+ max_tokens: input.maxTokens || 1024,
157
+ temperature: typeof input.temperature === 'number' ? input.temperature : 0.7,
158
+ messages: [
159
+ ...(input.systemPrompt
160
+ ? [{ role: 'system', content: input.systemPrompt }]
161
+ : []),
162
+ { role: 'user', content: input.prompt },
163
+ ],
164
+ }),
165
+ signal: controller.signal,
166
+ });
167
+ clearTimeout(timer);
168
+ if (!res.ok) {
169
+ const errText = await res.text().catch(() => '<unreadable error body>');
170
+ return { success: false, model, error: `Ollama API error ${res.status} at ${url}: ${errText.slice(0, 400)}` };
171
+ }
172
+ const data = (await res.json());
173
+ const textOut = data.choices?.[0]?.message?.content ?? '';
174
+ const usage = data.usage ?? {};
175
+ return {
176
+ success: true,
177
+ model: data.model ?? model,
178
+ messageId: data.id ?? `ollama-${Date.now()}`,
179
+ stopReason: data.choices?.[0]?.finish_reason ?? 'end_turn',
180
+ output: textOut,
181
+ usage: {
182
+ inputTokens: usage.prompt_tokens ?? 0,
183
+ outputTokens: usage.completion_tokens ?? 0,
184
+ totalTokens: usage.total_tokens ?? 0,
185
+ },
186
+ durationMs: Date.now() - startedAt,
187
+ };
188
+ }
189
+ catch (err) {
190
+ return {
191
+ success: false,
192
+ model,
193
+ error: err instanceof Error ? err.message : String(err),
194
+ durationMs: Date.now() - startedAt,
195
+ };
196
+ }
197
+ }
198
+ function resolveOllamaModel(input) {
199
+ const DEFAULT = 'gpt-oss:120b-cloud';
200
+ if (!input)
201
+ return DEFAULT;
202
+ // Logical → cloud default
203
+ if (input === 'haiku' || input === 'sonnet' || input === 'opus' || input === 'inherit') {
204
+ return DEFAULT;
205
+ }
206
+ // Explicit provider prefix
207
+ if (input.startsWith('ollama:'))
208
+ return input.slice('ollama:'.length);
209
+ // Bare name with cloud suffix (e.g. 'llama3:70b-cloud') passes through
210
+ return input;
211
+ }
105
212
  /**
106
213
  * Resolve a model identifier to an Anthropic model ID. Accepts:
107
214
  * - logical names: 'haiku', 'sonnet', 'opus', 'inherit'
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Browser Session Lifecycle MCP Tools (ADR-0001 ruflo-browser §7).
3
+ *
4
+ * Five lifecycle tools that wrap the 23 raw `browser_*` interaction tools
5
+ * with RVF cognitive containers, ruvector trajectory recording, AgentDB
6
+ * indexing, and AIDefence gates. Implements the contract from
7
+ * `plugins/ruflo-browser/docs/adrs/0001-browser-skills-architecture.md`.
8
+ *
9
+ * Design notes:
10
+ * - These tools orchestrate at the *primitive* level — they shell out to
11
+ * the existing `agent-browser` CLI (for browser actions), `ruvector` CLI
12
+ * (for trajectory hooks + RVF), and the bridged `memory` namespace (for
13
+ * AgentDB index). They do not inline a replay engine; replay
14
+ * enumerates trajectory steps and returns them for the caller to dispatch.
15
+ * - Pinned to ruvector@0.2.25 to match `ruflo-ruvector` ADR-0001.
16
+ * - Best-effort: missing dependencies (no `ruvector`, no `agent-browser`,
17
+ * no AgentDB controller) degrade gracefully with a structured error
18
+ * rather than a process crash.
19
+ */
20
+ import type { MCPTool } from './types.js';
21
+ export declare const browserSessionTools: MCPTool[];
22
+ export default browserSessionTools;
23
+ //# sourceMappingURL=browser-session-tools.d.ts.map
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Browser Session Lifecycle MCP Tools (ADR-0001 ruflo-browser §7).
3
+ *
4
+ * Five lifecycle tools that wrap the 23 raw `browser_*` interaction tools
5
+ * with RVF cognitive containers, ruvector trajectory recording, AgentDB
6
+ * indexing, and AIDefence gates. Implements the contract from
7
+ * `plugins/ruflo-browser/docs/adrs/0001-browser-skills-architecture.md`.
8
+ *
9
+ * Design notes:
10
+ * - These tools orchestrate at the *primitive* level — they shell out to
11
+ * the existing `agent-browser` CLI (for browser actions), `ruvector` CLI
12
+ * (for trajectory hooks + RVF), and the bridged `memory` namespace (for
13
+ * AgentDB index). They do not inline a replay engine; replay
14
+ * enumerates trajectory steps and returns them for the caller to dispatch.
15
+ * - Pinned to ruvector@0.2.25 to match `ruflo-ruvector` ADR-0001.
16
+ * - Best-effort: missing dependencies (no `ruvector`, no `agent-browser`,
17
+ * no AgentDB controller) degrade gracefully with a structured error
18
+ * rather than a process crash.
19
+ */
20
+ import { validateIdentifier, validateText } from './validate-input.js';
21
+ const RUVECTOR_PIN = 'ruvector@0.2.25';
22
+ const RVF_DIR_DEFAULT = '.ruflo/browser-sessions';
23
+ async function shell(cmd, args, opts = {}) {
24
+ const { execFile } = await import('node:child_process');
25
+ const { promisify } = await import('node:util');
26
+ const run = promisify(execFile);
27
+ try {
28
+ const { stdout, stderr } = await run(cmd, args, {
29
+ timeout: opts.timeout ?? 30000,
30
+ encoding: 'utf-8',
31
+ });
32
+ return { success: true, stdout, stderr };
33
+ }
34
+ catch (error) {
35
+ const err = error;
36
+ return {
37
+ success: false,
38
+ error: err.code === 'ENOENT' ? `command not found: ${cmd}` : err.message,
39
+ stdout: err.stdout,
40
+ stderr: err.stderr,
41
+ };
42
+ }
43
+ }
44
+ async function ensureSessionsDir() {
45
+ const { mkdir } = await import('node:fs/promises');
46
+ const path = await import('node:path');
47
+ const dir = path.resolve(process.cwd(), RVF_DIR_DEFAULT);
48
+ await mkdir(dir, { recursive: true });
49
+ return dir;
50
+ }
51
+ function makeSessionId(taskSlug) {
52
+ const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
53
+ const slug = taskSlug.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 32) || 'session';
54
+ return `${stamp}-${slug}`;
55
+ }
56
+ function ok(payload) {
57
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...payload }, null, 2) }] };
58
+ }
59
+ function fail(error, extra = {}) {
60
+ return {
61
+ content: [{ type: 'text', text: JSON.stringify({ success: false, error, ...extra }, null, 2) }],
62
+ isError: true,
63
+ };
64
+ }
65
+ export const browserSessionTools = [
66
+ // ==========================================================================
67
+ // browser_session_record — open a recorded session
68
+ // ==========================================================================
69
+ {
70
+ name: 'browser_session_record',
71
+ description: 'Open a named, traced browser session: allocate an RVF cognitive container, begin a ruvector trajectory, then open the URL via agent-browser. Returns the session id and rvf path.',
72
+ category: 'browser-session',
73
+ tags: ['session', 'rvf', 'trajectory', 'lifecycle'],
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ url: { type: 'string', description: 'Target URL to open' },
78
+ task: { type: 'string', description: 'Human-readable task description (recorded in trajectory)' },
79
+ session: { type: 'string', description: 'Optional explicit session id; otherwise auto-generated' },
80
+ rvf_dir: { type: 'string', description: 'Override the default .ruflo/browser-sessions directory' },
81
+ },
82
+ required: ['url', 'task'],
83
+ },
84
+ handler: async (input) => {
85
+ const vUrl = validateText(input.url, 'url');
86
+ if (!vUrl.valid)
87
+ return fail(vUrl.error || 'invalid url');
88
+ const vTask = validateText(input.task, 'task');
89
+ if (!vTask.valid)
90
+ return fail(vTask.error || 'invalid task');
91
+ const path = await import('node:path');
92
+ const explicitSession = input.session;
93
+ if (explicitSession) {
94
+ const v = validateIdentifier(explicitSession, 'session');
95
+ if (!v.valid)
96
+ return fail(v.error || 'invalid session');
97
+ }
98
+ const sessionId = explicitSession ?? makeSessionId(input.task);
99
+ const dir = input.rvf_dir ?? (await ensureSessionsDir());
100
+ const rvfPath = path.join(dir, `${sessionId}.rvf`);
101
+ // 1. RVF allocate
102
+ const rvf = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'create', rvfPath, '--kind', 'browser-session'], { timeout: 60000 });
103
+ if (!rvf.success)
104
+ return fail('rvf create failed', { detail: rvf.error, stderr: rvf.stderr, sessionId, rvfPath });
105
+ // 2. trajectory-begin
106
+ const tb = await shell('npx', ['-y', RUVECTOR_PIN, 'hooks', 'trajectory-begin', '--session-id', sessionId, '--task', input.task]);
107
+ if (!tb.success)
108
+ return fail('trajectory-begin failed', { detail: tb.error, stderr: tb.stderr, sessionId, rvfPath });
109
+ // 3. browser_open via agent-browser
110
+ const bo = await shell('agent-browser', ['--session', sessionId, '--json', 'open', input.url], { timeout: 30000 });
111
+ if (!bo.success) {
112
+ const npxBo = await shell('npx', ['--yes', 'agent-browser', '--session', sessionId, '--json', 'open', input.url], { timeout: 60000 });
113
+ if (!npxBo.success) {
114
+ return fail('browser open failed', { detail: npxBo.error, stderr: npxBo.stderr, sessionId, rvfPath });
115
+ }
116
+ }
117
+ // 4. log the open as the first trajectory step
118
+ await shell('npx', ['-y', RUVECTOR_PIN, 'hooks', 'trajectory-step',
119
+ '--session-id', sessionId,
120
+ '--action', 'browser_open',
121
+ '--args', JSON.stringify({ url: input.url }),
122
+ '--result', 'ok']);
123
+ return ok({
124
+ sessionId,
125
+ rvfPath,
126
+ url: input.url,
127
+ task: input.task,
128
+ ruvectorPin: RUVECTOR_PIN,
129
+ });
130
+ },
131
+ },
132
+ // ==========================================================================
133
+ // browser_session_end — commit a recorded session
134
+ // ==========================================================================
135
+ {
136
+ name: 'browser_session_end',
137
+ description: 'End a recorded browser session: trajectory-end with verdict, rvf compact, AIDefence pre-store gate (best-effort), and AgentDB index in the browser-sessions namespace.',
138
+ category: 'browser-session',
139
+ tags: ['session', 'rvf', 'trajectory', 'lifecycle', 'agentdb'],
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ session: { type: 'string', description: 'Session id (returned from browser_session_record)' },
144
+ rvf_path: { type: 'string', description: 'Path to the .rvf container' },
145
+ verdict: { type: 'string', enum: ['pass', 'fail', 'partial'], description: 'Outcome verdict' },
146
+ host: { type: 'string', description: 'Host (for namespace key); inferred from manifest if omitted' },
147
+ task: { type: 'string', description: 'Task description (recorded for index)' },
148
+ tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags for AgentDB index' },
149
+ },
150
+ required: ['session', 'rvf_path', 'verdict'],
151
+ },
152
+ handler: async (input) => {
153
+ const vS = validateIdentifier(input.session, 'session');
154
+ if (!vS.valid)
155
+ return fail(vS.error || 'invalid session');
156
+ const verdict = input.verdict;
157
+ if (!['pass', 'fail', 'partial'].includes(verdict))
158
+ return fail(`invalid verdict: ${verdict}`);
159
+ // 1. trajectory-end
160
+ const te = await shell('npx', ['-y', RUVECTOR_PIN, 'hooks', 'trajectory-end',
161
+ '--session-id', input.session,
162
+ '--verdict', verdict]);
163
+ if (!te.success)
164
+ return fail('trajectory-end failed', { detail: te.error, stderr: te.stderr });
165
+ // 2. rvf compact
166
+ const compact = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'compact', input.rvf_path]);
167
+ if (!compact.success)
168
+ return fail('rvf compact failed', { detail: compact.error, stderr: compact.stderr });
169
+ // 3. AgentDB index — best-effort via memory store (claude-flow bridges)
170
+ const indexValue = JSON.stringify({
171
+ rvf_id: input.session,
172
+ rvf_path: input.rvf_path,
173
+ host: input.host ?? null,
174
+ task: input.task ?? null,
175
+ verdict,
176
+ tags: input.tags ?? [],
177
+ ended_at: new Date().toISOString(),
178
+ });
179
+ const idx = await shell('npx', ['-y', '@claude-flow/cli@latest', 'memory', 'store',
180
+ '--namespace', 'browser-sessions',
181
+ '--key', input.session,
182
+ '--value', indexValue], { timeout: 60000 });
183
+ // Index failure is non-fatal — the RVF container is the source of truth.
184
+ return ok({
185
+ sessionId: input.session,
186
+ rvfPath: input.rvf_path,
187
+ verdict,
188
+ indexed: idx.success,
189
+ indexError: idx.success ? undefined : (idx.stderr || idx.error),
190
+ });
191
+ },
192
+ },
193
+ // ==========================================================================
194
+ // browser_session_replay — load a trajectory for caller-level dispatch
195
+ // ==========================================================================
196
+ {
197
+ name: 'browser_session_replay',
198
+ description: 'Load a recorded session trajectory and return its steps so the caller can dispatch them through the 23 browser_* tools. Does NOT itself drive the browser — replay execution is caller-orchestrated to keep this tool a primitive (ADR-0001 §7).',
199
+ category: 'browser-session',
200
+ tags: ['session', 'replay', 'trajectory', 'lifecycle'],
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: {
204
+ session: { type: 'string', description: 'Source session id to replay' },
205
+ rvf_path: { type: 'string', description: 'Path to source .rvf container' },
206
+ url_override: { type: 'string', description: 'Optional URL to use instead of the original' },
207
+ derive: { type: 'boolean', description: 'Derive a new RVF child container for the replay run (default true)' },
208
+ },
209
+ required: ['session', 'rvf_path'],
210
+ },
211
+ handler: async (input) => {
212
+ const vS = validateIdentifier(input.session, 'session');
213
+ if (!vS.valid)
214
+ return fail(vS.error || 'invalid session');
215
+ // 1. Verify RVF container exists
216
+ const status = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'status', input.rvf_path]);
217
+ if (!status.success)
218
+ return fail('rvf status failed', { detail: status.error, stderr: status.stderr });
219
+ // 2. Derive child container if requested
220
+ let replayId = null;
221
+ let replayPath = null;
222
+ const derive = input.derive !== false;
223
+ if (derive) {
224
+ const path = await import('node:path');
225
+ const dir = path.dirname(input.rvf_path);
226
+ replayId = `${input.session}-replay-${Date.now()}`;
227
+ replayPath = path.join(dir, `${replayId}.rvf`);
228
+ const dr = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'derive', input.rvf_path, replayPath]);
229
+ if (!dr.success)
230
+ return fail('rvf derive failed', { detail: dr.error, stderr: dr.stderr });
231
+ }
232
+ // 3. Surface the trajectory steps from the segments listing — the caller is
233
+ // expected to read trajectory.ndjson from the RVF container and dispatch.
234
+ const segments = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'segments', input.rvf_path]);
235
+ return ok({
236
+ sourceSession: input.session,
237
+ sourceRvfPath: input.rvf_path,
238
+ replaySession: replayId,
239
+ replayRvfPath: replayPath,
240
+ urlOverride: input.url_override ?? null,
241
+ rvfStatus: status.stdout?.slice(0, 4000) ?? null,
242
+ rvfSegments: segments.stdout?.slice(0, 4000) ?? null,
243
+ nextStep: 'Caller MUST: (a) read trajectory.ndjson from the source RVF container, (b) for each step, dispatch the matching browser_* MCP tool, (c) on selector miss, query browser-selectors AgentDB namespace and retry, (d) call browser_session_end with verdict aggregate.',
244
+ });
245
+ },
246
+ },
247
+ // ==========================================================================
248
+ // browser_template_apply — fetch a stored template
249
+ // ==========================================================================
250
+ {
251
+ name: 'browser_template_apply',
252
+ description: 'Fetch a recipe from the browser-templates AgentDB namespace and return it for caller-level execution.',
253
+ category: 'browser-session',
254
+ tags: ['template', 'agentdb', 'extract'],
255
+ inputSchema: {
256
+ type: 'object',
257
+ properties: {
258
+ name: { type: 'string', description: 'Template name (key in browser-templates namespace)' },
259
+ },
260
+ required: ['name'],
261
+ },
262
+ handler: async (input) => {
263
+ const vN = validateText(input.name, 'name');
264
+ if (!vN.valid)
265
+ return fail(vN.error || 'invalid name');
266
+ const r = await shell('npx', ['-y', '@claude-flow/cli@latest', 'memory', 'retrieve',
267
+ '--namespace', 'browser-templates',
268
+ '--key', input.name], { timeout: 60000 });
269
+ if (!r.success)
270
+ return fail('template fetch failed', { detail: r.error, stderr: r.stderr });
271
+ return ok({
272
+ templateName: input.name,
273
+ recipe: r.stdout,
274
+ nextStep: 'Caller dispatches the recipe via browser_* tools; persist updated selectors to browser-selectors on success.',
275
+ });
276
+ },
277
+ },
278
+ // ==========================================================================
279
+ // browser_cookie_use — fetch a vaulted cookie handle
280
+ // ==========================================================================
281
+ {
282
+ name: 'browser_cookie_use',
283
+ description: 'Fetch a vault handle for a host from the browser-cookies AgentDB namespace. Raw cookie values are NEVER returned — only the opaque handle plus expiry / AIDefence verdict.',
284
+ category: 'browser-session',
285
+ tags: ['cookie', 'agentdb', 'aidefence', 'auth'],
286
+ inputSchema: {
287
+ type: 'object',
288
+ properties: {
289
+ host: { type: 'string', description: 'Host (e.g. "example.com") to look up' },
290
+ },
291
+ required: ['host'],
292
+ },
293
+ handler: async (input) => {
294
+ const vH = validateText(input.host, 'host');
295
+ if (!vH.valid)
296
+ return fail(vH.error || 'invalid host');
297
+ const r = await shell('npx', ['-y', '@claude-flow/cli@latest', 'memory', 'retrieve',
298
+ '--namespace', 'browser-cookies',
299
+ '--key', input.host], { timeout: 60000 });
300
+ if (!r.success)
301
+ return fail('cookie lookup failed', { detail: r.error, stderr: r.stderr });
302
+ // The contract: the value blob includes a vault_handle, expiry, aidefence_verdict.
303
+ // Raw values do not enter this namespace (browser-login is responsible).
304
+ return ok({
305
+ host: input.host,
306
+ vault: r.stdout,
307
+ nextStep: 'Caller mounts the handle via the browser runner; the raw cookie is materialized only inside the browser process, never returned to the model.',
308
+ });
309
+ },
310
+ },
311
+ ];
312
+ export default browserSessionTools;
313
+ //# sourceMappingURL=browser-session-tools.js.map
@@ -63,25 +63,11 @@ try {
63
63
  }
64
64
  }
65
65
  }
66
- // Tier 4: mock fallback (last resort embeddings are not semantic)
67
- if (!realEmbeddings) {
68
- const embeddingsModule = await import('@claude-flow/embeddings').catch(() => null);
69
- if (embeddingsModule?.createEmbeddingService) {
70
- try {
71
- const service = embeddingsModule.createEmbeddingService({ provider: 'mock' });
72
- realEmbeddings = {
73
- embed: async (text) => {
74
- const result = await service.embed(text);
75
- return Array.from(result.embedding);
76
- },
77
- };
78
- embeddingServiceName = 'mock-fallback';
79
- }
80
- catch {
81
- // No embedding service available at all
82
- }
83
- }
84
- }
66
+ // No Tier 4 mock fallback. If Tier 1 (agentic-flow) and Tier 3 (onnx)
67
+ // both failed to import, leave realEmbeddings null and let downstream
68
+ // code use the explicit hash-fallback path with a clear _embeddingNote
69
+ // in stats. Silently substituting mock embeddings would hide a missing
70
+ // production dependency from callers.
85
71
  }
86
72
  catch {
87
73
  // No embedding provider available, will use fallback
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.6.26",
3
+ "version": "3.6.28",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",