claude-flow 3.48.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.
Files changed (63) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/hook-handler.cjs +20 -4
  3. package/.claude/helpers/memory.cjs +1 -1
  4. package/.claude/helpers/router.cjs +1 -1
  5. package/.claude/helpers/session.cjs +1 -1
  6. package/.claude/proven-config.json +42 -0
  7. package/.claude-plugin/marketplace.json +26 -1
  8. package/README.md +1 -53
  9. package/README.zh-CN.md +1 -53
  10. package/node_modules/@claude-flow/codex/package.json +1 -1
  11. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
  12. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  13. package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
  14. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  15. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
  16. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  17. package/node_modules/@claude-flow/security/package.json +1 -1
  18. package/package.json +2 -2
  19. package/v3/@claude-flow/cli/README.md +3 -53
  20. package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
  21. package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
  22. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +88 -9
  23. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +7 -4
  24. package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
  25. package/v3/@claude-flow/cli/dist/src/commands/init.js +20 -0
  26. package/v3/@claude-flow/cli/dist/src/commands/memory.js +30 -7
  27. package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +13 -0
  28. package/v3/@claude-flow/cli/dist/src/commands/mods.js +126 -0
  29. package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
  30. package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
  31. package/v3/@claude-flow/cli/dist/src/commands/session.js +128 -21
  32. package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
  33. package/v3/@claude-flow/cli/dist/src/index.js +10 -1
  34. package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
  35. package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
  36. package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
  37. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
  38. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
  39. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
  40. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
  41. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +22 -10
  42. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -3
  43. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +87 -14
  44. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
  45. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +43 -6
  46. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
  47. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +236 -50
  48. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +6 -3
  49. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.d.ts +29 -0
  50. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +83 -0
  51. package/v3/@claude-flow/cli/dist/src/mods/install.d.ts +64 -0
  52. package/v3/@claude-flow/cli/dist/src/mods/install.js +135 -0
  53. package/v3/@claude-flow/cli/dist/src/mods/policy-projection.d.ts +39 -0
  54. package/v3/@claude-flow/cli/dist/src/mods/policy-projection.js +65 -0
  55. package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +28 -0
  56. package/v3/@claude-flow/cli/dist/src/mods/probe.js +118 -0
  57. package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
  58. package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
  59. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
  60. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
  61. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
  62. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +42 -2
  63. package/v3/@claude-flow/cli/package.json +2 -2
@@ -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
@@ -3,6 +3,7 @@
3
3
  * Handles actual plugin installation, persistence, and lifecycle
4
4
  * Bridges discovery service with file system persistence
5
5
  */
6
+ import { type TrustDecision } from './trust-policy.js';
6
7
  export interface InstalledPlugin {
7
8
  name: string;
8
9
  version: string;
@@ -13,6 +14,40 @@ export interface InstalledPlugin {
13
14
  commands?: string[];
14
15
  hooks?: string[];
15
16
  config?: Record<string, unknown>;
17
+ /** Declared (or registry-assigned) trust level, recorded at install time. */
18
+ trustLevel?: string;
19
+ /** Declared permissions, recorded at install time so they can be enforced. */
20
+ permissions?: string[];
21
+ /** How the install was verified. */
22
+ verification?: 'checksum' | 'npm-integrity' | 'policy' | 'skipped';
23
+ /** Whether npm lifecycle scripts ran during install (false = `--ignore-scripts`). */
24
+ scriptsRun?: boolean;
25
+ /** Hooks/commands the plugin declared but that were not registered, and why. */
26
+ withheld?: {
27
+ hooks: string[];
28
+ commands: string[];
29
+ reasons: string[];
30
+ };
31
+ }
32
+ /** Options for {@link PluginManager.installFromLocal} / {@link PluginManager.installFromNpm}. */
33
+ export interface PluginInstallOptions {
34
+ /** `--verify` (default true). */
35
+ verify?: boolean;
36
+ /** `--trust` (default false). */
37
+ trust?: boolean;
38
+ /** Registry checksum (`sha256:<hex>`) the downloaded tarball must match. */
39
+ expectedChecksum?: string;
40
+ /** Trust level from the registry entry, when the plugin was found there. */
41
+ registryTrustLevel?: string;
42
+ /** Permissions from the registry entry, merged with the package's own declaration. */
43
+ registryPermissions?: string[];
44
+ }
45
+ export interface PluginInstallResult {
46
+ success: boolean;
47
+ error?: string;
48
+ plugin?: InstalledPlugin;
49
+ decision?: TrustDecision;
50
+ warnings?: string[];
16
51
  }
17
52
  export interface InstalledPluginsManifest {
18
53
  version: '1.0.0';
@@ -46,19 +81,11 @@ export declare class PluginManager {
46
81
  /**
47
82
  * Install a plugin from npm
48
83
  */
49
- installFromNpm(packageName: string, version?: string): Promise<{
50
- success: boolean;
51
- error?: string;
52
- plugin?: InstalledPlugin;
53
- }>;
84
+ installFromNpm(packageName: string, version?: string, opts?: PluginInstallOptions): Promise<PluginInstallResult>;
54
85
  /**
55
86
  * Install a plugin from a local path
56
87
  */
57
- installFromLocal(sourcePath: string): Promise<{
58
- success: boolean;
59
- error?: string;
60
- plugin?: InstalledPlugin;
61
- }>;
88
+ installFromLocal(sourcePath: string, opts?: PluginInstallOptions): Promise<PluginInstallResult>;
62
89
  /**
63
90
  * Uninstall a plugin
64
91
  */