claude-flow 3.49.0 → 3.50.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/hook-handler.cjs +14 -1
- package/.claude/proven-config.json +1 -1
- package/.claude-plugin/marketplace.json +10 -0
- package/package.json +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +3 -3
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +19 -1
- package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/commands/init.js +20 -0
- package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/commands/mods.js +126 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +12 -0
- package/v3/@claude-flow/cli/dist/src/mods/claude-installs.d.ts +29 -0
- package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +83 -0
- package/v3/@claude-flow/cli/dist/src/mods/install.d.ts +64 -0
- package/v3/@claude-flow/cli/dist/src/mods/install.js +135 -0
- package/v3/@claude-flow/cli/dist/src/mods/policy-projection.d.ts +39 -0
- package/v3/@claude-flow/cli/dist/src/mods/policy-projection.js +65 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.js +118 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +15 -0
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -286,6 +286,17 @@ async function readStdin() {
|
|
|
286
286
|
});
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// ADR-404: the ruflo mod (plugins/ruflo-mods) runs these events in-process
|
|
290
|
+
// inside Claude Code and sets RUFLO_MODS_OWNS on the process, which every hook
|
|
291
|
+
// started after inherits. An event named there is the mod's; returning here
|
|
292
|
+
// keeps it from firing twice. Only side-effect events can be handed over:
|
|
293
|
+
// guards such as pre-bash always run, whatever the variable says.
|
|
294
|
+
const MOD_OWNABLE_EVENTS = new Set(['route', 'post-edit']);
|
|
295
|
+
function ownedByMod(cmd, env = process.env) {
|
|
296
|
+
if (!MOD_OWNABLE_EVENTS.has(cmd)) return false;
|
|
297
|
+
return String(env.RUFLO_MODS_OWNS || '').split(',').some((owned) => owned.trim() === cmd);
|
|
298
|
+
}
|
|
299
|
+
|
|
289
300
|
function claimSideEffectEvent(family, stdinData, event) {
|
|
290
301
|
if (/^(1|true|yes|on)$/i.test(process.env.RUFLO_DISABLE_HOOK_DEDUP || '')) return true;
|
|
291
302
|
try {
|
|
@@ -312,6 +323,8 @@ function claimSideEffectEvent(family, stdinData, event) {
|
|
|
312
323
|
}
|
|
313
324
|
|
|
314
325
|
async function main() {
|
|
326
|
+
if (ownedByMod(command)) return;
|
|
327
|
+
|
|
315
328
|
// Global safety timeout: hooks must NEVER hang (#1530, #1531)
|
|
316
329
|
const safetyTimer = setTimeout(() => {
|
|
317
330
|
process.stderr.write("[WARN] Hook handler global timeout (5s), forcing exit\n");
|
|
@@ -606,4 +619,4 @@ if (require.main === module) {
|
|
|
606
619
|
// Which sibling helpers loaded (all CommonJS, shipped as .cjs — #3555).
|
|
607
620
|
const loadedHelpers = { router: !!router, session: !!session, memory: !!memory, intelligence: !!intelligence };
|
|
608
621
|
|
|
609
|
-
module.exports = { runWithTimeout, INTELLIGENCE_TIMEOUT_MS, loadedHelpers };
|
|
622
|
+
module.exports = { runWithTimeout, INTELLIGENCE_TIMEOUT_MS, loadedHelpers, ownedByMod, MOD_OWNABLE_EVENTS };
|
|
@@ -215,6 +215,16 @@
|
|
|
215
215
|
"name": "ruflo-music",
|
|
216
216
|
"source": "./plugins/ruflo-music",
|
|
217
217
|
"description": "AI music generation via Cognitum Music (cogmusic MCP) \u2014 compose lyrics/prompts, generate tracks, separate stems, extract MIDI, and master through your own music.cognitum.one account"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"name": "ruflo-mods",
|
|
221
|
+
"source": "./plugins/ruflo-mods",
|
|
222
|
+
"description": "Early access (ADR-404): ruflo as a Claude Code mod (function hooks) \u2014 in-process routing, edit learning signals, tighten-only tool checks from ruflo policy, and the cost budget ladder. Opt-in via `ruflo mods install`; classic hooks stay the fallback"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "ruflo-ruos",
|
|
226
|
+
"source": "./plugins/ruflo-ruos",
|
|
227
|
+
"description": "Run ruflo swarm agents on your own ruOS cloud desktops via the tenant-authenticated fleet MCP or per-tenant SSH; optional, removable"
|
|
218
228
|
}
|
|
219
229
|
]
|
|
220
230
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generation": 7,
|
|
4
|
-
"generatedAt": "2026-10-
|
|
5
|
-
"gitSha": "
|
|
4
|
+
"generatedAt": "2026-10-02T00:19:27.102Z",
|
|
5
|
+
"gitSha": "27982983",
|
|
6
6
|
"catalog": {
|
|
7
|
-
"agents":
|
|
7
|
+
"agents": 176,
|
|
8
8
|
"tools": 418,
|
|
9
9
|
"skills": 34
|
|
10
10
|
},
|
|
@@ -2306,6 +2306,22 @@ async function checkMetaharness() {
|
|
|
2306
2306
|
};
|
|
2307
2307
|
}
|
|
2308
2308
|
}
|
|
2309
|
+
// ADR-404 — ruflo as a Claude Code mod (function hooks, early access). One
|
|
2310
|
+
// line in a bare `doctor`: whether the mod is enabled, can load (function
|
|
2311
|
+
// hooks on, not refused by allowManagedModsOnly) and has started; never a
|
|
2312
|
+
// failure, since the classic hooks are the default and the fallback.
|
|
2313
|
+
// `ruflo mods doctor` prints every finding.
|
|
2314
|
+
async function checkMods() {
|
|
2315
|
+
const { probeMods } = await import('../mods/probe.js');
|
|
2316
|
+
const findings = probeMods({ projectRoot: process.cwd() });
|
|
2317
|
+
const enabled = findings.find((f) => f.name === 'ruflo-mods plugin')?.status === 'pass';
|
|
2318
|
+
if (!enabled)
|
|
2319
|
+
return { name: 'ruflo mods (ADR-404)', status: 'pass', message: 'not enabled; classic hooks handle every event' };
|
|
2320
|
+
const warnings = findings.filter((f) => f.status !== 'pass');
|
|
2321
|
+
return warnings.length === 0
|
|
2322
|
+
? { name: 'ruflo mods (ADR-404)', status: 'pass', message: findings.find((f) => f.name === 'last mod start')?.message ?? 'enabled' }
|
|
2323
|
+
: { name: 'ruflo mods (ADR-404)', status: 'warn', message: warnings.map((f) => `${f.name}: ${f.message}`).join('; '), fix: 'ruflo mods doctor' };
|
|
2324
|
+
}
|
|
2309
2325
|
// Opt-in @ruvector/typesafe task router (optional peer). `--component typesafe` only.
|
|
2310
2326
|
async function checkTypesafeRouter() {
|
|
2311
2327
|
const name = '@ruvector/typesafe router';
|
|
@@ -2519,7 +2535,7 @@ export const doctorCommand = {
|
|
|
2519
2535
|
{
|
|
2520
2536
|
name: 'component',
|
|
2521
2537
|
short: 'c',
|
|
2522
|
-
description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, mcp-overhead, claude, browser, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, typesafe, metaharness)',
|
|
2538
|
+
description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, mcp-overhead, claude, browser, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, typesafe, mods, metaharness)',
|
|
2523
2539
|
type: 'string'
|
|
2524
2540
|
},
|
|
2525
2541
|
{
|
|
@@ -2661,6 +2677,7 @@ export const doctorCommand = {
|
|
|
2661
2677
|
checkFunnel, // ADR-305 — effective funnel state + deciding precedence source
|
|
2662
2678
|
checkProxySponsoredConsent, // ADR-313 — Meta LLM Proxy sponsored-downtime health
|
|
2663
2679
|
checkAuth, // ADR-306 — Cognitum identity (warn-only; never fails bare `ruflo doctor`)
|
|
2680
|
+
checkMods, // ADR-404 — Claude Code mod path (warn-only)
|
|
2664
2681
|
];
|
|
2665
2682
|
// #2677: `--component memory` now runs the whole memory-health suite,
|
|
2666
2683
|
// not just the existence check. Values can be a single check or an
|
|
@@ -2714,6 +2731,7 @@ export const doctorCommand = {
|
|
|
2714
2731
|
'proxy': [checkProxySponsoredConsent, checkProxyBinary, checkProxyProcess, checkProxyBindAddress],
|
|
2715
2732
|
'auth': checkAuth, // ADR-306
|
|
2716
2733
|
'typesafe': checkTypesafeRouter, // opt-in @ruvector/typesafe task router
|
|
2734
|
+
'mods': checkMods, // ADR-404 — ruflo as a Claude Code mod
|
|
2717
2735
|
};
|
|
2718
2736
|
let checksToRun = allChecks;
|
|
2719
2737
|
if (component && componentMap[component]) {
|
|
@@ -88,6 +88,8 @@ const commandLoaders = {
|
|
|
88
88
|
advisor: () => import('./advisor.js'),
|
|
89
89
|
// Ruflo verbs in Claude Code's spinnerVerbs rotation (ADR-318)
|
|
90
90
|
spinner: () => import('./spinner.js'),
|
|
91
|
+
// ruflo as a Claude Code mod: function hooks, early access (ADR-404)
|
|
92
|
+
mods: () => import('./mods.js'),
|
|
91
93
|
// Ruflo entries in Claude Code's companyAnnouncements startup rotation (ADR-319)
|
|
92
94
|
announcements: () => import('./announcements.js'),
|
|
93
95
|
// AGNTCY/Outshift runtime transport selection (ADR-324 §2) — optional,
|
|
@@ -775,6 +775,19 @@ const initClaudeAction = async (ctx) => {
|
|
|
775
775
|
output.writeln(output.warning(' Embedding initialization skipped (run manually)'));
|
|
776
776
|
}
|
|
777
777
|
}
|
|
778
|
+
// ADR-404 — opt into the Claude Code mod path. Additive: the classic
|
|
779
|
+
// hooks just written stay the default and the fallback.
|
|
780
|
+
if (ctx.flags.mods === true) {
|
|
781
|
+
output.writeln();
|
|
782
|
+
try {
|
|
783
|
+
const { installMod } = await import('../mods/install.js');
|
|
784
|
+
const installed = installMod(ctx.cwd, 'local');
|
|
785
|
+
output.writeln(output.success(` ✓ ruflo-mods enabled in ${installed.settingsFile} (early access; run "ruflo mods doctor")`));
|
|
786
|
+
}
|
|
787
|
+
catch (err) {
|
|
788
|
+
output.writeln(output.warning(` ruflo-mods not enabled: ${err instanceof Error ? err.message : String(err)}`));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
778
791
|
if (!startDaemon && !startAll) {
|
|
779
792
|
const bin = (process.argv[1] || '').includes('ruflo') ? 'ruflo' : 'claude-flow';
|
|
780
793
|
output.writeln(output.bold('Next steps:'));
|
|
@@ -1500,6 +1513,13 @@ export const initCommand = {
|
|
|
1500
1513
|
type: 'boolean',
|
|
1501
1514
|
default: false,
|
|
1502
1515
|
},
|
|
1516
|
+
{
|
|
1517
|
+
// ADR-404 — Claude Code function hooks are early access; opt-in only.
|
|
1518
|
+
name: 'mods',
|
|
1519
|
+
description: 'Also enable the ruflo Claude Code mod (function hooks, early access); classic hooks stay as fallback',
|
|
1520
|
+
type: 'boolean',
|
|
1521
|
+
default: false,
|
|
1522
|
+
},
|
|
1503
1523
|
{
|
|
1504
1524
|
name: 'with-embeddings',
|
|
1505
1525
|
description: 'Initialize ONNX embedding subsystem with hyperbolic support',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo mods` — opt into running ruflo as a Claude Code mod (ADR-404,
|
|
3
|
+
* Claude Code function hooks, early access).
|
|
4
|
+
*
|
|
5
|
+
* install/uninstall edit one settings file and record what they added;
|
|
6
|
+
* status/doctor report what can be known from outside a session; sync-policy
|
|
7
|
+
* rewrites the policy projection the mod's tool check reads. Classic hooks
|
|
8
|
+
* are never removed: they stay the default and the fallback.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from '../types.js';
|
|
11
|
+
export declare const modsCommand: Command;
|
|
12
|
+
export default modsCommand;
|
|
13
|
+
//# sourceMappingURL=mods.d.ts.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo mods` — opt into running ruflo as a Claude Code mod (ADR-404,
|
|
3
|
+
* Claude Code function hooks, early access).
|
|
4
|
+
*
|
|
5
|
+
* install/uninstall edit one settings file and record what they added;
|
|
6
|
+
* status/doctor report what can be known from outside a session; sync-policy
|
|
7
|
+
* rewrites the policy projection the mod's tool check reads. Classic hooks
|
|
8
|
+
* are never removed: they stay the default and the fallback.
|
|
9
|
+
*/
|
|
10
|
+
import { output } from '../output.js';
|
|
11
|
+
import { installMod, uninstallMod } from '../mods/install.js';
|
|
12
|
+
import { probeMods } from '../mods/probe.js';
|
|
13
|
+
function projectRoot(ctx) {
|
|
14
|
+
return ctx.flags.projectRoot ?? ctx.flags['project-root'] ?? ctx.cwd ?? process.cwd();
|
|
15
|
+
}
|
|
16
|
+
function printFindings(findings) {
|
|
17
|
+
for (const f of findings) {
|
|
18
|
+
const mark = f.status === 'pass' ? output.success('✓') : f.status === 'warn' ? output.warning('!') : output.error('✗');
|
|
19
|
+
output.writeln(`${mark} ${f.name}: ${f.message}`);
|
|
20
|
+
if (f.fix && f.status !== 'pass')
|
|
21
|
+
output.writeln(output.dim(` fix: ${f.fix}`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const rootOption = { name: 'project-root', description: 'Project root (default: current directory)', type: 'string' };
|
|
25
|
+
const installSub = {
|
|
26
|
+
name: 'install',
|
|
27
|
+
description: 'Enable the ruflo-mods plugin for this project (opt-in; classic hooks stay as fallback)',
|
|
28
|
+
options: [
|
|
29
|
+
rootOption,
|
|
30
|
+
{ name: 'scope', description: 'local (.claude/settings.local.json, default) | project (.claude/settings.json)', type: 'string', default: 'local' },
|
|
31
|
+
{ name: 'dry-run', description: 'Show the settings that would be written', type: 'boolean', default: false },
|
|
32
|
+
],
|
|
33
|
+
action: async (ctx) => {
|
|
34
|
+
const scope = ctx.flags.scope ?? 'local';
|
|
35
|
+
if (scope !== 'local' && scope !== 'project') {
|
|
36
|
+
output.printError(`--scope must be local or project, got ${scope}`);
|
|
37
|
+
return { success: false, exitCode: 1 };
|
|
38
|
+
}
|
|
39
|
+
const root = projectRoot(ctx);
|
|
40
|
+
const result = installMod(root, scope, (ctx.flags.dryRun === true || ctx.flags['dry-run'] === true));
|
|
41
|
+
if (result.dryRun) {
|
|
42
|
+
output.writeln(`Would write ${result.settingsFile}:`);
|
|
43
|
+
output.printJson(result.next);
|
|
44
|
+
return { success: true, data: result };
|
|
45
|
+
}
|
|
46
|
+
await syncPolicy(root, true);
|
|
47
|
+
output.printSuccess(`ruflo-mods enabled in ${result.settingsFile}${result.backup ? ` (backup: ${result.backup})` : ''}`);
|
|
48
|
+
output.writeln('Restart Claude Code, then run /ruflo-mods in a session to see what the mod owns.');
|
|
49
|
+
output.writeln(output.dim('Early access: Claude Code loads it only where function hooks are on. Run `ruflo mods doctor`.'));
|
|
50
|
+
return { success: true, data: result };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const uninstallSub = {
|
|
54
|
+
name: 'uninstall',
|
|
55
|
+
description: 'Remove what `ruflo mods install` added (classic hooks take every event back)',
|
|
56
|
+
options: [rootOption, { name: 'dry-run', description: 'Show what would be removed', type: 'boolean', default: false }],
|
|
57
|
+
action: async (ctx) => {
|
|
58
|
+
const result = uninstallMod(projectRoot(ctx), (ctx.flags.dryRun === true || ctx.flags['dry-run'] === true));
|
|
59
|
+
if (!result.removed) {
|
|
60
|
+
output.printWarning('No install record (.claude-flow/mods/install.json): nothing ruflo added to remove.');
|
|
61
|
+
return { success: true, data: result };
|
|
62
|
+
}
|
|
63
|
+
output.printSuccess(`${result.dryRun ? 'Would remove' : 'Removed'} ruflo-mods from ${result.settingsFile}`);
|
|
64
|
+
return { success: true, data: result };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
function findingsCommand(name, description) {
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
description,
|
|
71
|
+
options: [rootOption, { name: 'json', description: 'Output as JSON', type: 'boolean', default: false }],
|
|
72
|
+
action: async (ctx) => {
|
|
73
|
+
const findings = probeMods({ projectRoot: projectRoot(ctx) });
|
|
74
|
+
if (ctx.flags.json)
|
|
75
|
+
output.printJson(findings);
|
|
76
|
+
else
|
|
77
|
+
printFindings(findings);
|
|
78
|
+
const failed = findings.some((f) => f.status === 'fail');
|
|
79
|
+
// status reports; doctor gates (warnings are expected while early access is off).
|
|
80
|
+
return { success: !failed, exitCode: name === 'doctor' && failed ? 1 : 0, data: findings };
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function syncPolicy(root, quiet) {
|
|
85
|
+
try {
|
|
86
|
+
const { loadPolicyState } = await import('../services/policy-runtime.js');
|
|
87
|
+
const { syncPolicyProjection } = await import('../mods/policy-projection.js');
|
|
88
|
+
const result = syncPolicyProjection(root, loadPolicyState(root));
|
|
89
|
+
if (!quiet || result.action !== 'unchanged')
|
|
90
|
+
output.writeln(`policy projection: ${result.action} (${result.path})`);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
output.printWarning(`policy projection not synced: ${error.message}`);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const syncPolicySub = {
|
|
99
|
+
name: 'sync-policy',
|
|
100
|
+
description: 'Rewrite the Claude Code policy projection from .claude-flow/policy/state.json',
|
|
101
|
+
options: [rootOption],
|
|
102
|
+
action: async (ctx) => {
|
|
103
|
+
const ok = await syncPolicy(projectRoot(ctx), false);
|
|
104
|
+
return { success: ok, exitCode: ok ? 0 : 1 };
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const statusSub = findingsCommand('status', 'Show whether the mod is enabled, can load, and what it owns');
|
|
108
|
+
export const modsCommand = {
|
|
109
|
+
name: 'mods',
|
|
110
|
+
description: 'Run ruflo as a Claude Code mod (function hooks, early access, ADR-404)',
|
|
111
|
+
subcommands: [
|
|
112
|
+
installSub,
|
|
113
|
+
uninstallSub,
|
|
114
|
+
statusSub,
|
|
115
|
+
findingsCommand('doctor', 'Check the mod path; exits 1 only on a failure'),
|
|
116
|
+
syncPolicySub,
|
|
117
|
+
],
|
|
118
|
+
examples: [
|
|
119
|
+
{ command: 'ruflo mods install', description: 'Enable for this project (settings.local.json)' },
|
|
120
|
+
{ command: 'ruflo mods doctor', description: 'Function hooks on? Refused by policy? Handshake supported?' },
|
|
121
|
+
{ command: 'ruflo mods uninstall', description: 'Remove only what install added' },
|
|
122
|
+
],
|
|
123
|
+
action: statusSub.action,
|
|
124
|
+
};
|
|
125
|
+
export default modsCommand;
|
|
126
|
+
//# sourceMappingURL=mods.js.map
|
|
@@ -530,7 +530,19 @@ export function generateHookHandler() {
|
|
|
530
530
|
' });',
|
|
531
531
|
'}',
|
|
532
532
|
'',
|
|
533
|
+
// ADR-404: same handshake as the shipped hook-handler.cjs, so a helper
|
|
534
|
+
// regenerated by the refresh fallback still hands route/post-edit to the mod.
|
|
535
|
+
'// ADR-404: the ruflo mod names the events it runs in-process in',
|
|
536
|
+
'// RUFLO_MODS_OWNS; only side-effect events hand over, guards always run.',
|
|
537
|
+
"const MOD_OWNABLE_EVENTS = new Set(['route', 'post-edit']);",
|
|
538
|
+
'function ownedByMod(cmd, env) {',
|
|
539
|
+
' env = env || process.env;',
|
|
540
|
+
' if (!MOD_OWNABLE_EVENTS.has(cmd)) return false;',
|
|
541
|
+
" return String(env.RUFLO_MODS_OWNS || '').split(',').some(function (owned) { return owned.trim() === cmd; });",
|
|
542
|
+
'}',
|
|
543
|
+
'',
|
|
533
544
|
'async function main() {',
|
|
545
|
+
' if (ownedByMod(command)) return;',
|
|
534
546
|
' let stdinData = "";',
|
|
535
547
|
' try { stdinData = await readStdin(); } catch (e) { /* ignore */ }',
|
|
536
548
|
' let hookInput = {};',
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Claude Code binaries a shell would find, and whether the one it runs
|
|
3
|
+
* can load mods (ADR-404). Mods are on by default from Claude Code 2.1.287;
|
|
4
|
+
* from 2.1.277 they load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1; older
|
|
5
|
+
* builds predate them. A server-side rollout switch can still hold them off
|
|
6
|
+
* on any version (reported separately). A stale install earlier on PATH
|
|
7
|
+
* (a 2.1.107 npm-global beside a current native one, say) silently decides.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MODS_DEFAULT_ON = "2.1.287";
|
|
10
|
+
export declare const MODS_FIRST = "2.1.277";
|
|
11
|
+
export interface ClaudeInstall {
|
|
12
|
+
path: string;
|
|
13
|
+
version: string | null;
|
|
14
|
+
}
|
|
15
|
+
export type VersionOf = (path: string) => string | null;
|
|
16
|
+
/** a < b for dotted numeric versions. */
|
|
17
|
+
export declare function versionLess(a: string, b: string): boolean;
|
|
18
|
+
/** `claude --version` of one binary, bounded; null when it cannot say. */
|
|
19
|
+
export declare const defaultVersionOf: VersionOf;
|
|
20
|
+
/** Every `claude` on PATH, in PATH order (the first is what runs), plus ~/.local/bin. */
|
|
21
|
+
export declare function findClaudeInstalls(env: NodeJS.ProcessEnv, home: string, versionOf?: VersionOf): ClaudeInstall[];
|
|
22
|
+
export interface InstallsFinding {
|
|
23
|
+
status: 'pass' | 'warn';
|
|
24
|
+
message: string;
|
|
25
|
+
fix?: string;
|
|
26
|
+
}
|
|
27
|
+
/** The doctor's reading of what `claude` resolves to and what else is installed. */
|
|
28
|
+
export declare function judgeInstalls(installs: readonly ClaudeInstall[], enableEnvSet: boolean): InstallsFinding;
|
|
29
|
+
//# sourceMappingURL=claude-installs.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Claude Code binaries a shell would find, and whether the one it runs
|
|
3
|
+
* can load mods (ADR-404). Mods are on by default from Claude Code 2.1.287;
|
|
4
|
+
* from 2.1.277 they load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1; older
|
|
5
|
+
* builds predate them. A server-side rollout switch can still hold them off
|
|
6
|
+
* on any version (reported separately). A stale install earlier on PATH
|
|
7
|
+
* (a 2.1.107 npm-global beside a current native one, say) silently decides.
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
11
|
+
import { delimiter, join } from 'node:path';
|
|
12
|
+
export const MODS_DEFAULT_ON = '2.1.287';
|
|
13
|
+
export const MODS_FIRST = '2.1.277';
|
|
14
|
+
const parse = (v) => v.split('.').map((n) => Number(n));
|
|
15
|
+
/** a < b for dotted numeric versions. */
|
|
16
|
+
export function versionLess(a, b) {
|
|
17
|
+
const x = parse(a);
|
|
18
|
+
const y = parse(b);
|
|
19
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
20
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0))
|
|
21
|
+
return (x[i] ?? 0) < (y[i] ?? 0);
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/** `claude --version` of one binary, bounded; null when it cannot say. */
|
|
26
|
+
export const defaultVersionOf = (path) => {
|
|
27
|
+
try {
|
|
28
|
+
const out = execFileSync(path, ['--version'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
29
|
+
return out.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/** Every `claude` on PATH, in PATH order (the first is what runs), plus ~/.local/bin. */
|
|
36
|
+
export function findClaudeInstalls(env, home, versionOf = defaultVersionOf) {
|
|
37
|
+
const names = process.platform === 'win32' ? ['claude.exe', 'claude.cmd', 'claude'] : ['claude'];
|
|
38
|
+
const dirs = [...(env.PATH ?? '').split(delimiter).filter(Boolean), join(home, '.local', 'bin')];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const installs = [];
|
|
41
|
+
for (const dir of dirs) {
|
|
42
|
+
for (const name of names) {
|
|
43
|
+
const path = join(dir, name);
|
|
44
|
+
let real;
|
|
45
|
+
try {
|
|
46
|
+
if (!existsSync(path) || !statSync(path).isFile())
|
|
47
|
+
continue;
|
|
48
|
+
real = realpathSync(path);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (seen.has(real))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(real);
|
|
56
|
+
installs.push({ path, version: versionOf(path) });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return installs;
|
|
60
|
+
}
|
|
61
|
+
/** The doctor's reading of what `claude` resolves to and what else is installed. */
|
|
62
|
+
export function judgeInstalls(installs, enableEnvSet) {
|
|
63
|
+
if (installs.length === 0)
|
|
64
|
+
return { status: 'warn', message: 'no claude binary on PATH', fix: 'install Claude Code >= 2.1.287' };
|
|
65
|
+
const [first] = installs;
|
|
66
|
+
const others = installs.slice(1).filter((i) => i.version !== first.version);
|
|
67
|
+
const mixed = others.length
|
|
68
|
+
? `; also installed: ${others.map((i) => `${i.path} (${i.version ?? 'unknown'})`).join(', ')}. The first on PATH runs; a stale one can shadow a current one.`
|
|
69
|
+
: '';
|
|
70
|
+
const runs = `${first.path} (${first.version ?? 'version unknown'})`;
|
|
71
|
+
if (!first.version)
|
|
72
|
+
return { status: 'warn', message: `claude resolves to ${runs}${mixed}` };
|
|
73
|
+
if (!versionLess(first.version, MODS_DEFAULT_ON)) {
|
|
74
|
+
return { status: others.length ? 'warn' : 'pass', message: `claude resolves to ${runs}: mods on by default${mixed}` };
|
|
75
|
+
}
|
|
76
|
+
if (!versionLess(first.version, MODS_FIRST)) {
|
|
77
|
+
return enableEnvSet
|
|
78
|
+
? { status: others.length ? 'warn' : 'pass', message: `claude resolves to ${runs}: mods load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (set)${mixed}` }
|
|
79
|
+
: { status: 'warn', message: `claude resolves to ${runs}: below ${MODS_DEFAULT_ON}, mods need CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (not set)${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON}, or export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` };
|
|
80
|
+
}
|
|
81
|
+
return { status: 'warn', message: `claude resolves to ${runs}: predates mods (< ${MODS_FIRST}); classic hooks handle every event${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON} and remove the stale install from PATH` };
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=claude-installs.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo mods install|uninstall` (ADR-404): enable the ruflo-mods plugin for
|
|
3
|
+
* one project, opt-in, and take back exactly what was added.
|
|
4
|
+
*
|
|
5
|
+
* Writes three keys of a Claude Code settings file (`.claude/settings.local.json`
|
|
6
|
+
* by default, so the early-access feature is one person's choice, not the
|
|
7
|
+
* repository's): `enabledPlugins["ruflo-mods@ruflo"]`, the `ruflo` entry of
|
|
8
|
+
* `extraKnownMarketplaces` when absent, and `env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS`.
|
|
9
|
+
* What was added is recorded in `.claude-flow/mods/install.json`, so uninstall
|
|
10
|
+
* removes those and nothing a person set themselves. Classic hooks are never
|
|
11
|
+
* touched: they stay the fallback (the mod takes an event over at runtime only).
|
|
12
|
+
*/
|
|
13
|
+
export declare const MOD_PLUGIN_ID = "ruflo-mods@ruflo";
|
|
14
|
+
export declare const MARKETPLACE_NAME = "ruflo";
|
|
15
|
+
export declare const MARKETPLACE_SOURCE: {
|
|
16
|
+
readonly source: {
|
|
17
|
+
readonly source: "github";
|
|
18
|
+
readonly repo: "ruvnet/ruflo";
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare const ENABLE_ENV = "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS";
|
|
22
|
+
export declare const INSTALL_RECORD: string;
|
|
23
|
+
export type Scope = 'local' | 'project';
|
|
24
|
+
export interface InstallRecord {
|
|
25
|
+
version: 1;
|
|
26
|
+
settingsFile: string;
|
|
27
|
+
installedAt: string;
|
|
28
|
+
added: {
|
|
29
|
+
plugin: boolean;
|
|
30
|
+
marketplace: boolean;
|
|
31
|
+
env: boolean;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
type Settings = Record<string, unknown> & {
|
|
35
|
+
enabledPlugins?: Record<string, unknown>;
|
|
36
|
+
extraKnownMarketplaces?: Record<string, unknown>;
|
|
37
|
+
env?: Record<string, unknown>;
|
|
38
|
+
};
|
|
39
|
+
export declare function settingsFileFor(projectRoot: string, scope: Scope): string;
|
|
40
|
+
/** Reads a settings file; absent is `{}`, anything unparseable throws (never overwritten). */
|
|
41
|
+
export declare function readSettingsFile(path: string): Settings;
|
|
42
|
+
/** The settings after install, and what install added (pure). */
|
|
43
|
+
export declare function withModEnabled(settings: Settings): {
|
|
44
|
+
next: Settings;
|
|
45
|
+
added: InstallRecord['added'];
|
|
46
|
+
};
|
|
47
|
+
/** The settings after uninstall: only what the record says install added (pure). */
|
|
48
|
+
export declare function withModRemoved(settings: Settings, added: InstallRecord['added']): Settings;
|
|
49
|
+
export interface InstallResult {
|
|
50
|
+
settingsFile: string;
|
|
51
|
+
backup?: string;
|
|
52
|
+
added: InstallRecord['added'];
|
|
53
|
+
dryRun: boolean;
|
|
54
|
+
next: Settings;
|
|
55
|
+
}
|
|
56
|
+
export declare function installMod(projectRoot: string, scope: Scope, dryRun?: boolean): InstallResult;
|
|
57
|
+
export declare function readRecord(projectRoot: string): InstallRecord | null;
|
|
58
|
+
export declare function uninstallMod(projectRoot: string, dryRun?: boolean): {
|
|
59
|
+
settingsFile?: string;
|
|
60
|
+
removed: boolean;
|
|
61
|
+
dryRun: boolean;
|
|
62
|
+
};
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo mods install|uninstall` (ADR-404): enable the ruflo-mods plugin for
|
|
3
|
+
* one project, opt-in, and take back exactly what was added.
|
|
4
|
+
*
|
|
5
|
+
* Writes three keys of a Claude Code settings file (`.claude/settings.local.json`
|
|
6
|
+
* by default, so the early-access feature is one person's choice, not the
|
|
7
|
+
* repository's): `enabledPlugins["ruflo-mods@ruflo"]`, the `ruflo` entry of
|
|
8
|
+
* `extraKnownMarketplaces` when absent, and `env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS`.
|
|
9
|
+
* What was added is recorded in `.claude-flow/mods/install.json`, so uninstall
|
|
10
|
+
* removes those and nothing a person set themselves. Classic hooks are never
|
|
11
|
+
* touched: they stay the fallback (the mod takes an event over at runtime only).
|
|
12
|
+
*/
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
15
|
+
export const MOD_PLUGIN_ID = 'ruflo-mods@ruflo';
|
|
16
|
+
export const MARKETPLACE_NAME = 'ruflo';
|
|
17
|
+
export const MARKETPLACE_SOURCE = { source: { source: 'github', repo: 'ruvnet/ruflo' } };
|
|
18
|
+
export const ENABLE_ENV = 'CLAUDE_CODE_ENABLE_FUNCTION_HOOKS';
|
|
19
|
+
export const INSTALL_RECORD = join('.claude-flow', 'mods', 'install.json');
|
|
20
|
+
export function settingsFileFor(projectRoot, scope) {
|
|
21
|
+
return join(resolve(projectRoot), '.claude', scope === 'local' ? 'settings.local.json' : 'settings.json');
|
|
22
|
+
}
|
|
23
|
+
function isRecord(v) {
|
|
24
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
25
|
+
}
|
|
26
|
+
/** Reads a settings file; absent is `{}`, anything unparseable throws (never overwritten). */
|
|
27
|
+
export function readSettingsFile(path) {
|
|
28
|
+
if (!existsSync(path))
|
|
29
|
+
return {};
|
|
30
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
31
|
+
if (!isRecord(parsed))
|
|
32
|
+
throw new Error(`${path} is not a JSON object`);
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
function writeJson(path, value) {
|
|
36
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
37
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
38
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
39
|
+
renameSync(tmp, path);
|
|
40
|
+
}
|
|
41
|
+
/** The settings after install, and what install added (pure). */
|
|
42
|
+
export function withModEnabled(settings) {
|
|
43
|
+
const next = { ...settings };
|
|
44
|
+
const plugins = isRecord(settings.enabledPlugins) ? { ...settings.enabledPlugins } : {};
|
|
45
|
+
const markets = isRecord(settings.extraKnownMarketplaces) ? { ...settings.extraKnownMarketplaces } : {};
|
|
46
|
+
const env = isRecord(settings.env) ? { ...settings.env } : {};
|
|
47
|
+
const added = {
|
|
48
|
+
plugin: plugins[MOD_PLUGIN_ID] !== true,
|
|
49
|
+
marketplace: !(MARKETPLACE_NAME in markets),
|
|
50
|
+
env: env[ENABLE_ENV] !== '1',
|
|
51
|
+
};
|
|
52
|
+
plugins[MOD_PLUGIN_ID] = true;
|
|
53
|
+
if (added.marketplace)
|
|
54
|
+
markets[MARKETPLACE_NAME] = MARKETPLACE_SOURCE;
|
|
55
|
+
env[ENABLE_ENV] = '1';
|
|
56
|
+
next.enabledPlugins = plugins;
|
|
57
|
+
next.extraKnownMarketplaces = markets;
|
|
58
|
+
next.env = env;
|
|
59
|
+
return { next, added };
|
|
60
|
+
}
|
|
61
|
+
/** The settings after uninstall: only what the record says install added (pure). */
|
|
62
|
+
export function withModRemoved(settings, added) {
|
|
63
|
+
const next = { ...settings };
|
|
64
|
+
const drop = (key, name) => {
|
|
65
|
+
const section = next[key];
|
|
66
|
+
if (!isRecord(section))
|
|
67
|
+
return;
|
|
68
|
+
const copy = { ...section };
|
|
69
|
+
delete copy[name];
|
|
70
|
+
if (Object.keys(copy).length === 0)
|
|
71
|
+
delete next[key];
|
|
72
|
+
else
|
|
73
|
+
next[key] = copy;
|
|
74
|
+
};
|
|
75
|
+
if (added.plugin)
|
|
76
|
+
drop('enabledPlugins', MOD_PLUGIN_ID);
|
|
77
|
+
if (added.marketplace)
|
|
78
|
+
drop('extraKnownMarketplaces', MARKETPLACE_NAME);
|
|
79
|
+
if (added.env)
|
|
80
|
+
drop('env', ENABLE_ENV);
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
export function installMod(projectRoot, scope, dryRun = false) {
|
|
84
|
+
const settingsFile = settingsFileFor(projectRoot, scope);
|
|
85
|
+
const current = readSettingsFile(settingsFile);
|
|
86
|
+
const { next, added } = withModEnabled(current);
|
|
87
|
+
if (dryRun)
|
|
88
|
+
return { settingsFile, added, dryRun, next };
|
|
89
|
+
let backup;
|
|
90
|
+
if (existsSync(settingsFile)) {
|
|
91
|
+
backup = `${settingsFile}.bak-ruflo-mods-${Date.now()}`;
|
|
92
|
+
copyFileSync(settingsFile, backup);
|
|
93
|
+
}
|
|
94
|
+
writeJson(settingsFile, next);
|
|
95
|
+
const recordPath = join(resolve(projectRoot), INSTALL_RECORD);
|
|
96
|
+
const previous = readRecord(projectRoot);
|
|
97
|
+
// A second install keeps the first record's claims: what ruflo added once
|
|
98
|
+
// is still ruflo's to remove.
|
|
99
|
+
const merged = previous && previous.settingsFile === settingsFile
|
|
100
|
+
? { plugin: previous.added.plugin || added.plugin, marketplace: previous.added.marketplace || added.marketplace, env: previous.added.env || added.env }
|
|
101
|
+
: added;
|
|
102
|
+
const record = { version: 1, settingsFile, installedAt: new Date().toISOString(), added: merged };
|
|
103
|
+
writeJson(recordPath, record);
|
|
104
|
+
return { settingsFile, backup, added: merged, dryRun, next };
|
|
105
|
+
}
|
|
106
|
+
export function readRecord(projectRoot) {
|
|
107
|
+
const path = join(resolve(projectRoot), INSTALL_RECORD);
|
|
108
|
+
if (!existsSync(path))
|
|
109
|
+
return null;
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
112
|
+
return parsed && parsed.version === 1 && typeof parsed.settingsFile === 'string' && isRecord(parsed.added) ? parsed : null;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function uninstallMod(projectRoot, dryRun = false) {
|
|
119
|
+
const record = readRecord(projectRoot);
|
|
120
|
+
if (!record)
|
|
121
|
+
return { removed: false, dryRun };
|
|
122
|
+
// The record names the file; never follow it outside this project.
|
|
123
|
+
const root = resolve(projectRoot);
|
|
124
|
+
if (!resolve(record.settingsFile).startsWith(join(root, '.claude') + sep)) {
|
|
125
|
+
throw new Error(`install record names a settings file outside ${root}/.claude: ${record.settingsFile}`);
|
|
126
|
+
}
|
|
127
|
+
if (dryRun)
|
|
128
|
+
return { settingsFile: record.settingsFile, removed: true, dryRun };
|
|
129
|
+
if (existsSync(record.settingsFile)) {
|
|
130
|
+
writeJson(record.settingsFile, withModRemoved(readSettingsFile(record.settingsFile), record.added));
|
|
131
|
+
}
|
|
132
|
+
unlinkSync(join(root, INSTALL_RECORD));
|
|
133
|
+
return { settingsFile: record.settingsFile, removed: true, dryRun };
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The policy projection the ruflo mod reads (ADR-404).
|
|
3
|
+
*
|
|
4
|
+
* A hooks module may read at most 4 MiB and cannot import @claude-flow/security,
|
|
5
|
+
* while `.claude-flow/policy/state.json` carries the whole receipt ledger
|
|
6
|
+
* (tens of MB on a busy project). So every policy state write also writes a
|
|
7
|
+
* small file holding only what the mod's `tool.check` needs: the mode and the
|
|
8
|
+
* rules that name a `claude-code.` action explicitly. Legacy mode, or no such
|
|
9
|
+
* rule, removes the file, and the mod then adds no policy of its own.
|
|
10
|
+
*
|
|
11
|
+
* The projection never authorizes anything: the mod only tightens Claude
|
|
12
|
+
* Code's own verdict with it, so a deleted or stale projection returns to the
|
|
13
|
+
* no-mod baseline and can never loosen a call. It is not a ledger and holds
|
|
14
|
+
* no receipts; issue #3602 (the ledger anchor) is untouched by it.
|
|
15
|
+
*/
|
|
16
|
+
import type { PolicyRule, PolicyState } from '@claude-flow/security';
|
|
17
|
+
export declare const PROJECTION_RELATIVE: string;
|
|
18
|
+
export declare const CLAUDE_CODE_ACTION_PREFIX = "claude-code.";
|
|
19
|
+
export interface PolicyProjection {
|
|
20
|
+
version: 1;
|
|
21
|
+
mode: PolicyState['mode'];
|
|
22
|
+
generatedAt: number;
|
|
23
|
+
rules: PolicyRule[];
|
|
24
|
+
}
|
|
25
|
+
/** Whether a rule names Claude Code tool calls explicitly (never via `*` or no actions). */
|
|
26
|
+
export declare function targetsClaudeCode(rule: Pick<PolicyRule, 'actions'>): boolean;
|
|
27
|
+
/** The projection of a state, or null when there is nothing to project. */
|
|
28
|
+
export declare function projectionOf(state: Pick<PolicyState, 'mode' | 'rules'>, now?: number): PolicyProjection | null;
|
|
29
|
+
export type ProjectionSync = {
|
|
30
|
+
action: 'written' | 'removed' | 'unchanged';
|
|
31
|
+
path: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Writes (atomically, owner-only) or removes the projection for a state.
|
|
35
|
+
* Throws on I/O failure; policy-runtime calls it after the state is safely
|
|
36
|
+
* written and keeps the state write's own result whatever happens here.
|
|
37
|
+
*/
|
|
38
|
+
export declare function syncPolicyProjection(projectRoot: string, state: Pick<PolicyState, 'mode' | 'rules'>): ProjectionSync;
|
|
39
|
+
//# sourceMappingURL=policy-projection.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The policy projection the ruflo mod reads (ADR-404).
|
|
3
|
+
*
|
|
4
|
+
* A hooks module may read at most 4 MiB and cannot import @claude-flow/security,
|
|
5
|
+
* while `.claude-flow/policy/state.json` carries the whole receipt ledger
|
|
6
|
+
* (tens of MB on a busy project). So every policy state write also writes a
|
|
7
|
+
* small file holding only what the mod's `tool.check` needs: the mode and the
|
|
8
|
+
* rules that name a `claude-code.` action explicitly. Legacy mode, or no such
|
|
9
|
+
* rule, removes the file, and the mod then adds no policy of its own.
|
|
10
|
+
*
|
|
11
|
+
* The projection never authorizes anything: the mod only tightens Claude
|
|
12
|
+
* Code's own verdict with it, so a deleted or stale projection returns to the
|
|
13
|
+
* no-mod baseline and can never loosen a call. It is not a ledger and holds
|
|
14
|
+
* no receipts; issue #3602 (the ledger anchor) is untouched by it.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join, resolve } from 'node:path';
|
|
18
|
+
export const PROJECTION_RELATIVE = join('.claude-flow', 'policy', 'claude-code.json');
|
|
19
|
+
export const CLAUDE_CODE_ACTION_PREFIX = 'claude-code.';
|
|
20
|
+
/** Whether a rule names Claude Code tool calls explicitly (never via `*` or no actions). */
|
|
21
|
+
export function targetsClaudeCode(rule) {
|
|
22
|
+
return Array.isArray(rule.actions) && rule.actions.some((a) => typeof a === 'string' && a.startsWith(CLAUDE_CODE_ACTION_PREFIX));
|
|
23
|
+
}
|
|
24
|
+
/** The projection of a state, or null when there is nothing to project. */
|
|
25
|
+
export function projectionOf(state, now = Date.now()) {
|
|
26
|
+
if (state.mode === 'legacy')
|
|
27
|
+
return null;
|
|
28
|
+
const rules = (Array.isArray(state.rules) ? state.rules : []).filter(targetsClaudeCode).map((rule) => ({
|
|
29
|
+
id: rule.id,
|
|
30
|
+
effect: rule.effect,
|
|
31
|
+
actions: [...rule.actions],
|
|
32
|
+
...(rule.enabled !== undefined ? { enabled: rule.enabled } : {}),
|
|
33
|
+
...(rule.priority !== undefined ? { priority: rule.priority } : {}),
|
|
34
|
+
...(rule.resources ? { resources: [...rule.resources] } : {}),
|
|
35
|
+
...(rule.principals ? { principals: [...rule.principals] } : {}),
|
|
36
|
+
...(rule.identityTypes ? { identityTypes: [...rule.identityTypes] } : {}),
|
|
37
|
+
...(rule.roles ? { roles: [...rule.roles] } : {}),
|
|
38
|
+
...(rule.environments ? { environments: [...rule.environments] } : {}),
|
|
39
|
+
...(rule.constraints ? { constraints: { ...rule.constraints } } : {}),
|
|
40
|
+
}));
|
|
41
|
+
if (rules.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
return { version: 1, mode: state.mode, generatedAt: now, rules };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Writes (atomically, owner-only) or removes the projection for a state.
|
|
47
|
+
* Throws on I/O failure; policy-runtime calls it after the state is safely
|
|
48
|
+
* written and keeps the state write's own result whatever happens here.
|
|
49
|
+
*/
|
|
50
|
+
export function syncPolicyProjection(projectRoot, state) {
|
|
51
|
+
const path = join(resolve(projectRoot), PROJECTION_RELATIVE);
|
|
52
|
+
const projection = projectionOf(state);
|
|
53
|
+
if (!projection) {
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
return { action: 'unchanged', path };
|
|
56
|
+
unlinkSync(path);
|
|
57
|
+
return { action: 'removed', path };
|
|
58
|
+
}
|
|
59
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
60
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
61
|
+
writeFileSync(tmp, `${JSON.stringify(projection, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
62
|
+
renameSync(tmp, path);
|
|
63
|
+
return { action: 'written', path };
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=policy-projection.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What can be known, from outside a Claude Code session, about whether the
|
|
3
|
+
* ruflo mod will load and what it will own (ADR-404). Every finding names its
|
|
4
|
+
* source; nothing here claims a live load it did not see. The one direct
|
|
5
|
+
* evidence of a load is the heartbeat the mod writes at session start.
|
|
6
|
+
*/
|
|
7
|
+
import { type ClaudeInstall } from './claude-installs.js';
|
|
8
|
+
export declare const HANDSHAKE_MARKER = "RUFLO_MODS_OWNS";
|
|
9
|
+
export declare const HEARTBEAT_RELATIVE: string;
|
|
10
|
+
export type Status = 'pass' | 'warn' | 'fail';
|
|
11
|
+
export interface Finding {
|
|
12
|
+
name: string;
|
|
13
|
+
status: Status;
|
|
14
|
+
message: string;
|
|
15
|
+
fix?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Managed settings, where an organization sets sec-default's options. */
|
|
18
|
+
export declare function managedSettingsPath(os?: NodeJS.Platform): string;
|
|
19
|
+
export interface ProbeInputs {
|
|
20
|
+
projectRoot: string;
|
|
21
|
+
home?: string;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
managedPath?: string;
|
|
24
|
+
/** Claude Code binaries on PATH; discovered (each `--version` run) when absent. */
|
|
25
|
+
installs?: ClaudeInstall[];
|
|
26
|
+
}
|
|
27
|
+
export declare function probeMods(inputs: ProbeInputs): Finding[];
|
|
28
|
+
//# sourceMappingURL=probe.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What can be known, from outside a Claude Code session, about whether the
|
|
3
|
+
* ruflo mod will load and what it will own (ADR-404). Every finding names its
|
|
4
|
+
* source; nothing here claims a live load it did not see. The one direct
|
|
5
|
+
* evidence of a load is the heartbeat the mod writes at session start.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { homedir, platform } from 'node:os';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
import { findClaudeInstalls, judgeInstalls } from './claude-installs.js';
|
|
11
|
+
import { ENABLE_ENV, MOD_PLUGIN_ID, readRecord, readSettingsFile, settingsFileFor } from './install.js';
|
|
12
|
+
import { PROJECTION_RELATIVE } from './policy-projection.js';
|
|
13
|
+
export const HANDSHAKE_MARKER = 'RUFLO_MODS_OWNS';
|
|
14
|
+
export const HEARTBEAT_RELATIVE = join('.claude-flow', 'mods', 'session.json');
|
|
15
|
+
const SEC_DEFAULT_ID = 'cc-plugin-sec-default@builtin';
|
|
16
|
+
function readJson(path) {
|
|
17
|
+
try {
|
|
18
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function get(obj, ...keys) {
|
|
25
|
+
return keys.reduce((o, k) => (o !== null && typeof o === 'object' ? o[k] : undefined), obj);
|
|
26
|
+
}
|
|
27
|
+
/** Managed settings, where an organization sets sec-default's options. */
|
|
28
|
+
export function managedSettingsPath(os = platform()) {
|
|
29
|
+
if (os === 'darwin')
|
|
30
|
+
return '/Library/Application Support/ClaudeCode/managed-settings.json';
|
|
31
|
+
if (os === 'win32')
|
|
32
|
+
return 'C:\\ProgramData\\ClaudeCode\\managed-settings.json';
|
|
33
|
+
return '/etc/claude-code/managed-settings.json';
|
|
34
|
+
}
|
|
35
|
+
export function probeMods(inputs) {
|
|
36
|
+
const root = resolve(inputs.projectRoot);
|
|
37
|
+
const home = inputs.home ?? homedir();
|
|
38
|
+
const env = inputs.env ?? process.env;
|
|
39
|
+
const findings = [];
|
|
40
|
+
// 1. Enabled for this project?
|
|
41
|
+
const files = [settingsFileFor(root, 'local'), settingsFileFor(root, 'project'), join(home, '.claude', 'settings.json')];
|
|
42
|
+
const enabledIn = files.find((f) => {
|
|
43
|
+
try {
|
|
44
|
+
return get(readSettingsFile(f), 'enabledPlugins', MOD_PLUGIN_ID) === true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
findings.push(enabledIn
|
|
51
|
+
? { name: 'ruflo-mods plugin', status: 'pass', message: `enabled in ${enabledIn}` }
|
|
52
|
+
: { name: 'ruflo-mods plugin', status: 'warn', message: 'not enabled; classic hooks handle every event', fix: 'ruflo mods install' });
|
|
53
|
+
// 2. Function hooks switched on for installed plugins (early access).
|
|
54
|
+
const envOn = env[ENABLE_ENV] === '1';
|
|
55
|
+
const settingsOn = files.some((f) => {
|
|
56
|
+
try {
|
|
57
|
+
return get(readSettingsFile(f), 'env', ENABLE_ENV) === '1';
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// 2a. Which Claude Code runs, and whether a stale install shadows it.
|
|
64
|
+
findings.push({ name: 'claude installs', ...judgeInstalls(inputs.installs ?? findClaudeInstalls(env, home), envOn || settingsOn) });
|
|
65
|
+
// Observed live (ADR-404): 2.1.287 loads mods with ENABLE_ENV unset;
|
|
66
|
+
// 2.1.282 refuses them without it ("not turned on for installed plugins");
|
|
67
|
+
// on either, a server-side rollout switch served off holds them off, the
|
|
68
|
+
// variable notwithstanding. Which binary runs is the 'claude installs' finding.
|
|
69
|
+
const rollout = get(readJson(join(home, '.claude.json')), 'cachedGrowthBookFeatures', 'tengu_plugin_hooks_modules');
|
|
70
|
+
const enableMsg = `${ENABLE_ENV} ${envOn ? 'set in this environment' : settingsOn ? 'set in settings env' : 'not set'}`;
|
|
71
|
+
findings.push(rollout === true
|
|
72
|
+
? { name: 'function hooks', status: 'pass', message: `Claude Code rollout switch cached on; ${enableMsg}` }
|
|
73
|
+
: rollout === false
|
|
74
|
+
? { name: 'function hooks', status: 'warn', message: `Claude Code serves function hooks OFF for this account (cached); the mod will not load and classic hooks keep every event; ${enableMsg}` }
|
|
75
|
+
: { name: 'function hooks', status: 'warn', message: `rollout switch not cached (unverified whether the mod can load); ${enableMsg}`, fix: `export ${ENABLE_ENV}=1 and start Claude Code once` });
|
|
76
|
+
// 3. Refused by policy?
|
|
77
|
+
const managed = readJson(inputs.managedPath ?? managedSettingsPath());
|
|
78
|
+
const managedOnly = get(managed, 'pluginConfigs', SEC_DEFAULT_ID, 'options', 'allowManagedModsOnly');
|
|
79
|
+
findings.push(managedOnly !== undefined && managedOnly !== false
|
|
80
|
+
? { name: 'managed policy', status: 'warn', message: 'allowManagedModsOnly is set: Claude Code refuses user mods; classic hooks stay in charge' }
|
|
81
|
+
: { name: 'managed policy', status: 'pass', message: managed === undefined ? 'no managed settings' : 'user mods allowed (allowManagedModsOnly not set)' });
|
|
82
|
+
// 4. Every classic helper a hook could run honours the handshake (the mod's
|
|
83
|
+
// own rule, plugins/ruflo-mods/hooks/session.ts); else the mod stands down.
|
|
84
|
+
const helpers = [...new Set([join(root, '.claude', 'helpers', 'hook-handler.cjs'), join(home, '.claude', 'helpers', 'hook-handler.cjs')])].filter(existsSync);
|
|
85
|
+
const stale = helpers.filter((h) => {
|
|
86
|
+
try {
|
|
87
|
+
return !readFileSync(h, 'utf8').includes(HANDSHAKE_MARKER);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
findings.push(helpers.length === 0
|
|
94
|
+
? { name: 'classic handshake', status: 'pass', message: 'no hook-handler.cjs: the mod owns route and post-edit outright' }
|
|
95
|
+
: stale.length === 0
|
|
96
|
+
? { name: 'classic handshake', status: 'pass', message: `${helpers.join(', ')} hand route and post-edit to the mod while it runs` }
|
|
97
|
+
: { name: 'classic handshake', status: 'warn', message: `${stale.join(', ')} predate${stale.length === 1 ? 's' : ''} the handshake: the mod stands down and classic hooks keep route/post-edit`, fix: 'ruflo init --upgrade (refresh helpers in the project and in ~/.claude)' });
|
|
98
|
+
// 5. Policy projection.
|
|
99
|
+
const projectionPath = join(root, PROJECTION_RELATIVE);
|
|
100
|
+
const projection = readJson(projectionPath);
|
|
101
|
+
const stateMode = get(readJson(join(root, '.claude-flow', 'policy', 'state.json')), 'mode');
|
|
102
|
+
findings.push(projection !== undefined
|
|
103
|
+
? { name: 'policy projection', status: 'pass', message: `${get(projection, 'mode')} mode, ${get(projection, 'rules')?.length ?? 0} claude-code rule(s)` }
|
|
104
|
+
: stateMode === 'enforce' || stateMode === 'observe'
|
|
105
|
+
? { name: 'policy projection', status: 'warn', message: `policy is ${String(stateMode)} but no projection exists (no claude-code.* rules, or state written by an older CLI)`, fix: 'ruflo mods sync-policy' }
|
|
106
|
+
: { name: 'policy projection', status: 'pass', message: 'none (no ruflo policy for Claude Code tools)' });
|
|
107
|
+
// 6. Evidence of a load.
|
|
108
|
+
const beat = readJson(join(root, HEARTBEAT_RELATIVE));
|
|
109
|
+
const startedAt = get(beat, 'startedAt');
|
|
110
|
+
const owned = get(beat, 'owned');
|
|
111
|
+
findings.push(typeof startedAt === 'string'
|
|
112
|
+
? { name: 'last mod start', status: 'pass', message: `${startedAt}, owning ${Array.isArray(owned) && owned.length ? owned.join(', ') : 'nothing'}` }
|
|
113
|
+
: { name: 'last mod start', status: enabledIn ? 'warn' : 'pass', message: 'the mod has not started in this project' + (enabledIn ? ' (restart Claude Code; check function hooks above)' : '') });
|
|
114
|
+
if (readRecord(root))
|
|
115
|
+
findings.push({ name: 'install record', status: 'pass', message: join(root, '.claude-flow', 'mods', 'install.json') });
|
|
116
|
+
return findings;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=probe.js.map
|
|
@@ -4,6 +4,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
4
4
|
import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { hostname, userInfo } from 'node:os';
|
|
7
|
+
import { syncPolicyProjection } from '../mods/policy-projection.js';
|
|
7
8
|
const POLICY_DIR = join('.claude-flow', 'policy');
|
|
8
9
|
const POLICY_FILE = 'state.json';
|
|
9
10
|
const LOCK_FILE = 'state.lock';
|
|
@@ -227,6 +228,20 @@ function verifyStateAnchor(projectRoot, state) {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
async function writePolicyState(projectRoot, statePath, state) {
|
|
231
|
+
await writePolicyStateFiles(projectRoot, statePath, state);
|
|
232
|
+
// ADR-404: the ruflo mod reads Claude Code tool rules from a small
|
|
233
|
+
// projection, never from state.json. Written only after the state and its
|
|
234
|
+
// anchor are safely down; a failure here never fails the state write. A
|
|
235
|
+
// projection left stale by such a failure can only tighten, never loosen,
|
|
236
|
+
// a Claude Code verdict (the mod merges with `stricter`).
|
|
237
|
+
try {
|
|
238
|
+
syncPolicyProjection(projectRoot, state);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
process.stderr.write(`[policy] claude-code projection not written: ${error.message}\n`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function writePolicyStateFiles(projectRoot, statePath, state) {
|
|
230
245
|
const anchorPath = trustPaths(projectRoot).anchor;
|
|
231
246
|
if (state.mode === 'enforce' || existsSync(anchorPath)) {
|
|
232
247
|
const key = trustKey(projectRoot, true);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.50.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|