@claude-flow/cli 3.72.2 → 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.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/README.md +29 -21
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/agent.js +2 -0
- package/dist/src/commands/funnel.d.ts +1 -0
- package/dist/src/commands/funnel.js +37 -2
- package/dist/src/commands/hooks.js +16 -2
- package/dist/src/commands/init.js +70 -16
- package/dist/src/commands/mcp.js +15 -0
- package/dist/src/commands/swarm.js +2 -0
- package/dist/src/funnel/activation.d.ts +30 -0
- package/dist/src/funnel/activation.js +69 -0
- package/dist/src/funnel/events.js +4 -1
- package/dist/src/funnel/index.d.ts +2 -1
- package/dist/src/funnel/index.js +2 -1
- package/dist/src/funnel/message-transport.d.ts +12 -0
- package/dist/src/funnel/message-transport.js +23 -1
- package/dist/src/funnel/types.d.ts +2 -2
- package/dist/src/init/first-win.d.ts +24 -0
- package/dist/src/init/first-win.js +97 -0
- package/dist/src/mcp-tools/agent-tools.js +2 -0
- package/dist/src/mcp-tools/swarm-tools.js +2 -0
- package/dist/src/missions/schemas.d.ts +117 -117
- package/dist/src/mods/command-registry/schema.d.ts +121 -121
- package/dist/src/transfer/store/download.d.ts +1 -1
- package/dist/src/transfer/store/download.js +22 -21
- package/dist/src/transfer/store/publish.d.ts +1 -1
- package/dist/src/transfer/store/publish.js +13 -14
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +5 -5
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
- package/package.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.
|
|
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": "
|
|
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
|
[](https://www.npmjs.com/package/ruflo)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
|
-
[](https://github.com/ruvnet/ruflo)
|
|
11
11
|
[](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
|
|
26
|
+
## <img src="docs/assets/readme/icons/terminal.svg" width="32" height="32" alt=""> Get started in one command
|
|
27
27
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
[First useful task, step by step](docs/ruflo-explained.md) · [User guide](docs/USERGUIDE.md) · [Compare install paths](#quick-start)
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```
|
|
46
|
+
<details>
|
|
47
|
+
<summary><b>Other ways to install</b> (Claude Code plugins only, MCP-only clients, terminal UI)</summary>
|
|
48
48
|
|
|
49
|
-
|
|
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
|
-
```
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. |
|
package/catalog-manifest.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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)',
|
package/dist/src/commands/mcp.js
CHANGED
|
@@ -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
|
|
@@ -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';
|
package/dist/src/funnel/index.js
CHANGED
|
@@ -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':
|
|
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;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The concrete result `ruflo init` ends with (adoption plan item 1): a quick,
|
|
3
|
+
* local snapshot of the user's own project, plus one prompt to paste into
|
|
4
|
+
* Claude Code. Replaces a list of commands with something about *their* repo.
|
|
5
|
+
*
|
|
6
|
+
* Local only: `git` with argument arrays (no shell), a hard timeout per call,
|
|
7
|
+
* no network. Any failure returns null and init prints nothing extra.
|
|
8
|
+
*/
|
|
9
|
+
export interface ProjectSnapshot {
|
|
10
|
+
files: number;
|
|
11
|
+
languages: string[];
|
|
12
|
+
hotFile?: {
|
|
13
|
+
path: string;
|
|
14
|
+
changes: number;
|
|
15
|
+
};
|
|
16
|
+
testCommand?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Snapshot of the project at `cwd`, or null when it is not a git repo with code. */
|
|
19
|
+
export declare function snapshotProject(cwd: string, timeoutMs?: number): ProjectSnapshot | null;
|
|
20
|
+
/** The one prompt to paste into Claude Code, specific to this project. */
|
|
21
|
+
export declare function firstPrompt(s: ProjectSnapshot | null): string;
|
|
22
|
+
/** Lines for init's closing summary. Empty when there is nothing useful to say. */
|
|
23
|
+
export declare function snapshotLines(s: ProjectSnapshot | null): string[];
|
|
24
|
+
//# sourceMappingURL=first-win.d.ts.map
|