claude-flow 3.56.5 → 3.56.7

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 (59) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +109 -10
  2. package/.claude/helpers/hook-handler.cjs +198 -1
  3. package/.claude/helpers/intelligence.cjs +57 -4
  4. package/node_modules/@claude-flow/codex/README.md +20 -3
  5. package/node_modules/@claude-flow/codex/dist/cli.js +7 -0
  6. package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
  7. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  8. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  9. package/node_modules/@claude-flow/codex/dist/initializer.d.ts +8 -3
  10. package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
  11. package/node_modules/@claude-flow/codex/dist/initializer.js +75 -13
  12. package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
  13. package/node_modules/@claude-flow/codex/dist/types.d.ts +8 -0
  14. package/node_modules/@claude-flow/codex/dist/types.d.ts.map +1 -1
  15. package/node_modules/@claude-flow/codex/package.json +1 -1
  16. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  17. package/package.json +7 -4
  18. package/v3/@claude-flow/cli/bin/cli.js +11 -1
  19. package/v3/@claude-flow/cli/bin/mcp-server.js +11 -1
  20. package/v3/@claude-flow/cli/dist/src/commands/doctor-native-addon.d.ts +23 -0
  21. package/v3/@claude-flow/cli/dist/src/commands/doctor-native-addon.js +135 -0
  22. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +3 -0
  23. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +8 -1
  24. package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
  25. package/v3/@claude-flow/cli/dist/src/commands/init.d.ts +2 -0
  26. package/v3/@claude-flow/cli/dist/src/commands/init.js +37 -5
  27. package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +3 -0
  28. package/v3/@claude-flow/cli/dist/src/commands/mods.js +19 -5
  29. package/v3/@claude-flow/cli/dist/src/commands/tui.d.ts +12 -0
  30. package/v3/@claude-flow/cli/dist/src/commands/tui.js +98 -0
  31. package/v3/@claude-flow/cli/dist/src/fs-secure.d.ts +25 -0
  32. package/v3/@claude-flow/cli/dist/src/fs-secure.js +51 -2
  33. package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -14
  34. package/v3/@claude-flow/cli/dist/src/init/helper-commands.d.ts +2 -0
  35. package/v3/@claude-flow/cli/dist/src/init/helper-commands.js +42 -11
  36. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -1
  37. package/v3/@claude-flow/cli/dist/src/init/settings-generator.js +3 -3
  38. package/v3/@claude-flow/cli/dist/src/init/settings-risk-scanner.js +25 -6
  39. package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -2
  40. package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.d.ts +1 -1
  41. package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.js +1 -1
  42. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +41 -15
  43. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +2 -1
  44. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -1
  45. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +3 -1
  46. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +4 -0
  47. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +3 -2
  48. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +4 -0
  49. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +12 -11
  50. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +7 -3
  51. package/v3/@claude-flow/cli/dist/src/mods/install.js +32 -12
  52. package/v3/@claude-flow/cli/dist/src/mods/plugin-resolve.js +3 -1
  53. package/v3/@claude-flow/cli/dist/src/mods/probe.js +92 -4
  54. package/v3/@claude-flow/cli/dist/src/services/command-recording.d.ts +17 -0
  55. package/v3/@claude-flow/cli/dist/src/services/command-recording.js +91 -0
  56. package/v3/@claude-flow/cli/package.json +4 -4
  57. package/v3/@claude-flow/shared/dist/events/rvf-event-log.js +19 -11
  58. package/v3/@claude-flow/shared/dist/resilience/circuit-breaker.js +5 -3
  59. package/v3/@claude-flow/shared/package.json +1 -1
@@ -107,18 +107,38 @@ export function installMod(projectRoot, scope, dryRun = false, plugins = MOD_PLU
107
107
  backup = `${settingsFile}.bak-ruflo-mods-${Date.now()}`;
108
108
  copyFileSync(settingsFile, backup);
109
109
  }
110
- writeJson(settingsFile, next);
111
- // What ruflo added once is still ruflo's to remove: merge into the record.
112
- const record = readRecord(projectRoot) ?? { version: 2, installedAt: '', files: {} };
113
- const before = record.files[settingsFile] ?? { plugins: [], marketplace: false, env: false };
114
- record.files[settingsFile] = {
115
- plugins: [...new Set([...before.plugins, ...added.plugins])],
116
- marketplace: before.marketplace || added.marketplace,
117
- env: before.env || added.env,
118
- claudeInstalled: before.claudeInstalled ?? [],
119
- };
120
- record.installedAt = new Date().toISOString();
121
- writeJson(join(resolve(projectRoot), INSTALL_RECORD), record);
110
+ // One operation: the settings and the record that lets uninstall take them back are written together.
111
+ // If the record cannot be written, the settings go back as they were (or the file ruflo created goes
112
+ // away), so nothing is left half-installed with no record to remove it by.
113
+ try {
114
+ writeJson(settingsFile, next);
115
+ // What ruflo added once is still ruflo's to remove: merge into the record.
116
+ const record = readRecord(projectRoot) ?? { version: 2, installedAt: '', files: {} };
117
+ const before = record.files[settingsFile] ?? { plugins: [], marketplace: false, env: false };
118
+ record.files[settingsFile] = {
119
+ plugins: [...new Set([...before.plugins, ...added.plugins])],
120
+ marketplace: before.marketplace || added.marketplace,
121
+ env: before.env || added.env,
122
+ claudeInstalled: before.claudeInstalled ?? [],
123
+ };
124
+ record.installedAt = new Date().toISOString();
125
+ writeJson(join(resolve(projectRoot), INSTALL_RECORD), record);
126
+ }
127
+ catch (error) {
128
+ try {
129
+ if (backup) {
130
+ copyFileSync(backup, settingsFile);
131
+ unlinkSync(backup);
132
+ }
133
+ else if (existsSync(settingsFile)) {
134
+ unlinkSync(settingsFile);
135
+ }
136
+ }
137
+ catch {
138
+ // the restore itself failed: the original error below is the one to report
139
+ }
140
+ throw error;
141
+ }
122
142
  return { settingsFile, backup, added, dryRun, next };
123
143
  }
124
144
  /** The install record, v1 (3.50.0: one file, ruflo-mods only) read as v2. */
@@ -160,7 +160,9 @@ export function resolveFindings(projectRoot, scope, configDir, fs = nodeReadFs,
160
160
  else if (!market.location) {
161
161
  findings.push(installed.installed
162
162
  ? { name, status: 'pass', message: `installed (${installed.scope} scope, ${installed.installPath})` }
163
- : { name, status: 'warn', message: plugin.required ? 'loads once Claude Code has cloned the marketplace' : 'pending: not released in the ruflo marketplace yet' });
163
+ : plugin.required
164
+ ? { name, status: 'warn', message: 'loads once Claude Code has cloned the marketplace (a headless `claude -p` run on a fresh config does not clone it, so the mod is absent there)', fix }
165
+ : { name, status: 'warn', message: 'pending: not released in the ruflo marketplace yet' });
164
166
  }
165
167
  else if (!plugin.required) {
166
168
  findings.push({ name, status: 'warn', message: `pending: not in the ruflo marketplace yet (${market.location})${also}` });
@@ -8,8 +8,8 @@ import { existsSync, readFileSync } from 'node:fs';
8
8
  import { homedir, platform } from 'node:os';
9
9
  import { join, resolve } from 'node:path';
10
10
  import { findClaudeInstalls, judgeInstalls } from './claude-installs.js';
11
- import { ENABLE_ENV, MOD_PLUGIN_ID, MOD_PLUGIN_IDS, readRecord, readSettingsFile, settingsFileFor } from './install.js';
12
- import { claudeConfigDir, resolveFindings } from './plugin-resolve.js';
11
+ import { ENABLE_ENV, MARKETPLACE_NAME, MOD_PLUGIN_ID, MOD_PLUGIN_IDS, readRecord, readSettingsFile, settingsFileFor } from './install.js';
12
+ import { claudeConfigDir, installedState, resolveFindings } from './plugin-resolve.js';
13
13
  import { PROJECTION_RELATIVE } from './policy-projection.js';
14
14
  export const HANDSHAKE_MARKER = 'RUFLO_MODS_OWNS';
15
15
  export const HEARTBEAT_RELATIVE = join('.claude-flow', 'mods', 'session.json');
@@ -53,6 +53,65 @@ export function legacyCliHooks(files) {
53
53
  return commands(LEGACY_EVENTS[sub]).some((c) => word.test(c));
54
54
  });
55
55
  }
56
+ /** Settings files (in the order given) where `enabledPlugins[id]` is exactly `value`. */
57
+ function enabledValues(files, id) {
58
+ const out = new Map();
59
+ for (const f of files) {
60
+ try {
61
+ const v = get(readSettingsFile(f), 'enabledPlugins', id);
62
+ if (v !== undefined)
63
+ out.set(f, v);
64
+ }
65
+ catch {
66
+ // an unparseable file is reported elsewhere; never overwritten
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ function partialInstallFindings(root, files, _inputs) {
72
+ const enabled = new Set(MOD_PLUGIN_IDS.filter((id) => [...enabledValues(files, id).values()].includes(true)));
73
+ const out = [];
74
+ const parts = [];
75
+ if (enabled.size > 0 && enabled.size < MOD_PLUGIN_IDS.length) {
76
+ const missing = MOD_PLUGIN_IDS.filter((id) => !enabled.has(id) && ![...enabledValues(files, id).values()].includes(false));
77
+ if (missing.length)
78
+ parts.push(`enabled ${[...enabled].join(', ')} but not ${missing.join(', ')}`);
79
+ }
80
+ if (enabled.size > 0) {
81
+ const declared = files.some((f) => {
82
+ try {
83
+ return get(readSettingsFile(f), 'extraKnownMarketplaces', MARKETPLACE_NAME) !== undefined;
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ });
89
+ if (!declared)
90
+ parts.push(`no \`${MARKETPLACE_NAME}\` marketplace is declared in settings for Claude Code to load them from`);
91
+ }
92
+ const record = readRecord(root);
93
+ if (record) {
94
+ const lost = [];
95
+ for (const [file, added] of Object.entries(record.files)) {
96
+ try {
97
+ const now = readSettingsFile(file);
98
+ for (const id of added.plugins)
99
+ if (get(now, 'enabledPlugins', id) !== true && get(now, 'enabledPlugins', id) !== false)
100
+ lost.push(id);
101
+ if (added.marketplace && get(now, 'extraKnownMarketplaces', MARKETPLACE_NAME) === undefined)
102
+ lost.push(`${MARKETPLACE_NAME} marketplace`);
103
+ }
104
+ catch {
105
+ // unreadable settings: nothing to compare
106
+ }
107
+ }
108
+ if (lost.length)
109
+ parts.push(`the install record says ruflo added ${[...new Set(lost)].join(', ')}, which the settings no longer have`);
110
+ }
111
+ if (parts.length)
112
+ out.push({ name: 'partial install', status: 'warn', message: parts.join('; '), fix: 'ruflo mods install' });
113
+ return out;
114
+ }
56
115
  export function probeMods(inputs) {
57
116
  const root = resolve(inputs.projectRoot);
58
117
  const home = inputs.home ?? homedir();
@@ -69,9 +128,38 @@ export function probeMods(inputs) {
69
128
  return false;
70
129
  }
71
130
  });
131
+ // A plugin a person set to false is their decision: `ruflo mods install` never flips it, so it is not the repair.
132
+ const disabledIn = enabledIn ? undefined : files.find((f) => {
133
+ try {
134
+ return get(readSettingsFile(f), 'enabledPlugins', MOD_PLUGIN_ID) === false;
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ });
140
+ const cachedInstall = enabledIn || disabledIn ? { installed: false } : installedState(root, configDir, MOD_PLUGIN_ID, inputs.fs);
141
+ const scopeOf = (f) => (f === files[0] ? 'local' : f === files[1] ? 'project' : 'user');
72
142
  findings.push(enabledIn
73
143
  ? { name: 'ruflo-mods plugin', status: 'pass', message: `enabled in settings: ${enabledIn} (whether Claude Code can load it: see below)` }
74
- : { name: 'ruflo-mods plugin', status: 'warn', message: 'not enabled; classic hooks handle every event', fix: 'ruflo mods install' });
144
+ : disabledIn
145
+ ? {
146
+ name: 'ruflo-mods plugin',
147
+ status: 'warn',
148
+ message: `disabled in ${disabledIn} (enabledPlugins is false): the mod does not load and classic hooks handle every event; \`ruflo mods install\` leaves a plugin you switched off alone`,
149
+ fix: `claude plugin enable ${MOD_PLUGIN_ID} --scope ${scopeOf(disabledIn)}`,
150
+ }
151
+ : cachedInstall.installed
152
+ ? {
153
+ name: 'ruflo-mods plugin',
154
+ status: 'warn',
155
+ message: `installed (${cachedInstall.scope} scope) but not enabled in this project's settings: the mod does not load`,
156
+ fix: `claude plugin enable ${MOD_PLUGIN_ID} --scope ${cachedInstall.scope === 'user' ? 'user' : 'local'}`,
157
+ }
158
+ : { name: 'ruflo-mods plugin', status: 'warn', message: 'not enabled; classic hooks handle every event', fix: 'ruflo mods install' });
159
+ // 1b. A half-finished install: some of the mod plugins enabled and not the rest, a plugin enabled with no
160
+ // marketplace declared for Claude Code to load it from, or keys the install record says ruflo added that
161
+ // the settings file no longer has. `ruflo mods install` is idempotent and adds exactly what is missing.
162
+ findings.push(...partialInstallFindings(root, files, inputs));
75
163
  // 1a. Enabled is a request; Claude Code silently skips a plugin its
76
164
  // marketplace clone does not carry (a clone older than the plugin).
77
165
  if (enabledIn) {
@@ -163,7 +251,7 @@ export function probeMods(inputs) {
163
251
  const owned = get(beat, 'owned');
164
252
  findings.push(typeof startedAt === 'string'
165
253
  ? { name: 'last mod start', status: 'pass', message: `${startedAt}, owning ${Array.isArray(owned) && owned.length ? owned.join(', ') : 'nothing'}` }
166
- : { 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)' : '') });
254
+ : { 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)' : ''), ...(enabledIn ? { fix: 'restart Claude Code in this project, then run /ruflo mods' } : {}) });
167
255
  if (readRecord(root))
168
256
  findings.push({ name: 'install record', status: 'pass', message: join(root, '.claude-flow', 'mods', 'install.json') });
169
257
  return findings;
@@ -0,0 +1,17 @@
1
+ /** Longest command text kept per row. */
2
+ export declare const MAX_RECORDED_COMMAND_CHARS = 2000;
3
+ /**
4
+ * True when command recording is switched off. Any one of these turns it off:
5
+ * - `CLAUDE_FLOW_HOOKS_RECORD_COMMANDS` = 0 | false | off | no
6
+ * - `CLAUDE_FLOW_HOOKS_ENABLED` = 0 | false | off | no
7
+ * - `hooks.recordCommands: false` in `claude-flow.config.json` or `.claude-flow/config.json`
8
+ */
9
+ export declare function commandRecordingDisabled(env?: Record<string, string | undefined>, cwd?: string): boolean;
10
+ /**
11
+ * The form of a command that may be written to the store: secrets of the
12
+ * common shapes replaced with `[redacted]`, and the text capped at
13
+ * `MAX_RECORDED_COMMAND_CHARS`. This is best effort, not a guarantee; the off
14
+ * switch is the guarantee.
15
+ */
16
+ export declare function redactCommandForStorage(command: string): string;
17
+ //# sourceMappingURL=command-recording.d.ts.map
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Policy for what the PostToolUse Bash hook may keep (#4056).
3
+ *
4
+ * `hooks post-command` used to store every command in full as
5
+ * `commands/cmd-<ts>`: 27k rows / 285 MB on one project, full command text
6
+ * that can carry tokens, and no way to switch it off. This module answers two
7
+ * questions for that writer:
8
+ *
9
+ * 1. May a command be recorded at all? (`commandRecordingDisabled`)
10
+ * 2. What form of it may be stored? (`redactCommandForStorage`)
11
+ *
12
+ * Every pattern below is bounded, and the input is capped before any pattern
13
+ * runs, so a hostile command cannot stall the hook (the same lesson as the
14
+ * settings-risk scanner, #4053).
15
+ */
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ /** Longest command text kept per row. */
19
+ export const MAX_RECORDED_COMMAND_CHARS = 2000;
20
+ /** Only this much of a command is ever scanned; the stored text is shorter still. */
21
+ const MAX_SCANNED_CHARS = 8000;
22
+ const OFF_VALUES = new Set(['0', 'false', 'off', 'no']);
23
+ const isOff = (v) => v !== undefined && OFF_VALUES.has(v.trim().toLowerCase());
24
+ function configDisablesRecording(cwd) {
25
+ for (const file of [
26
+ path.resolve(cwd, 'claude-flow.config.json'),
27
+ path.resolve(cwd, '.claude-flow', 'config.json'),
28
+ ]) {
29
+ try {
30
+ if (!fs.existsSync(file))
31
+ continue;
32
+ const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
33
+ if (raw?.hooks?.recordCommands === false)
34
+ return true;
35
+ }
36
+ catch {
37
+ /* malformed config: keep the default (recording on) rather than guess */
38
+ }
39
+ }
40
+ return false;
41
+ }
42
+ /**
43
+ * True when command recording is switched off. Any one of these turns it off:
44
+ * - `CLAUDE_FLOW_HOOKS_RECORD_COMMANDS` = 0 | false | off | no
45
+ * - `CLAUDE_FLOW_HOOKS_ENABLED` = 0 | false | off | no
46
+ * - `hooks.recordCommands: false` in `claude-flow.config.json` or `.claude-flow/config.json`
47
+ */
48
+ export function commandRecordingDisabled(env = process.env, cwd = process.cwd()) {
49
+ if (isOff(env.CLAUDE_FLOW_HOOKS_RECORD_COMMANDS))
50
+ return true;
51
+ if (isOff(env.CLAUDE_FLOW_HOOKS_ENABLED))
52
+ return true;
53
+ return configDisablesRecording(cwd);
54
+ }
55
+ const QUOTED_OR_BARE = `"[^"]{0,512}"|'[^']{0,512}'|[^\\s"']{1,512}`;
56
+ // Order matters little; each rule only ever shortens the text.
57
+ const RULES = [
58
+ // Authorization: Bearer xyz / authorization=Basic xyz
59
+ [/(authorization["']?\s*[:=]\s*)(?:(?:bearer|basic|token)\s+)?[^\s"']{1,512}/gi, '$1[redacted]'],
60
+ // --password x / --token=x / --api-key "x" / -token x
61
+ [new RegExp(`(--?(?:password|passwd|pwd|token|secret|api[-_]?key|access[-_]?token|auth[-_]?token|client[-_]?secret)(?:=|\\s+))(?:${QUOTED_OR_BARE})`, 'gi'), '$1[redacted]'],
62
+ // NPM_TOKEN=x AWS_SECRET_ACCESS_KEY=x DB_PASSWORD=x
63
+ [new RegExp(`\\b([A-Za-z_][A-Za-z0-9_]{0,63}(?:TOKEN|SECRET|PASSWORD|PASSWD|API_?KEY|ACCESS_?KEY|PRIVATE_?KEY|CREDENTIALS?))=(?:${QUOTED_OR_BARE})`, 'gi'), '$1=[redacted]'],
64
+ // user:password@host in a URL
65
+ [/(:\/\/[^\s:@/]{1,128}:)[^\s@/]{1,256}(@)/g, '$1[redacted]$2'],
66
+ // well-known token shapes
67
+ [/\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|npm_[A-Za-z0-9]{20,}|eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,})/g, '[redacted]'],
68
+ ];
69
+ /**
70
+ * The form of a command that may be written to the store: secrets of the
71
+ * common shapes replaced with `[redacted]`, and the text capped at
72
+ * `MAX_RECORDED_COMMAND_CHARS`. This is best effort, not a guarantee; the off
73
+ * switch is the guarantee.
74
+ */
75
+ export function redactCommandForStorage(command) {
76
+ let text = String(command);
77
+ if (text.length > MAX_SCANNED_CHARS) {
78
+ // Cut at a token boundary so a half-token at the cut is not left behind.
79
+ const cut = text.slice(0, MAX_SCANNED_CHARS);
80
+ const lastSpace = cut.search(/\s\S*$/);
81
+ text = lastSpace > 0 ? cut.slice(0, lastSpace) : cut;
82
+ text += ' ';
83
+ }
84
+ for (const [re, replacement] of RULES)
85
+ text = text.replace(re, replacement);
86
+ if (text.length > MAX_RECORDED_COMMAND_CHARS) {
87
+ text = `${text.slice(0, MAX_RECORDED_COMMAND_CHARS)} …[truncated]`;
88
+ }
89
+ return text.trimEnd();
90
+ }
91
+ //# sourceMappingURL=command-recording.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.56.5",
3
+ "version": "3.56.7",
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",
@@ -98,13 +98,13 @@
98
98
  "vitest": "^4.1.0"
99
99
  },
100
100
  "dependencies": {
101
- "@claude-flow/cli-core": "3.7.0",
102
- "@claude-flow/codex": "3.0.3",
101
+ "@claude-flow/cli-core": "3.7.1",
102
+ "@claude-flow/codex": "3.0.4",
103
103
  "@claude-flow/mcp": "3.1.1",
104
104
  "@claude-flow/neural": "3.0.0",
105
105
  "@claude-flow/plugin-agent-federation": "1.0.1",
106
106
  "@claude-flow/security": "3.0.4",
107
- "@claude-flow/shared": "3.0.2",
107
+ "@claude-flow/shared": "3.0.3",
108
108
  "@iarna/toml": "^2.2.5",
109
109
  "@noble/ed25519": "2.3.0",
110
110
  "@ruvector/rabitq-wasm": "0.1.0",
@@ -69,6 +69,12 @@ export class RvfEventLog extends EventEmitter {
69
69
  async initialize() {
70
70
  if (this.initialized)
71
71
  return;
72
+ // A failed prior replay may have populated only part of the indexes.
73
+ // Every initialization attempt must rebuild from an empty state.
74
+ this.events = [];
75
+ this.aggregateIndex.clear();
76
+ this.aggregateVersions.clear();
77
+ this.snapshots.clear();
72
78
  this.ensureDirectory(this.config.logPath);
73
79
  // --- events file ---
74
80
  if (existsSync(this.config.logPath)) {
@@ -129,10 +135,10 @@ export class RvfEventLog extends EventEmitter {
129
135
  const nextVersion = currentVersion + 1;
130
136
  event.version = nextVersion;
131
137
  // Persist to disk first (crash-safe ordering)
132
- this.appendRecord(this.config.logPath, event);
138
+ const persisted = this.appendRecord(this.config.logPath, event);
133
139
  // Update in-memory state
134
- this.indexEvent(event);
135
- this.emit('event:appended', event);
140
+ this.indexEvent(persisted);
141
+ this.emit('event:appended', structuredClone(persisted));
136
142
  if (nextVersion % this.config.snapshotThreshold === 0) {
137
143
  this.emit('snapshot:recommended', {
138
144
  aggregateId: event.aggregateId,
@@ -143,9 +149,9 @@ export class RvfEventLog extends EventEmitter {
143
149
  /** Save a snapshot for an aggregate. */
144
150
  async saveSnapshot(snapshot) {
145
151
  this.ensureInitialized();
146
- this.appendRecord(this.snapshotPath, snapshot);
147
- this.snapshots.set(snapshot.aggregateId, snapshot);
148
- this.emit('snapshot:saved', snapshot);
152
+ const persisted = this.appendRecord(this.snapshotPath, snapshot);
153
+ this.snapshots.set(persisted.aggregateId, persisted);
154
+ this.emit('snapshot:saved', structuredClone(persisted));
149
155
  }
150
156
  // ===========================================================================
151
157
  // Read Operations
@@ -156,7 +162,7 @@ export class RvfEventLog extends EventEmitter {
156
162
  const indices = this.aggregateIndex.get(aggregateId);
157
163
  if (!indices || indices.length === 0)
158
164
  return [];
159
- let result = indices.map((i) => this.events[i]);
165
+ let result = indices.map((i) => structuredClone(this.events[i]));
160
166
  if (fromVersion !== undefined) {
161
167
  result = result.filter((e) => e.version >= fromVersion);
162
168
  }
@@ -168,9 +174,9 @@ export class RvfEventLog extends EventEmitter {
168
174
  async getAllEvents(filter) {
169
175
  this.ensureInitialized();
170
176
  if (!filter) {
171
- return [...this.events].sort((a, b) => a.timestamp - b.timestamp);
177
+ return structuredClone(this.events).sort((a, b) => a.timestamp - b.timestamp);
172
178
  }
173
- let result = [...this.events];
179
+ let result = structuredClone(this.events);
174
180
  // Aggregate ID filter
175
181
  if (filter.aggregateIds && filter.aggregateIds.length > 0) {
176
182
  const set = new Set(filter.aggregateIds);
@@ -203,7 +209,7 @@ export class RvfEventLog extends EventEmitter {
203
209
  if (filter.offset) {
204
210
  result = result.slice(filter.offset);
205
211
  }
206
- if (filter.limit) {
212
+ if (filter.limit !== undefined) {
207
213
  result = result.slice(0, filter.limit);
208
214
  }
209
215
  return result;
@@ -211,7 +217,7 @@ export class RvfEventLog extends EventEmitter {
211
217
  /** Get latest snapshot for an aggregate. */
212
218
  async getSnapshot(aggregateId) {
213
219
  this.ensureInitialized();
214
- return this.snapshots.get(aggregateId) ?? null;
220
+ return structuredClone(this.snapshots.get(aggregateId) ?? null);
215
221
  }
216
222
  /** Return event store statistics. */
217
223
  async getStats() {
@@ -301,10 +307,12 @@ export class RvfEventLog extends EventEmitter {
301
307
  /** Append a single record to an RVF file. */
302
308
  appendRecord(filePath, record) {
303
309
  const json = JSON.stringify(record);
310
+ const persisted = JSON.parse(json);
304
311
  const payload = Buffer.from(json, 'utf8');
305
312
  const lengthBuf = Buffer.allocUnsafe(LENGTH_PREFIX_BYTES);
306
313
  lengthBuf.writeUInt32BE(payload.length, 0);
307
314
  appendFileSync(filePath, Buffer.concat([lengthBuf, payload]));
315
+ return persisted;
308
316
  }
309
317
  /** Add an event to the in-memory indexes. */
310
318
  indexEvent(event) {
@@ -112,9 +112,9 @@ export class CircuitBreaker extends EventEmitter {
112
112
  successes: this.requests.filter((r) => r.success).length,
113
113
  totalRequests: this.requests.length,
114
114
  rejectedRequests: this.rejectedCount,
115
- lastFailure: this.lastFailure,
116
- lastSuccess: this.lastSuccess,
117
- openSince: this.openedAt,
115
+ lastFailure: this.lastFailure ? new Date(this.lastFailure.getTime()) : null,
116
+ lastSuccess: this.lastSuccess ? new Date(this.lastSuccess.getTime()) : null,
117
+ openSince: this.openedAt ? new Date(this.openedAt.getTime()) : null,
118
118
  };
119
119
  }
120
120
  /**
@@ -138,6 +138,7 @@ export class CircuitBreaker extends EventEmitter {
138
138
  * Handle successful request
139
139
  */
140
140
  onSuccess() {
141
+ this.cleanOldRequests();
141
142
  this.lastSuccess = new Date();
142
143
  this.requests.push({ timestamp: Date.now(), success: true });
143
144
  if (this.state === CircuitBreakerState.HALF_OPEN) {
@@ -152,6 +153,7 @@ export class CircuitBreaker extends EventEmitter {
152
153
  * Handle failed request
153
154
  */
154
155
  onFailure(error) {
156
+ this.cleanOldRequests();
155
157
  this.lastFailure = new Date();
156
158
  this.requests.push({ timestamp: Date.now(), success: false });
157
159
  if (this.state === CircuitBreakerState.HALF_OPEN) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/shared",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "type": "module",
5
5
  "description": "Shared module - common types, events, utilities, core interfaces",
6
6
  "main": "dist/index.js",