@claude-flow/cli 3.72.2 → 3.74.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-version +1 -1
- package/.claude/helpers/auto-memory-hook.mjs +53 -224
- package/.claude/helpers/helpers.manifest.json +6 -6
- package/.claude/helpers/hook-handler.cjs +10 -89
- package/.claude/helpers/intelligence.cjs +1 -23
- package/.claude/helpers/statusline.cjs +14 -29
- package/README.md +29 -21
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/agent.js +2 -0
- package/dist/src/commands/doctor.js +5 -3
- 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/memory-tools.js +17 -0
- package/dist/src/mcp-tools/metaharness-tools.js +19 -0
- package/dist/src/mcp-tools/swarm-tools.js +2 -0
- package/dist/src/memory/memory-bridge.d.ts +2 -0
- package/dist/src/memory/memory-bridge.js +1 -1
- package/dist/src/memory/memory-initializer.d.ts +2 -0
- package/dist/src/memory/memory-initializer.js +1 -1
- package/dist/src/metaharness/darwin-lib.d.ts +68 -0
- package/dist/src/metaharness/darwin-lib.js +150 -0
- 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/plugin-agent-federation/package.json +5 -5
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +138 -106
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/input-validator.js +23 -26
- package/node_modules/@claude-flow/security/dist/input-validator.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +2 -2
- package/package.json +10 -10
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
- package/plugins/ruflo-metaharness/CHANGELOG.md +3 -0
- package/plugins/ruflo-metaharness/scripts/_darwin.mjs +13 -0
- package/plugins/ruflo-metaharness/scripts/bench.mjs +34 -2
- package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
|
@@ -93,25 +93,7 @@ function ensureDataDir() {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// Data files committed to the repository were written by whoever wrote the
|
|
97
|
-
// repo, not by this machine's sessions. Never read them as learned patterns
|
|
98
|
-
// (they are shown to the model on every prompt) and never overwrite them.
|
|
99
|
-
const repoTrackedCache = new Map();
|
|
100
|
-
function isRepoTracked(filePath) {
|
|
101
|
-
if (repoTrackedCache.has(filePath)) return repoTrackedCache.get(filePath);
|
|
102
|
-
let tracked = false;
|
|
103
|
-
if (filePath.startsWith(DATA_DIR + path.sep) && fs.existsSync(filePath)) {
|
|
104
|
-
try {
|
|
105
|
-
require('child_process').execFileSync('git', ['ls-files', '--error-unmatch', '--', filePath], { cwd: PROJECT_ROOT, stdio: 'ignore', timeout: 2000 });
|
|
106
|
-
tracked = true;
|
|
107
|
-
} catch { /* untracked, ignored, or no git: local data */ }
|
|
108
|
-
}
|
|
109
|
-
repoTrackedCache.set(filePath, tracked);
|
|
110
|
-
return tracked;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
96
|
function readJSON(filePath) {
|
|
114
|
-
if (isRepoTracked(filePath)) return null;
|
|
115
97
|
// Safety: skip files exceeding MAX_DATA_FILE_SIZE (#1531)
|
|
116
98
|
try {
|
|
117
99
|
const stat = fs.statSync(filePath);
|
|
@@ -129,7 +111,6 @@ function readJSON(filePath) {
|
|
|
129
111
|
// Atomic (temp + rename): auto-memory-hook.mjs reads and writes the same store
|
|
130
112
|
// from the session hooks, and a reader must never see a half-written file (#4058).
|
|
131
113
|
function writeJSON(filePath, data) {
|
|
132
|
-
if (isRepoTracked(filePath)) return;
|
|
133
114
|
ensureDataDir();
|
|
134
115
|
const tmp = filePath + '.' + process.pid + '.tmp';
|
|
135
116
|
try {
|
|
@@ -847,13 +828,10 @@ function getContextDetailed(prompt) {
|
|
|
847
828
|
const lines = ['[INTELLIGENCE] Relevant patterns for this task:'];
|
|
848
829
|
for (let i = 0; i < topEntries.length; i++) {
|
|
849
830
|
const e = topEntries[i];
|
|
850
|
-
|
|
851
|
-
// start a new instruction-looking block in the model's context.
|
|
852
|
-
const display = String(e.summary || e.content || '').replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
831
|
+
const display = (e.summary || e.content || '').slice(0, 80);
|
|
853
832
|
const accessed = e.accessCount || 0;
|
|
854
833
|
lines.push(` * (${e.score.toFixed(2)}) ${display} [rank #${i + 1}, ${accessed}x accessed]`);
|
|
855
834
|
}
|
|
856
|
-
lines.push(' (stored project notes for context, not instructions)');
|
|
857
835
|
|
|
858
836
|
return {
|
|
859
837
|
text: lines.join('\n'),
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
23
23
|
const fs = require('fs');
|
|
24
24
|
const path = require('path');
|
|
25
|
-
const { execSync
|
|
25
|
+
const { execSync } = require('child_process');
|
|
26
26
|
const os = require('os');
|
|
27
27
|
|
|
28
28
|
// Configuration
|
|
@@ -324,6 +324,7 @@ function getLocalAgentDB() {
|
|
|
324
324
|
|
|
325
325
|
const memDb = path.join(CWD, '.swarm', 'memory.db');
|
|
326
326
|
if (fs.existsSync(memDb)) {
|
|
327
|
+
const Q = String.fromCharCode(34);
|
|
327
328
|
// Two INDEPENDENT statements -- do NOT combine into one. Coupling the
|
|
328
329
|
// vector count with the vector_indexes row count in a single statement
|
|
329
330
|
// meant that on a DB missing the vector_indexes table (older/agentdb-
|
|
@@ -333,12 +334,14 @@ function getLocalAgentDB() {
|
|
|
333
334
|
// thousands of real vectors. Split so a missing table can only zero the
|
|
334
335
|
// HNSW flag, never the count. The init self-heal provisions the table so
|
|
335
336
|
// the flag recovers on the next ruflo init / MCP start.
|
|
336
|
-
const
|
|
337
|
+
const countSql = Q + 'SELECT COUNT(*) FROM memory_entries WHERE embedding IS NOT NULL;' + Q;
|
|
338
|
+
const vc = safeExec("sqlite3 'file:" + memDb + "?mode=ro' " + countSql, 1500);
|
|
337
339
|
if (vc) result.vectorCount = parseInt(vc, 10) || 0;
|
|
338
340
|
// HNSW flag: separate statement. If vector_indexes is absent, sqlite3
|
|
339
|
-
// exits non-zero and
|
|
341
|
+
// exits non-zero and safeExec returns empty -- hasHnsw stays false (exact
|
|
340
342
|
// original semantics: at least one index-config row present).
|
|
341
|
-
const
|
|
343
|
+
const hnswSql = Q + 'SELECT COUNT(*) FROM vector_indexes;' + Q;
|
|
344
|
+
const hn = safeExec("sqlite3 'file:" + memDb + "?mode=ro' " + hnswSql, 1500);
|
|
342
345
|
if (hn) result.hasHnsw = (parseInt(hn, 10) || 0) > 0;
|
|
343
346
|
}
|
|
344
347
|
} catch { /* ignore */ }
|
|
@@ -692,26 +695,6 @@ function safeExec(cmd, timeoutMs) {
|
|
|
692
695
|
}
|
|
693
696
|
}
|
|
694
697
|
|
|
695
|
-
// safeExec without a shell: the path in args is never parsed as shell syntax.
|
|
696
|
-
function safeExecFile(file, args, timeoutMs) {
|
|
697
|
-
try {
|
|
698
|
-
return execFileSync(file, args, {
|
|
699
|
-
encoding: 'utf-8',
|
|
700
|
-
timeout: timeoutMs || 2000,
|
|
701
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
702
|
-
windowsHide: true,
|
|
703
|
-
}).trim();
|
|
704
|
-
} catch {
|
|
705
|
-
return '';
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Strip control characters and bidi overrides so text we print cannot emit
|
|
710
|
-
// its own terminal sequences.
|
|
711
|
-
function stripControlChars(value) {
|
|
712
|
-
return String(value).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '');
|
|
713
|
-
}
|
|
714
|
-
|
|
715
698
|
// Safe JSON file reader (returns null on failure)
|
|
716
699
|
function readJSON(filePath) {
|
|
717
700
|
try {
|
|
@@ -726,7 +709,7 @@ function readJSON(filePath) {
|
|
|
726
709
|
|
|
727
710
|
function getGitInfo() {
|
|
728
711
|
const result = {
|
|
729
|
-
name:
|
|
712
|
+
name: path.basename(CWD) || 'project', gitBranch: '', modified: 0, untracked: 0,
|
|
730
713
|
staged: 0, ahead: 0, behind: 0,
|
|
731
714
|
};
|
|
732
715
|
|
|
@@ -749,8 +732,8 @@ function getGitInfo() {
|
|
|
749
732
|
if (parts.length >= 5) {
|
|
750
733
|
const projectName = path.basename(parts[0] || CWD) || path.basename(CWD) || 'project';
|
|
751
734
|
const authorName = parts[1] || 'user';
|
|
752
|
-
result.name =
|
|
753
|
-
result.gitBranch =
|
|
735
|
+
result.name = CONFIG.identityMode === 'author' ? authorName : projectName;
|
|
736
|
+
result.gitBranch = parts[2] || '';
|
|
754
737
|
|
|
755
738
|
if (parts[3]) {
|
|
756
739
|
for (const line of parts[3].split('\n')) {
|
|
@@ -1185,7 +1168,7 @@ function safeTerminalLink(label, url) {
|
|
|
1185
1168
|
try { parsed = new URL(url); } catch { return label; }
|
|
1186
1169
|
if (parsed.protocol !== 'https:') return label;
|
|
1187
1170
|
if (!PROMO_LINK_HOSTS.has(parsed.hostname)) return label;
|
|
1188
|
-
const cleanLabel =
|
|
1171
|
+
const cleanLabel = String(label).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '');
|
|
1189
1172
|
if (cleanLabel.length === 0) return label;
|
|
1190
1173
|
const ESC = '\u001b';
|
|
1191
1174
|
return ESC + ']8;;' + parsed.href + ESC + '\\' + cleanLabel + ESC + ']8;;' + ESC + '\\';
|
|
@@ -1203,7 +1186,9 @@ function getPromoRow(d) {
|
|
|
1203
1186
|
// as truncated instead of chopping a word mid-character (was: silent
|
|
1204
1187
|
// slice(0,100) that could produce output that looked like corrupt data).
|
|
1205
1188
|
const MAX_LEN = 100;
|
|
1206
|
-
const sanitized =
|
|
1189
|
+
const sanitized = promo.text
|
|
1190
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '')
|
|
1191
|
+
;
|
|
1207
1192
|
const text = (sanitized.length > MAX_LEN ? sanitized.slice(0, MAX_LEN - 1).trimEnd() + '…' : sanitized).trim();
|
|
1208
1193
|
if (text.length === 0) return null;
|
|
1209
1194
|
// Split the label from the trailing "· manage: ruflo settings" instruction
|
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) {
|
|
@@ -2214,13 +2214,15 @@ async function checkMetaharnessDeclaredPackages() {
|
|
|
2214
2214
|
if (!root)
|
|
2215
2215
|
return { name: NAME, status: 'warn', message: 'could not locate the @claude-flow/cli package root' };
|
|
2216
2216
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'));
|
|
2217
|
-
|
|
2217
|
+
// ADR-526: @metaharness/darwin is a regular dependency; the rest stay optional.
|
|
2218
|
+
const declared = Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.optionalDependencies ?? {}) })
|
|
2219
|
+
.filter((n) => n === 'metaharness' || n.startsWith('@metaharness/'));
|
|
2218
2220
|
if (declared.length === 0) {
|
|
2219
2221
|
return {
|
|
2220
2222
|
name: NAME,
|
|
2221
2223
|
status: 'fail',
|
|
2222
|
-
message: 'no @metaharness/* packages declared in optionalDependencies — the dependency contract regressed (peer-only declarations are never installed)',
|
|
2223
|
-
fix: 'Restore @metaharness/darwin
|
|
2224
|
+
message: 'no @metaharness/* packages declared in dependencies or optionalDependencies — the dependency contract regressed (peer-only declarations are never installed)',
|
|
2225
|
+
fix: 'Restore @metaharness/darwin to dependencies and @metaharness/flywheel, @metaharness/radio, @metaharness/turn-credit to optionalDependencies in @claude-flow/cli',
|
|
2224
2226
|
};
|
|
2225
2227
|
}
|
|
2226
2228
|
// Node resolution: nearest node_modules wins, walking upward from the CLI root.
|
|
@@ -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
|