@claude-flow/cli 3.72.1 → 3.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/README.md +29 -21
  3. package/catalog-manifest.json +2 -2
  4. package/dist/src/commands/agent.js +2 -0
  5. package/dist/src/commands/funnel.d.ts +1 -0
  6. package/dist/src/commands/funnel.js +37 -2
  7. package/dist/src/commands/hooks.js +16 -2
  8. package/dist/src/commands/init.js +70 -16
  9. package/dist/src/commands/mcp.js +15 -0
  10. package/dist/src/commands/neural.js +5 -3
  11. package/dist/src/commands/swarm.js +2 -0
  12. package/dist/src/funnel/activation.d.ts +30 -0
  13. package/dist/src/funnel/activation.js +69 -0
  14. package/dist/src/funnel/events.js +4 -1
  15. package/dist/src/funnel/index.d.ts +2 -1
  16. package/dist/src/funnel/index.js +2 -1
  17. package/dist/src/funnel/message-transport.d.ts +12 -0
  18. package/dist/src/funnel/message-transport.js +23 -1
  19. package/dist/src/funnel/types.d.ts +2 -2
  20. package/dist/src/init/first-win.d.ts +24 -0
  21. package/dist/src/init/first-win.js +97 -0
  22. package/dist/src/mcp-tools/agent-tools.js +2 -0
  23. package/dist/src/mcp-tools/browser-session-tools.d.ts +2 -2
  24. package/dist/src/mcp-tools/browser-session-tools.js +4 -4
  25. package/dist/src/mcp-tools/hooks-tools.js +17 -1
  26. package/dist/src/mcp-tools/neural-tools.js +3 -3
  27. package/dist/src/mcp-tools/swarm-tools.js +2 -0
  28. package/dist/src/missions/schemas.d.ts +117 -117
  29. package/dist/src/mods/command-registry/schema.d.ts +121 -121
  30. package/dist/src/transfer/store/download.d.ts +1 -1
  31. package/dist/src/transfer/store/download.js +22 -21
  32. package/dist/src/transfer/store/publish.d.ts +1 -1
  33. package/dist/src/transfer/store/publish.js +13 -14
  34. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  35. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  36. package/node_modules/@claude-flow/plugin-agent-federation/package.json +5 -5
  37. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  38. package/package.json +7 -7
  39. package/plugins/ruflo-console/dist/tui.mjs +153 -153
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.72.1",
3
+ "version": "3.73.0",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "6078bd1c6b02dd3f80097f8d7f28904d48b3cdf7a534d2aa13966590694ca403",
6
6
  "hook-handler.cjs": "31db0f7492fbd8b2e48ea9adbc1ba293dfed1e006666d20e93ea76e158986947",
@@ -10,6 +10,6 @@
10
10
  "learning-loop.cjs": "d046cd6f8efb7dd40e1d2451516c181fc8b26477aefb1124c0df5f52a23f9190"
11
11
  }
12
12
  },
13
- "signature": "xOPNAkZaRFxW1eLWGKMWxYTKSag+Q6LH2zEvMSKUkICLeNN5KDOOBmd9G6nqQqqj2W9Yj+iNPgo6fMvxM7KgAw==",
13
+ "signature": "JPs8pGUFfkmmIj+GpqM7snJ1AX9IJS8olXiwHgZjSTKCIcK0aERAd+Wjy3jfBbK1V8mqP+ZWeX8CRr4aA+3BDA==",
14
14
  "algorithm": "ed25519"
15
15
  }
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  <!-- Try Ruflo — the 3 badges first-time visitors actually act on -->
8
8
  [![npm version (ruflo)](https://img.shields.io/npm/v/ruflo?label=npx%20ruflo&style=for-the-badge&logo=npm&color=cb3837)](https://www.npmjs.com/package/ruflo)
9
9
  [![MIT License](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](https://opensource.org/licenses/MIT)
10
- [![Star on GitHub](https://img.shields.io/github/stars/ruvnet/claude-flow?style=for-the-badge&logo=github&color=gold)](https://github.com/ruvnet/claude-flow)
10
+ [![Star on GitHub](https://img.shields.io/github/stars/ruvnet/ruflo?style=for-the-badge&logo=github&color=gold)](https://github.com/ruvnet/ruflo)
11
11
  [![Follow @rUv on X](https://img.shields.io/badge/Follow-@rUv-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/rUv)
12
12
 
13
13
  <p align="center">
@@ -23,42 +23,49 @@
23
23
 
24
24
  <a id="start-here"></a>
25
25
 
26
- ## <img src="docs/assets/readme/icons/terminal.svg" width="32" height="32" alt=""> Get started with Ruflo
26
+ ## <img src="docs/assets/readme/icons/terminal.svg" width="32" height="32" alt=""> Get started in one command
27
27
 
28
- <img src="docs/assets/readme/icons/plugins.svg" width="28" height="28" alt=""> **Start in Claude Code** with core tools, the visual console and runtime mods. Requires Claude Code **2.1.287 or later**. Run inside Claude Code:
28
+ Run this in your project folder:
29
+
30
+ ```bash
31
+ npx ruflo@latest init
32
+ ```
33
+
34
+ Then open Claude Code in the same folder and give it a real task, for example:
29
35
 
30
36
  ```text
31
- /plugin marketplace add ruvnet/ruflo
32
- /plugin install ruflo-core@ruflo
33
- /plugin install ruflo-console@ruflo
34
- /plugin install ruflo-mods@ruflo
35
- /reload-plugins
36
- /ruflo
37
+ Use a ruflo swarm to review this repo: find the riskiest module, write tests for it, and summarise what you changed.
37
38
  ```
38
39
 
39
- From a terminal on Claude Code 2.1.292 or later, one command adds the marketplace and installs a plugin: `claude plugin install ruflo-core@ruflo --marketplace ruvnet/ruflo`.
40
+ `init` adds ruflo's agents, hooks, memory and MCP server to the project, and Claude Code picks them up on its next start. Codex and other MCP clients work too: point them at `npx -y ruflo@latest mcp start`.
40
41
 
41
- **Core** provides foundation tools. **Console** opens the agent cockpit. **Mods** add routing and policy enforcement inside Claude Code. Mods run with your account's permissions; review their code before installing.
42
+ <p align="center"><img src="docs/assets/ruflo-console-tour.gif" width="720" alt="Looping recording of the ruflo console: overview, swarm view and missions in a sample project"></p>
42
43
 
43
- <img src="docs/assets/readme/icons/terminal.svg" width="28" height="28" alt=""> **Using Codex or another MCP enabled tool?** Use NPX for Ruflo project setup and connect your client to the Ruflo MCP server. Run the setup wizard in your project terminal:
44
+ [First useful task, step by step](docs/ruflo-explained.md) · [User guide](docs/USERGUIDE.md) · [Compare install paths](#quick-start)
44
45
 
45
- ```bash
46
- npx ruflo@latest init wizard
47
- ```
46
+ <details>
47
+ <summary><b>Other ways to install</b> (Claude Code plugins only, MCP-only clients, terminal UI)</summary>
48
48
 
49
- For clients that support local stdio MCP servers, configure **command** `npx` and **arguments** `["-y", "ruflo@latest", "mcp", "start"]`. The equivalent server command is:
49
+ **Claude Code plugins only.** This route installs core tools, the visual console and runtime mods, with no files added to your project. It needs Claude Code **2.1.287 or later**. Run inside Claude Code:
50
50
 
51
- ```bash
52
- npx -y ruflo@latest mcp start
51
+ ```text
52
+ /plugin marketplace add ruvnet/ruflo
53
+ /plugin install ruflo-core@ruflo
54
+ /plugin install ruflo-console@ruflo
55
+ /plugin install ruflo-mods@ruflo
56
+ /reload-plugins
57
+ /ruflo
53
58
  ```
54
59
 
55
- MCP exposes Ruflo tools to your client. The console and mods above are Claude Code integrations; hook support depends on the client.
60
+ From a terminal, on Claude Code 2.1.292 or later: `claude plugin install ruflo-core@ruflo --marketplace ruvnet/ruflo`. Mods run with your account's permissions, so review their code before installing.
61
+
62
+ **Codex or another MCP client, without project setup.** Configure the **command** `npx` with the **arguments** `["-y", "ruflo@latest", "mcp", "start"]`. To set up a project interactively, run `npx ruflo@latest init wizard`.
56
63
 
57
- <img src="docs/assets/readme/icons/terminal.svg" width="28" height="28" alt=""> **Prefer a plain terminal? Optional.** The console also runs on its own, with no Claude Code: `npx ruflo tui` (add `--plain` to print one frame for scripts). It shows Overview, Cost, Swarm, Events and Missions, plus an optional AI page. [Terminal UI guide](docs/USERGUIDE.md#terminal-ui-optional)
64
+ **Plain terminal.** The console also runs on its own, without Claude Code: `npx ruflo tui`. Add `--plain` to print one frame for scripts. See the [terminal UI guide](docs/USERGUIDE.md#terminal-ui-optional).
58
65
 
59
66
  <p align="center"><img src="docs/assets/ruflo-tui-compact.svg" width="640" alt="Looping recording of the optional ruflo terminal UI with its pictures: Overview in the Classic BBS look, the Orbital look with its moving globe, and the Swarm graph, in a sample project"></p>
60
67
 
61
- [Compare install paths](#quick-start) · [Open the console](#console-walkthrough) · [User guide](docs/USERGUIDE.md)
68
+ </details>
62
69
 
63
70
  <div align="center">
64
71
 
@@ -484,6 +491,7 @@ Four docs for four audiences:
484
491
  |-----|-----------------|
485
492
  | **[Status](docs/STATUS.md)** | See what currently works — capability counts, test baselines, recent fixes, what's next. The *is-it-ready* doc. |
486
493
  | **[User Guide](docs/USERGUIDE.md)** | Daily reference — every command, every config flag, every plugin. The *how-do-I* doc. |
494
+ | **[Compare frameworks](docs/compare.md)** | Deciding between Ruflo, Claude Code built-in agents, CrewAI, LangGraph, or AutoGen. The *which-one-for-me* doc. |
487
495
  | **[MetaHarness Guide](docs/metaharness-user-guide.md)** | How to grade your agent setup, scan tool configs for security, detect changes between runs, and eject a project into a standalone agent toolkit. The *audit-my-setup* doc. |
488
496
  | **[Benchmarks](https://gist.github.com/ruvnet/298f8c668c8859b369f91734a0e9cbbe)** | v3.8.0 SOTA matrix vs LangGraph / AutoGen / CrewAI on darwin-arm64 + linux-x64. ruflo wins cold start, single turn, RSS by 1.3×–1953×. The *is-it-fast* doc. |
489
497
  | **[Verification](verification.md)** | Cryptographically prove your installed bytes match the signed witness — `ruflo verify`. The *trust-but-verify* doc. |
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-11-06T02:50:18.000Z",
5
- "gitSha": "29b327ee",
4
+ "generatedAt": "2026-11-09T01:20:38.869Z",
5
+ "gitSha": "a8b3304b",
6
6
  "catalog": {
7
7
  "agents": 177,
8
8
  "tools": 451,
@@ -9,6 +9,7 @@ import { wasmSubcommands } from './agent-wasm.js';
9
9
  import { agentPublishCommand } from './agntcy/publish.js';
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
+ import { noteFirstAgentRun } from '../funnel/activation.js';
12
13
  /**
13
14
  * Update swarm-activity.json metrics after agent count changes.
14
15
  * The statusline reads this file to display the swarm agent count.
@@ -172,6 +173,7 @@ const spawnCommand = {
172
173
  if (ctx.flags.format === 'json') {
173
174
  output.printJson(result);
174
175
  }
176
+ noteFirstAgentRun('cli'); // activation funnel (opt-in; no-op without telemetry consent)
175
177
  return { success: true, data: result };
176
178
  }
177
179
  catch (error) {
@@ -11,6 +11,7 @@
11
11
  * funnel earnings show accrued and paid revenue-share balance
12
12
  * funnel unenroll revoke local enrollment (funnel itself stays enabled)
13
13
  * funnel id print the pseudonymous funnel ID, if one exists
14
+ * funnel telemetry opt in or out of anonymous usage telemetry (on | off | status)
14
15
  */
15
16
  import type { Command } from '../types.js';
16
17
  export declare const funnelCommand: Command;
@@ -11,6 +11,7 @@
11
11
  * funnel earnings show accrued and paid revenue-share balance
12
12
  * funnel unenroll revoke local enrollment (funnel itself stays enabled)
13
13
  * funnel id print the pseudonymous funnel ID, if one exists
14
+ * funnel telemetry opt in or out of anonymous usage telemetry (on | off | status)
14
15
  */
15
16
  import { execFile } from 'node:child_process';
16
17
  import * as fs from 'node:fs';
@@ -63,11 +64,45 @@ const disableSub = {
63
64
  action: async () => {
64
65
  setUserConfigEnabled(false);
65
66
  recordDisclosureDeclined();
67
+ // "All surfaces" includes opt-in event telemetry: revoke it too.
68
+ if (hasConsent('telemetry'))
69
+ recordConsent('telemetry', false, 'cli-funnel-disable');
66
70
  deleteFunnelData(); // opt-out deletes the pseudonymous ID + local event queue
67
- output.printSuccess('Funnel disabled. All promotional surfaces are off; local funnel data deleted.');
71
+ output.printSuccess('Funnel disabled. All promotional surfaces are off, no funnel.ruv.io requests are made, and local funnel data is deleted.');
68
72
  return { success: true };
69
73
  },
70
74
  };
75
+ // ADR-302 consent domain `telemetry` (ADR-305/309): explicit opt-in to the
76
+ // anonymous funnel event stream. Enrollment never toggles it; only this
77
+ // command does. Off by default.
78
+ const telemetrySub = {
79
+ name: 'telemetry',
80
+ description: 'Opt in or out of anonymous usage telemetry: telemetry on | off | status',
81
+ action: async (ctx) => {
82
+ const verb = String(ctx.args[0] ?? 'status').toLowerCase();
83
+ if (verb === 'on') {
84
+ recordConsent('telemetry', true, 'cli-funnel-telemetry');
85
+ const id = getFunnelId();
86
+ output.printSuccess('Telemetry on. Once per session ruflo sends anonymous funnel events (daily-bucketed, no paths, prompts, code or account data) with a random ID that rotates every 90 days.');
87
+ output.writeln('Turn it off any time with `ruflo funnel telemetry off`; that also deletes the ID and the local event queue.');
88
+ return { success: true, data: { telemetry: true, id } };
89
+ }
90
+ if (verb === 'off') {
91
+ recordConsent('telemetry', false, 'cli-funnel-telemetry');
92
+ deleteFunnelData();
93
+ output.printSuccess('Telemetry off. The pseudonymous ID and the local event queue are deleted.');
94
+ return { success: true, data: { telemetry: false } };
95
+ }
96
+ if (verb !== 'status') {
97
+ output.printError(`Unknown telemetry option "${verb}". Use: on | off | status`);
98
+ return { success: false, exitCode: 1 };
99
+ }
100
+ const on = hasConsent('telemetry');
101
+ output.writeln(`Telemetry: ${on ? 'on (opted in)' : 'off'}`);
102
+ output.writeln('Without opting in, ruflo makes one anonymous request per session to fetch tips (release + OS only); `ruflo funnel disable` stops that too.');
103
+ return { success: true, data: { telemetry: on } };
104
+ },
105
+ };
71
106
  const enableSub = {
72
107
  name: 'enable',
73
108
  description: 'Re-enable funnel surfaces at the user tier (env/enterprise disables still win)',
@@ -287,7 +322,7 @@ const idSub = {
287
322
  export const funnelCommand = {
288
323
  name: 'funnel',
289
324
  description: 'Control the Cognitum lifecycle funnel surfaces (tips, enrollment, notices)',
290
- subcommands: [statusSub, disableSub, enableSub, acceptSub, openSub, enrollSub, earningsSub, unenrollSub, idSub],
325
+ subcommands: [statusSub, disableSub, enableSub, acceptSub, openSub, enrollSub, earningsSub, unenrollSub, idSub, telemetrySub],
291
326
  examples: [
292
327
  { command: 'ruflo funnel status', description: 'Effective state + deciding source' },
293
328
  { command: 'ruflo funnel accept', description: 'Skip the 24h grace so rotation starts now' },
@@ -4612,8 +4612,22 @@ const refreshFunnelCommand = {
4612
4612
  ],
4613
4613
  action: async (ctx) => {
4614
4614
  try {
4615
- const { refreshRemoteMessages } = await import('../funnel/index.js');
4616
- const result = await refreshRemoteMessages();
4615
+ const { refreshRemoteMessages, resolveFunnelEnabled, hasConsent, flushEvents } = await import('../funnel/index.js');
4616
+ // `ruflo funnel disable` (and every other opt-out source) must stop the
4617
+ // network call, not only the promo row (ADR-309 operational telemetry).
4618
+ const decision = resolveFunnelEnabled();
4619
+ const result = decision.enabled
4620
+ ? await refreshRemoteMessages()
4621
+ : { refreshed: false, skipped: 'funnel-disabled', decidedBy: decision.decidedBy };
4622
+ // Opt-in event telemetry (ADR-305/309): only with an explicit, current
4623
+ // telemetry consent receipt; flushEvents re-checks consent itself.
4624
+ if (decision.enabled && hasConsent('telemetry')) {
4625
+ const { rufloPackageVersion } = await import('../mcp-protocol.js');
4626
+ // Activation funnel: one "active today" marker per day (7-day retention).
4627
+ const { recordSessionActive } = await import('../funnel/activation.js');
4628
+ recordSessionActive(rufloPackageVersion());
4629
+ result.events = await flushEvents({ release: rufloPackageVersion() });
4630
+ }
4617
4631
  if (!ctx.flags.quiet) {
4618
4632
  output.writeln(JSON.stringify(result));
4619
4633
  }
@@ -79,6 +79,45 @@ async function offerCapabilityEnrollment(ctx) {
79
79
  // Enrollment is optional; init success is never affected.
80
80
  }
81
81
  }
82
+ /**
83
+ * One-time opt-in to anonymous usage telemetry (ADR-302 telemetry domain,
84
+ * ADR-309). Separate from enrollment, which must never toggle telemetry.
85
+ * Interactive TTY only, never in CI, skipped with --no-telemetry-prompt,
86
+ * asked once whatever the answer, default No. Never affects init success.
87
+ */
88
+ async function offerTelemetryOptIn(ctx) {
89
+ try {
90
+ if (ctx.flags.format === 'json')
91
+ return;
92
+ const noPrompt = Boolean(ctx.flags['no-telemetry-prompt'] || ctx.flags.noTelemetryPrompt || ctx.flags.telemetryPrompt === false);
93
+ const { shouldOfferTelemetry } = await import('../funnel/activation.js');
94
+ if (!shouldOfferTelemetry({ noTelemetryPrompt: noPrompt, cwd: ctx.cwd }))
95
+ return;
96
+ output.writeln();
97
+ output.writeln('Help improve ruflo? Opting in sends anonymous usage events: init finished, first agent run, active days.');
98
+ output.writeln(output.dim(' Daily-bucketed, with a random ID that rotates every 90 days. No code, paths, prompts or account data.'));
99
+ const accepted = await confirm({ message: 'Share anonymous usage telemetry?', default: false });
100
+ const { recordConsent } = await import('../funnel/index.js');
101
+ recordConsent('telemetry', Boolean(accepted), 'init-prompt');
102
+ output.writeln(output.dim(accepted
103
+ ? ' Thanks. Turn it off any time with \`ruflo funnel telemetry off\`.'
104
+ : ' Telemetry stays off. Turn it on later with \`ruflo funnel telemetry on\`.'));
105
+ }
106
+ catch {
107
+ // Optional; init success is never affected.
108
+ }
109
+ }
110
+ /** Activation funnel: no-op without telemetry consent; never throws. */
111
+ async function noteInitCompleted() {
112
+ try {
113
+ const [{ recordInitCompleted }, { rufloPackageVersion }] = await Promise.all([
114
+ import('../funnel/activation.js'),
115
+ import('../mcp-protocol.js'),
116
+ ]);
117
+ recordInitCompleted(rufloPackageVersion());
118
+ }
119
+ catch { /* optional */ }
120
+ }
82
121
  async function resolveCodexInitializer(cwd) {
83
122
  // Use a variable to prevent TypeScript from statically resolving the optional module
84
123
  const codexModuleId = '@claude-flow/codex';
@@ -937,26 +976,33 @@ const initClaudeAction = async (ctx) => {
937
976
  output.writeln(output.warning(` ruflo mods not enabled: ${err instanceof Error ? err.message : String(err)}`));
938
977
  }
939
978
  }
940
- if (!startDaemon && !startAll) {
941
- const bin = (process.argv[1] || '').includes('ruflo') ? 'ruflo' : 'claude-flow';
942
- output.writeln(output.bold('Next steps:'));
943
- output.printList([
944
- `Run ${output.highlight(`${bin} daemon start`)} to start background workers`,
945
- // Memory is initialized automatically during init (persistent by
946
- // default — see executor.ts) — no separate `memory init` step needed
947
- // unless the DB was skipped (MINIMAL_INIT_OPTIONS) or needs --force.
948
- `Run ${output.highlight(`${bin} swarm init`)} to initialize a swarm`,
949
- `Or use ${output.highlight(`${bin} init --start-all`)} to do all of the above`,
950
- options.components.settings ? `Review ${output.highlight('.claude/settings.json')} for hook configurations` : '',
951
- // ADR-150 — surface the new metaharness scorecard to every new user.
952
- // Optional dep; the command degrades gracefully when not installed.
953
- `Run ${output.highlight(`${bin} metaharness score`)} for a 5-dim harness readiness scorecard (ADR-150)`,
954
- ].filter(Boolean));
955
- }
956
979
  if (ctx.flags.format === 'json') {
957
980
  output.printJson(result);
958
981
  }
959
982
  await offerCapabilityEnrollment(ctx);
983
+ await offerTelemetryOptIn(ctx);
984
+ await noteInitCompleted();
985
+ if (!startDaemon && !startAll && ctx.flags.format !== 'json') {
986
+ // End on something about the user's own repo, not a list of commands
987
+ // (adoption plan item 1). Printed after the optional prompts so it is
988
+ // the last thing on screen. Local-only, time-capped, never throws.
989
+ const { snapshotProject, snapshotLines, firstPrompt } = await import('../init/first-win.js');
990
+ const snap = snapshotProject(ctx.cwd);
991
+ const lines = snapshotLines(snap);
992
+ output.writeln();
993
+ if (lines.length) {
994
+ output.writeln(output.bold('Your project:'));
995
+ output.printList(lines);
996
+ output.writeln();
997
+ }
998
+ output.writeln(output.bold('Try it — open Claude Code here and paste:'));
999
+ output.writeln(` ${output.highlight(firstPrompt(snap))}`);
1000
+ output.writeln();
1001
+ const bin = (process.argv[1] || '').includes('ruflo') ? 'ruflo' : 'claude-flow';
1002
+ output.writeln(output.dim(`Background workers: ${bin} daemon start · health check: ${bin} doctor`));
1003
+ // ADR-150: surface the metaharness scorecard (optional dep; degrades gracefully when absent).
1004
+ output.writeln(output.dim(`Harness readiness: ${bin} metaharness score (5-dim scorecard)`));
1005
+ }
960
1006
  return { success: true, data: result };
961
1007
  }
962
1008
  catch (error) {
@@ -1298,6 +1344,8 @@ const wizardCommand = {
1298
1344
  ],
1299
1345
  });
1300
1346
  await offerCapabilityEnrollment(ctx);
1347
+ await offerTelemetryOptIn(ctx);
1348
+ await noteInitCompleted();
1301
1349
  return { success: true, data: result };
1302
1350
  }
1303
1351
  catch (error) {
@@ -1714,6 +1762,12 @@ export const initCommand = {
1714
1762
  type: 'boolean',
1715
1763
  default: false,
1716
1764
  },
1765
+ {
1766
+ name: 'no-telemetry-prompt',
1767
+ description: 'Skip the one-time question about anonymous usage telemetry (default answer is No)',
1768
+ type: 'boolean',
1769
+ default: false,
1770
+ },
1717
1771
  {
1718
1772
  name: 'only-claude',
1719
1773
  description: 'Only create .claude/ directory (skip runtime)',
@@ -276,6 +276,21 @@ const startCommand = {
276
276
  });
277
277
  // Start the server
278
278
  const status = await manager.start();
279
+ // Plugin-user visibility (ADR-309 operational telemetry): plugin-only
280
+ // users never run `init`, so no SessionStart hook ever refreshes the
281
+ // funnel cache for them. When the ruflo-core plugin launched us, make
282
+ // the same 6h-cached /v1/messages fetch, honouring every opt-out. Off
283
+ // the start path, never awaited, and silent: stdout is the MCP channel.
284
+ if (transport === 'stdio') {
285
+ void (async () => {
286
+ try {
287
+ const { launchSurface, resolveFunnelEnabled, refreshRemoteMessages } = await import('../funnel/index.js');
288
+ if (launchSurface() && resolveFunnelEnabled().enabled)
289
+ await refreshRemoteMessages();
290
+ }
291
+ catch { /* best-effort */ }
292
+ })();
293
+ }
279
294
  // #2234 — exit cleanly if Claude Code (our parent) exits and we get
280
295
  // reparented to launchd/init (ppid === 1). Otherwise the node stdio
281
296
  // server lingers as an orphan, accumulating ~50 MB per restart, and an
@@ -1526,7 +1526,9 @@ const benchmarkCommand = {
1526
1526
  // when it isn't installed — install-safety / Build V3 pattern from #2586).
1527
1527
  const attentionPkg = '@ruvector/attention';
1528
1528
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic import of optional native WASM module with no type declarations
1529
- const attention = await import(attentionPkg);
1529
+ // @ruvector/attention is CommonJS: under import() its classes sit on `default`.
1530
+ const attentionMod = await import(attentionPkg);
1531
+ const attention = (attentionMod.default ?? attentionMod);
1530
1532
  // Manual benchmark since benchmarkAttention has a binding bug
1531
1533
  const benchmarkMechanism = async (name, mechanism) => {
1532
1534
  const query = new Float32Array(dim);
@@ -1546,10 +1548,10 @@ const benchmarkCommand = {
1546
1548
  }
1547
1549
  // Warmup
1548
1550
  for (let i = 0; i < 10; i++)
1549
- mechanism.computeRaw(query, keys, values);
1551
+ mechanism.compute(query, keys, values);
1550
1552
  const start = performance.now();
1551
1553
  for (let i = 0; i < iterations; i++) {
1552
- mechanism.computeRaw(query, keys, values);
1554
+ mechanism.compute(query, keys, values);
1553
1555
  }
1554
1556
  const elapsed = performance.now() - start;
1555
1557
  return {
@@ -8,6 +8,7 @@ import { callMCPTool, MCPClientError } from '../mcp-client.js';
8
8
  import { swarmJoinCommand } from './agntcy/swarm-join.js';
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
+ import { noteFirstAgentRun } from '../funnel/activation.js';
11
12
  // Read the CLI-side swarm state file (`.swarm/state.json`), written by
12
13
  // `swarm init` and rewritten by `swarm start` / `swarm stop`.
13
14
  function readLocalSwarmState() {
@@ -566,6 +567,7 @@ const initCommand = {
566
567
  if (ctx.flags.format === 'json') {
567
568
  output.printJson(result);
568
569
  }
570
+ noteFirstAgentRun('cli'); // activation funnel (opt-in; no-op without telemetry consent)
569
571
  return { success: true, data: result };
570
572
  }
571
573
  catch (error) {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Activation funnel (ruflo adoption plan; ADR-309 opt-in telemetry).
3
+ *
4
+ * installed -> init_completed -> first_agent_run -> session_active (7-day retention)
5
+ *
6
+ * Every event goes through recordFunnelEvent, which records nothing without
7
+ * an explicit, current `telemetry` consent receipt. The once-ever and
8
+ * once-per-day markers live in their own state file because a successful
9
+ * flush removes sent events from the queue (lastRecordedEvent would forget).
10
+ */
11
+ import type { FunnelSurface } from './types.js';
12
+ /** `ruflo init` finished in a project. Recorded on every successful init. */
13
+ export declare function recordInitCompleted(release: string, now?: Date): boolean;
14
+ /** The first agent spawn or swarm init on this machine. Recorded once, ever. */
15
+ export declare function recordFirstAgentRun(release: string, surface?: FunnelSurface, now?: Date): boolean;
16
+ /** At most one "active today" marker per calendar day (UTC). */
17
+ export declare function recordSessionActive(release: string, now?: Date): boolean;
18
+ /**
19
+ * Whether `ruflo init` should ask the one-time telemetry question: an
20
+ * interactive terminal, not CI, funnel not disabled, no telemetry receipt
21
+ * yet (asked once, whatever the answer), and not suppressed by flag.
22
+ */
23
+ export declare function shouldOfferTelemetry(opts?: {
24
+ noTelemetryPrompt?: boolean;
25
+ cwd?: string;
26
+ env?: NodeJS.ProcessEnv;
27
+ }): boolean;
28
+ /** Fire-and-forget wrapper for command handlers: never throws, never blocks. */
29
+ export declare function noteFirstAgentRun(surface?: FunnelSurface): void;
30
+ //# sourceMappingURL=activation.d.ts.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Activation funnel (ruflo adoption plan; ADR-309 opt-in telemetry).
3
+ *
4
+ * installed -> init_completed -> first_agent_run -> session_active (7-day retention)
5
+ *
6
+ * Every event goes through recordFunnelEvent, which records nothing without
7
+ * an explicit, current `telemetry` consent receipt. The once-ever and
8
+ * once-per-day markers live in their own state file because a successful
9
+ * flush removes sent events from the queue (lastRecordedEvent would forget).
10
+ */
11
+ import { getConsent } from './consent.js';
12
+ import { recordFunnelEvent } from './events.js';
13
+ import { isCI, isInteractive } from './environment.js';
14
+ import { resolveFunnelEnabled } from './precedence.js';
15
+ import { readStateJson, writeStateJson } from './state.js';
16
+ const MARKERS_FILE = 'funnel-activation.json';
17
+ const day = (now) => now.toISOString().slice(0, 10);
18
+ function markers() {
19
+ return readStateJson(MARKERS_FILE) ?? {};
20
+ }
21
+ /** `ruflo init` finished in a project. Recorded on every successful init. */
22
+ export function recordInitCompleted(release, now = new Date()) {
23
+ return recordFunnelEvent('init_completed', 'init', release, now);
24
+ }
25
+ /** The first agent spawn or swarm init on this machine. Recorded once, ever. */
26
+ export function recordFirstAgentRun(release, surface = 'cli', now = new Date()) {
27
+ const m = markers();
28
+ if (m.firstAgentRunAt)
29
+ return false;
30
+ if (!recordFunnelEvent('first_agent_run', surface, release, now))
31
+ return false;
32
+ writeStateJson(MARKERS_FILE, { ...m, firstAgentRunAt: now.toISOString() });
33
+ return true;
34
+ }
35
+ /** At most one "active today" marker per calendar day (UTC). */
36
+ export function recordSessionActive(release, now = new Date()) {
37
+ const m = markers();
38
+ const today = day(now);
39
+ if (m.lastActiveDay === today)
40
+ return false;
41
+ if (!recordFunnelEvent('session_active', 'cli', release, now))
42
+ return false;
43
+ writeStateJson(MARKERS_FILE, { ...m, lastActiveDay: today });
44
+ return true;
45
+ }
46
+ /**
47
+ * Whether `ruflo init` should ask the one-time telemetry question: an
48
+ * interactive terminal, not CI, funnel not disabled, no telemetry receipt
49
+ * yet (asked once, whatever the answer), and not suppressed by flag.
50
+ */
51
+ export function shouldOfferTelemetry(opts = {}) {
52
+ const env = opts.env ?? process.env;
53
+ if (opts.noTelemetryPrompt)
54
+ return false;
55
+ if (isCI(env))
56
+ return false;
57
+ if (!isInteractive())
58
+ return false;
59
+ if (!resolveFunnelEnabled(opts.cwd ?? process.cwd(), env).enabled)
60
+ return false;
61
+ return getConsent('telemetry').at === null;
62
+ }
63
+ /** Fire-and-forget wrapper for command handlers: never throws, never blocks. */
64
+ export function noteFirstAgentRun(surface = 'cli') {
65
+ void import('../mcp-protocol.js')
66
+ .then(({ rufloPackageVersion }) => { recordFirstAgentRun(rufloPackageVersion(), surface); })
67
+ .catch(() => undefined);
68
+ }
69
+ //# sourceMappingURL=activation.js.map
@@ -39,8 +39,11 @@ const EVENT_NAMES = [
39
39
  'training_share_disabled',
40
40
  'advisor_tip_enabled',
41
41
  'advisor_tip_disabled',
42
+ 'init_completed',
43
+ 'first_agent_run',
44
+ 'session_active',
42
45
  ];
43
- const SURFACES = ['statusline', 'init', 'credit_exhaustion'];
46
+ const SURFACES = ['statusline', 'init', 'credit_exhaustion', 'cli'];
44
47
  /** Rotate the pseudonymous ID every 90 days (ADR-305). */
45
48
  const FUNNEL_ID_TTL_MS = 90 * 24 * 60 * 60 * 1000;
46
49
  /**
@@ -9,7 +9,8 @@ export { resolveFunnelEnabled } from './precedence.js';
9
9
  export { CONSENT_DOMAINS, getConsent, hasConsent, readConsents, recordConsent, revokeConsent, } from './consent.js';
10
10
  export { DISCLOSURE_GRACE_MS, DISCLOSURE_ROTATION_SLOT_MS, getDisclosure, promoEligible, recordDisclosureAccepted, recordDisclosureDeclined, recordDisclosureReenabled, recordDisclosureShown, selectDisclosureMessage, } from './disclosure.js';
11
11
  export { MAX_MESSAGE_COLUMNS, MESSAGES, containsForbiddenSequences, displayWidth, eligibleMessages, eligibleMessagesFromPools, isAllowedUrl, isValidMessage, } from './messages.js';
12
- export { DEFAULT_MESSAGES_ENDPOINT, getRemoteMessages, refreshRemoteMessages, } from './message-transport.js';
12
+ export { DEFAULT_MESSAGES_ENDPOINT, getRemoteMessages, funnelUserAgent, launchSurface, refreshRemoteMessages, } from './message-transport.js';
13
+ export { flushEvents } from './event-transport.js';
13
14
  export { PROMO_REPEAT_CAP_MS, PROMO_SLOT_MODULO, ROTATION_SLOT_MS, selectMessage, } from './rotation.js';
14
15
  export { CREDIT_RECOVERY_HINT, classifyCreditError, renderCreditRecovery, shouldShowCreditRecovery, type CreditPromptSession, type ProviderErrorLike, } from './credit-errors.js';
15
16
  export { deleteFunnelData, getFunnelId, lastRecordedEvent, recordFunnelEvent } from './events.js';
@@ -9,7 +9,8 @@ export { resolveFunnelEnabled } from './precedence.js';
9
9
  export { CONSENT_DOMAINS, getConsent, hasConsent, readConsents, recordConsent, revokeConsent, } from './consent.js';
10
10
  export { DISCLOSURE_GRACE_MS, DISCLOSURE_ROTATION_SLOT_MS, getDisclosure, promoEligible, recordDisclosureAccepted, recordDisclosureDeclined, recordDisclosureReenabled, recordDisclosureShown, selectDisclosureMessage, } from './disclosure.js';
11
11
  export { MAX_MESSAGE_COLUMNS, MESSAGES, containsForbiddenSequences, displayWidth, eligibleMessages, eligibleMessagesFromPools, isAllowedUrl, isValidMessage, } from './messages.js';
12
- export { DEFAULT_MESSAGES_ENDPOINT, getRemoteMessages, refreshRemoteMessages, } from './message-transport.js';
12
+ export { DEFAULT_MESSAGES_ENDPOINT, getRemoteMessages, funnelUserAgent, launchSurface, refreshRemoteMessages, } from './message-transport.js';
13
+ export { flushEvents } from './event-transport.js';
13
14
  export { PROMO_REPEAT_CAP_MS, PROMO_SLOT_MODULO, ROTATION_SLOT_MS, selectMessage, } from './rotation.js';
14
15
  export { CREDIT_RECOVERY_HINT, classifyCreditError, renderCreditRecovery, shouldShowCreditRecovery, } from './credit-errors.js';
15
16
  export { deleteFunnelData, getFunnelId, lastRecordedEvent, recordFunnelEvent } from './events.js';
@@ -31,6 +31,18 @@
31
31
  * the trust boundary. All content is treated as untrusted regardless.
32
32
  */
33
33
  import type { FunnelMessage } from './types.js';
34
+ /**
35
+ * User-Agent for the messages fetch — ADR-309 operational telemetry (see
36
+ * docs/privacy/funnel-legitimate-interest-assessment.md). Only the CLI
37
+ * release and the coarse OS/CPU family, so the server can count releases
38
+ * and platforms. No identifier, path, hostname, or account: the
39
+ * `ruflo-funnel/messages` prefix stays first so existing log filters match.
40
+ * A trailing `surface/plugin` marks a fetch made by the ruflo-core plugin's
41
+ * MCP launch, so plugin-only users (who never run `init`) can be counted.
42
+ */
43
+ export declare function funnelUserAgent(version?: string, platform?: string, arch?: string, surface?: string | undefined): string;
44
+ /** The surface set by a launcher (plugins/ruflo-core/scripts/mcp-launch.cjs), if any. */
45
+ export declare function launchSurface(env?: NodeJS.ProcessEnv): string | undefined;
34
46
  /** Endpoint the client hits — overridable for staging / self-hosted. */
35
47
  export declare const DEFAULT_MESSAGES_ENDPOINT: string;
36
48
  /**
@@ -34,7 +34,29 @@ import * as https from 'https';
34
34
  import { URL as NodeUrl } from 'url';
35
35
  import { isValidMessage } from './messages.js';
36
36
  import { readStateJson, writeStateJson } from './state.js';
37
+ import { rufloPackageVersion } from '../mcp-protocol.js';
37
38
  const CACHE_FILE = 'funnel-messages-cache.json';
39
+ /**
40
+ * User-Agent for the messages fetch — ADR-309 operational telemetry (see
41
+ * docs/privacy/funnel-legitimate-interest-assessment.md). Only the CLI
42
+ * release and the coarse OS/CPU family, so the server can count releases
43
+ * and platforms. No identifier, path, hostname, or account: the
44
+ * `ruflo-funnel/messages` prefix stays first so existing log filters match.
45
+ * A trailing `surface/plugin` marks a fetch made by the ruflo-core plugin's
46
+ * MCP launch, so plugin-only users (who never run `init`) can be counted.
47
+ */
48
+ export function funnelUserAgent(version = rufloPackageVersion(), platform = process.platform, arch = process.arch, surface = launchSurface()) {
49
+ const clean = (v) => v.replace(/[^A-Za-z0-9._-]/g, '').slice(0, 32) || 'unknown';
50
+ const tail = surface ? ` surface/${surface}` : '';
51
+ return `ruflo-funnel/messages ruflo/${clean(version)} (${clean(platform)}; ${clean(arch)})${tail}`;
52
+ }
53
+ /** Launch surfaces the UA may name. Closed set: an env value outside it is dropped. */
54
+ const LAUNCH_SURFACES = new Set(['plugin']);
55
+ /** The surface set by a launcher (plugins/ruflo-core/scripts/mcp-launch.cjs), if any. */
56
+ export function launchSurface(env = process.env) {
57
+ const v = env.RUFLO_LAUNCH_SURFACE;
58
+ return v && LAUNCH_SURFACES.has(v) ? v : undefined;
59
+ }
38
60
  const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6h — matches server Cache-Control
39
61
  const CACHE_MAX_BYTES = 128 * 1024;
40
62
  const CACHE_MAX_MESSAGES = 200;
@@ -79,7 +101,7 @@ function httpsGet(url) {
79
101
  path: target.pathname + target.search,
80
102
  method: 'GET',
81
103
  headers: {
82
- 'User-Agent': 'ruflo-funnel/messages',
104
+ 'User-Agent': funnelUserAgent(),
83
105
  'Accept': 'application/json',
84
106
  },
85
107
  }, (res) => {
@@ -77,8 +77,8 @@ export interface NormalizedCreditError {
77
77
  /** Original provider error, preserved verbatim for --verbose paths. */
78
78
  cause?: unknown;
79
79
  }
80
- export type FunnelEventName = 'disclosure_shown' | 'funnel_disabled' | 'signup_opened' | 'account_created' | 'proxy_activated' | 'promo_impression' | 'promo_open' | 'sponsor_mode_enabled' | 'sponsor_mode_disabled' | 'sponsor_capacity_exhausted' | 'power_saver_enabled' | 'power_saver_disabled' | 'toggle_cooldown_blocked' | 'training_share_enabled' | 'training_share_disabled' | 'advisor_tip_enabled' | 'advisor_tip_disabled';
81
- export type FunnelSurface = 'statusline' | 'init' | 'credit_exhaustion';
80
+ export type FunnelEventName = 'disclosure_shown' | 'funnel_disabled' | 'signup_opened' | 'account_created' | 'proxy_activated' | 'promo_impression' | 'promo_open' | 'sponsor_mode_enabled' | 'sponsor_mode_disabled' | 'sponsor_capacity_exhausted' | 'power_saver_enabled' | 'power_saver_disabled' | 'toggle_cooldown_blocked' | 'training_share_enabled' | 'training_share_disabled' | 'advisor_tip_enabled' | 'advisor_tip_disabled' | 'init_completed' | 'first_agent_run' | 'session_active';
81
+ export type FunnelSurface = 'statusline' | 'init' | 'credit_exhaustion' | 'cli';
82
82
  export interface FunnelEvent {
83
83
  schemaVersion: 1;
84
84
  event: FunnelEventName;