@claude-flow/cli 3.54.0 → 3.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/helpers.manifest.json +3 -3
- package/.claude/helpers/hook-handler.cjs +65 -3
- package/bin/cli.js +1 -1
- package/bin/mcp-server.js +1 -1
- package/catalog-manifest.json +2 -2
- package/dist/src/blocking-stdio.d.ts +11 -0
- package/dist/src/blocking-stdio.js +23 -0
- package/dist/src/commands/autopilot.js +2 -2
- package/dist/src/commands/daemon.js +21 -1
- package/dist/src/commands/hive-mind.js +7 -0
- package/dist/src/commands/mcp.js +35 -1
- package/dist/src/commands/policy.js +10 -2
- package/dist/src/commands/session.js +11 -37
- package/dist/src/index.js +3 -0
- package/dist/src/init/executor.js +1 -0
- package/dist/src/init/helpers-generator.js +65 -3
- package/dist/src/mcp-server.d.ts +33 -0
- package/dist/src/mcp-server.js +96 -4
- package/dist/src/mcp-tools/agentdb-tools.js +13 -1
- package/dist/src/mcp-tools/hive-mind-tools.d.ts +2 -4
- package/dist/src/mcp-tools/hive-mind-tools.js +214 -40
- package/dist/src/mcp-tools/hooks-tools.js +1 -1
- package/dist/src/mcp-tools/session-tools.js +4 -1
- package/dist/src/memory/graph-edge-writer.js +3 -2
- package/dist/src/memory/lucene-bm25.js +11 -19
- package/dist/src/memory/memory-bridge.d.ts +1 -0
- package/dist/src/memory/memory-bridge.js +14 -1
- package/dist/src/memory/memory-initializer.js +5 -4
- package/dist/src/memory/shared-sqlite.d.ts +7 -0
- package/dist/src/memory/shared-sqlite.js +45 -0
- package/dist/src/mods/probe.d.ts +7 -0
- package/dist/src/mods/probe.js +29 -1
- package/dist/src/parser.d.ts +3 -0
- package/dist/src/parser.js +64 -13
- package/dist/src/production/monitoring.d.ts +2 -0
- package/dist/src/production/monitoring.js +8 -3
- package/dist/src/production/rate-limiter.js +8 -5
- package/dist/src/production/retry.js +3 -1
- package/dist/src/services/bounded-worker-pool.js +3 -0
- package/dist/src/services/config-file-manager.d.ts +3 -0
- package/dist/src/services/config-file-manager.js +24 -7
- package/dist/src/services/git-workspace-identity.js +20 -3
- package/dist/src/services/policy-ledger-anchor.d.ts +42 -0
- package/dist/src/services/policy-ledger-anchor.js +163 -0
- package/dist/src/services/policy-runtime.d.ts +11 -5
- package/dist/src/services/policy-runtime.js +70 -15
- package/dist/src/services/worker-daemon.d.ts +1 -0
- package/dist/src/services/worker-daemon.js +12 -0
- package/dist/src/services/worker-queue.d.ts +1 -0
- package/dist/src/services/worker-queue.js +20 -3
- package/node_modules/@claude-flow/mcp/dist/connection-pool.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/connection-pool.js +5 -2
- package/node_modules/@claude-flow/mcp/dist/connection-pool.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts +7 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.js +60 -47
- package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts +3 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js +4 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js +1 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +11 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +19 -3
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +4 -2
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +7 -7
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
- package/plugins/ruflo-metaharness/scripts/_invoke.mjs +80 -7
- package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
- package/plugins/ruflo-metaharness/scripts/test-cli-resolution-3558.mjs +141 -0
- package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +5 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3693 — one better-sqlite3 identity per process.
|
|
3
|
+
*
|
|
4
|
+
* AgentDB (via @claude-flow/memory) owns a live native handle on memory.db. If
|
|
5
|
+
* the CLI opens its own handle through a DIFFERENT installed copy of
|
|
6
|
+
* better-sqlite3 (a nested `agentdb/node_modules/better-sqlite3` next to a
|
|
7
|
+
* hoisted one is the common layout), the two copies each statically link their
|
|
8
|
+
* own SQLite and keep separate per-process file bookkeeping. Closing the CLI's
|
|
9
|
+
* handle (graph-edge idle release: wal_checkpoint(TRUNCATE) + close) then looks
|
|
10
|
+
* like "last connection" to that copy and deletes the -wal/-shm sidecars out
|
|
11
|
+
* from under AgentDB's still-open handle.
|
|
12
|
+
*
|
|
13
|
+
* Resolve the constructor from the same place AgentDB resolves it, so both
|
|
14
|
+
* share one SQLite instance (and its connection bookkeeping). Falls back to the
|
|
15
|
+
* CLI's own copy when agentdb cannot be resolved.
|
|
16
|
+
*/
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
let cached;
|
|
19
|
+
/** Test seam: pass `from` to resolve relative to another entry file. */
|
|
20
|
+
export function resolveAgentdbBetterSqlite3(from) {
|
|
21
|
+
if (!from && cached !== undefined)
|
|
22
|
+
return cached;
|
|
23
|
+
let found = null;
|
|
24
|
+
try {
|
|
25
|
+
const own = createRequire(import.meta.url);
|
|
26
|
+
const agentdbEntry = from ?? own.resolve('agentdb');
|
|
27
|
+
const mod = createRequire(agentdbEntry)('better-sqlite3');
|
|
28
|
+
found = (mod?.default ?? mod);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
found = null;
|
|
32
|
+
}
|
|
33
|
+
if (!from)
|
|
34
|
+
cached = found;
|
|
35
|
+
return found;
|
|
36
|
+
}
|
|
37
|
+
/** Constructor shared with AgentDB when possible, else the CLI's own copy. */
|
|
38
|
+
export async function loadBetterSqlite3() {
|
|
39
|
+
const shared = resolveAgentdbBetterSqlite3();
|
|
40
|
+
if (shared)
|
|
41
|
+
return shared;
|
|
42
|
+
const mod = 'better-sqlite3';
|
|
43
|
+
return (await import(mod)).default;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=shared-sqlite.js.map
|
package/dist/src/mods/probe.d.ts
CHANGED
|
@@ -29,5 +29,12 @@ export interface ProbeInputs {
|
|
|
29
29
|
/** Reads for the plugin-resolution findings; the real filesystem when absent. */
|
|
30
30
|
fs?: ReadFs;
|
|
31
31
|
}
|
|
32
|
+
declare const LEGACY_EVENTS: {
|
|
33
|
+
readonly route: "UserPromptSubmit";
|
|
34
|
+
readonly 'post-edit': "PostToolUse";
|
|
35
|
+
};
|
|
36
|
+
/** Which of route/post-edit the settings run through the CLI's own `hooks` command, in any of the files. */
|
|
37
|
+
export declare function legacyCliHooks(files: readonly string[]): Array<keyof typeof LEGACY_EVENTS>;
|
|
32
38
|
export declare function probeMods(inputs: ProbeInputs): Finding[];
|
|
39
|
+
export {};
|
|
33
40
|
//# sourceMappingURL=probe.d.ts.map
|
package/dist/src/mods/probe.js
CHANGED
|
@@ -33,6 +33,26 @@ export function managedSettingsPath(os = platform()) {
|
|
|
33
33
|
return 'C:\\ProgramData\\ClaudeCode\\managed-settings.json';
|
|
34
34
|
return '/etc/claude-code/managed-settings.json';
|
|
35
35
|
}
|
|
36
|
+
const LEGACY_EVENTS = { route: 'UserPromptSubmit', 'post-edit': 'PostToolUse' };
|
|
37
|
+
/** Which of route/post-edit the settings run through the CLI's own `hooks` command, in any of the files. */
|
|
38
|
+
export function legacyCliHooks(files) {
|
|
39
|
+
const commands = (event) => files.flatMap((f) => {
|
|
40
|
+
let groups;
|
|
41
|
+
try {
|
|
42
|
+
groups = get(readSettingsFile(f), 'hooks', event);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
return Array.isArray(groups)
|
|
48
|
+
? groups.flatMap((g) => (Array.isArray(g?.hooks) ? g.hooks.map((h) => h?.command).filter((c) => typeof c === 'string') : []))
|
|
49
|
+
: [];
|
|
50
|
+
});
|
|
51
|
+
return Object.keys(LEGACY_EVENTS).filter((sub) => {
|
|
52
|
+
const word = new RegExp(`(?:^|[\\s/"'])(?:@claude-flow/cli|claude-flow|ruflo)(?:@[\\w.-]+)?["']?\\s+hooks\\s+${sub}(?![\\w-])`);
|
|
53
|
+
return commands(LEGACY_EVENTS[sub]).some((c) => word.test(c));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
36
56
|
export function probeMods(inputs) {
|
|
37
57
|
const root = resolve(inputs.projectRoot);
|
|
38
58
|
const home = inputs.home ?? homedir();
|
|
@@ -104,6 +124,14 @@ export function probeMods(inputs) {
|
|
|
104
124
|
findings.push(managedOnly !== undefined && managedOnly !== false
|
|
105
125
|
? { name: 'managed policy', status: 'warn', message: 'allowManagedModsOnly is set: Claude Code refuses user mods; classic hooks stay in charge' }
|
|
106
126
|
: { name: 'managed policy', status: 'pass', message: managed === undefined ? 'no managed settings' : 'user mods allowed (allowManagedModsOnly not set)' });
|
|
127
|
+
// 4a. Settings that still run the CLI's own `hooks route|post-edit` (what init
|
|
128
|
+
// wrote before hook-handler.cjs). That command never reads the handshake, so
|
|
129
|
+
// the mod stands down for those events (plugins/ruflo-mods/hooks/ownership.ts,
|
|
130
|
+
// legacyCliConfigured: keep the two patterns in step).
|
|
131
|
+
const legacy = legacyCliHooks(files);
|
|
132
|
+
if (legacy.length) {
|
|
133
|
+
findings.push({ name: 'legacy CLI hooks', status: 'warn', message: `settings run the CLI's own \`hooks ${legacy.join('` and `hooks ')}\`: the mod stands down for ${legacy.join(' and ')}, which that command keeps`, fix: 'ruflo init upgrade --settings (regenerate the settings hooks)' });
|
|
134
|
+
}
|
|
107
135
|
// 4. Every classic helper a hook could run honours the handshake (the mod's
|
|
108
136
|
// own rule, plugins/ruflo-mods/hooks/session.ts); else the mod stands down.
|
|
109
137
|
const helpers = [...new Set([join(root, '.claude', 'helpers', 'hook-handler.cjs'), join(home, '.claude', 'helpers', 'hook-handler.cjs')])].filter(existsSync);
|
|
@@ -116,7 +144,7 @@ export function probeMods(inputs) {
|
|
|
116
144
|
}
|
|
117
145
|
});
|
|
118
146
|
findings.push(helpers.length === 0
|
|
119
|
-
? { name: 'classic handshake', status: 'pass', message: 'no hook-handler.cjs: the mod owns route and post-edit outright' }
|
|
147
|
+
? { name: 'classic handshake', status: 'pass', message: legacy.length ? 'no hook-handler.cjs (see legacy CLI hooks)' : 'no hook-handler.cjs: the mod owns route and post-edit outright' }
|
|
120
148
|
: stale.length === 0
|
|
121
149
|
? { name: 'classic handshake', status: 'pass', message: `${helpers.join(', ')} hand route and post-edit to the mod while it runs` }
|
|
122
150
|
: { 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)' });
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -94,6 +94,9 @@ export declare class CommandParser {
|
|
|
94
94
|
* commit message.)
|
|
95
95
|
*/
|
|
96
96
|
private getScopedBooleanFlags;
|
|
97
|
+
/** Resolve declared types from broad to narrow without borrowing string
|
|
98
|
+
* declarations from unrelated registered commands. */
|
|
99
|
+
private getFlagScope;
|
|
97
100
|
private getBooleanFlags;
|
|
98
101
|
private applyDefaults;
|
|
99
102
|
validateFlags(flags: ParsedFlags, command?: Command): string[];
|
package/dist/src/parser.js
CHANGED
|
@@ -135,14 +135,27 @@ export class CommandParser {
|
|
|
135
135
|
// with `daemon` left as a positional.
|
|
136
136
|
let resolvedCmd;
|
|
137
137
|
let resolvedSub;
|
|
138
|
+
const resolvedCommands = [];
|
|
138
139
|
let sawFirstPositional = false;
|
|
139
|
-
for (
|
|
140
|
-
|
|
140
|
+
for (let index = 0; index < args.length; index++) {
|
|
141
|
+
const arg = args[index];
|
|
142
|
+
if (arg === '--')
|
|
143
|
+
break;
|
|
144
|
+
if (arg.startsWith('-')) {
|
|
145
|
+
// Pass 2 recognizes only contiguous subcommands after the root.
|
|
146
|
+
if (sawFirstPositional)
|
|
147
|
+
break;
|
|
148
|
+
// Skip consumed values as well as flags: a global option value is not
|
|
149
|
+
// the command slot, even when it happens to name another command.
|
|
150
|
+
const scope = this.getFlagScope(resolvedCommands);
|
|
151
|
+
index = this.parseFlag(args, index, scope.aliases, scope.booleanFlags, scope.stringFlags).nextIndex - 1;
|
|
141
152
|
continue;
|
|
153
|
+
}
|
|
142
154
|
if (!sawFirstPositional) {
|
|
143
155
|
sawFirstPositional = true;
|
|
144
156
|
if (this.commands.has(arg)) {
|
|
145
157
|
resolvedCmd = this.commands.get(arg);
|
|
158
|
+
resolvedCommands.push(resolvedCmd);
|
|
146
159
|
continue;
|
|
147
160
|
}
|
|
148
161
|
// Lazy command: we know its name but not its subcommands. Stop the
|
|
@@ -153,14 +166,21 @@ export class CommandParser {
|
|
|
153
166
|
// Unknown first positional — not a command. Stop walking.
|
|
154
167
|
break;
|
|
155
168
|
}
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
// Match the same supported command depth as Pass 2, with each option
|
|
170
|
+
// declaration taking precedence over its parent scope.
|
|
171
|
+
const parent = resolvedCommands[resolvedCommands.length - 1];
|
|
172
|
+
if (resolvedCommands.length < 4 && parent?.subcommands) {
|
|
173
|
+
const child = parent.subcommands.find(sc => sc.name === arg || sc.aliases?.includes(arg));
|
|
174
|
+
if (child) {
|
|
175
|
+
resolvedCommands.push(child);
|
|
176
|
+
resolvedSub ??= child;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
158
179
|
}
|
|
180
|
+
// A positional that is not the next subcommand ends the command path.
|
|
181
|
+
break;
|
|
159
182
|
}
|
|
160
|
-
|
|
161
|
-
// Subcommand-specific aliases take priority over global ones
|
|
162
|
-
const aliases = this.buildScopedAliases(resolvedSub || resolvedCmd);
|
|
163
|
-
const booleanFlags = this.getScopedBooleanFlags(resolvedSub || resolvedCmd);
|
|
183
|
+
const { aliases, booleanFlags, stringFlags } = this.getFlagScope(resolvedCommands);
|
|
164
184
|
let i = 0;
|
|
165
185
|
let parsingFlags = true;
|
|
166
186
|
while (i < args.length) {
|
|
@@ -173,7 +193,7 @@ export class CommandParser {
|
|
|
173
193
|
}
|
|
174
194
|
// Handle flags
|
|
175
195
|
if (parsingFlags && arg.startsWith('-')) {
|
|
176
|
-
const parseResult = this.parseFlag(args, i, aliases, booleanFlags);
|
|
196
|
+
const parseResult = this.parseFlag(args, i, aliases, booleanFlags, stringFlags);
|
|
177
197
|
// Apply to result flags
|
|
178
198
|
Object.assign(result.flags, parseResult.flags);
|
|
179
199
|
i = parseResult.nextIndex;
|
|
@@ -231,7 +251,7 @@ export class CommandParser {
|
|
|
231
251
|
this.applyDefaults(result.flags, resolvedCmd, resolvedSub);
|
|
232
252
|
return result;
|
|
233
253
|
}
|
|
234
|
-
parseFlag(args, index, aliases, booleanFlags) {
|
|
254
|
+
parseFlag(args, index, aliases, booleanFlags, stringFlags) {
|
|
235
255
|
const flags = { _: [] };
|
|
236
256
|
const arg = args[index];
|
|
237
257
|
let nextIndex = index + 1;
|
|
@@ -242,7 +262,8 @@ export class CommandParser {
|
|
|
242
262
|
// --flag=value
|
|
243
263
|
const key = arg.slice(2, equalIndex);
|
|
244
264
|
const value = arg.slice(equalIndex + 1);
|
|
245
|
-
|
|
265
|
+
const normalizedKey = this.normalizeKey(key);
|
|
266
|
+
flags[normalizedKey] = stringFlags.has(normalizedKey) ? value : this.parseValue(value);
|
|
246
267
|
}
|
|
247
268
|
else if (arg.startsWith('--no-')) {
|
|
248
269
|
// --no-flag (boolean negation)
|
|
@@ -266,7 +287,7 @@ export class CommandParser {
|
|
|
266
287
|
}
|
|
267
288
|
}
|
|
268
289
|
else if (nextIndex < args.length && this.isFlagValue(args[nextIndex])) {
|
|
269
|
-
flags[normalizedKey] = this.parseValue(args[nextIndex]);
|
|
290
|
+
flags[normalizedKey] = stringFlags.has(normalizedKey) ? args[nextIndex] : this.parseValue(args[nextIndex]);
|
|
270
291
|
nextIndex++;
|
|
271
292
|
}
|
|
272
293
|
else {
|
|
@@ -293,7 +314,7 @@ export class CommandParser {
|
|
|
293
314
|
}
|
|
294
315
|
}
|
|
295
316
|
else if (nextIndex < args.length && this.isFlagValue(args[nextIndex])) {
|
|
296
|
-
flags[normalizedKey] = this.parseValue(args[nextIndex]);
|
|
317
|
+
flags[normalizedKey] = stringFlags.has(normalizedKey) ? args[nextIndex] : this.parseValue(args[nextIndex]);
|
|
297
318
|
nextIndex++;
|
|
298
319
|
}
|
|
299
320
|
else {
|
|
@@ -439,6 +460,36 @@ export class CommandParser {
|
|
|
439
460
|
}
|
|
440
461
|
return flags;
|
|
441
462
|
}
|
|
463
|
+
/** Resolve declared types from broad to narrow without borrowing string
|
|
464
|
+
* declarations from unrelated registered commands. */
|
|
465
|
+
getFlagScope(commands) {
|
|
466
|
+
const aliases = this.buildScopedAliases(commands[commands.length - 1]);
|
|
467
|
+
const booleanFlags = this.getScopedBooleanFlags(commands[commands.length - 1]);
|
|
468
|
+
const stringFlags = new Set((this.options.stringFlags ?? []).map(flag => this.normalizeKey(flag)));
|
|
469
|
+
for (const command of commands) {
|
|
470
|
+
for (const option of command.options ?? []) {
|
|
471
|
+
if (option.short)
|
|
472
|
+
aliases[option.short] = option.name;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const layers = [this.globalOptions, ...commands.map(command => command.options ?? [])];
|
|
476
|
+
for (const options of layers) {
|
|
477
|
+
for (const option of options) {
|
|
478
|
+
const key = this.normalizeKey(option.name);
|
|
479
|
+
if (option.type === 'string')
|
|
480
|
+
stringFlags.add(key);
|
|
481
|
+
else if (option.type)
|
|
482
|
+
stringFlags.delete(key);
|
|
483
|
+
if (option.type === 'boolean')
|
|
484
|
+
booleanFlags.add(key);
|
|
485
|
+
else if (option.type)
|
|
486
|
+
booleanFlags.delete(key);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
for (const flag of stringFlags)
|
|
490
|
+
booleanFlags.delete(flag);
|
|
491
|
+
return { aliases, booleanFlags, stringFlags };
|
|
492
|
+
}
|
|
442
493
|
getBooleanFlags() {
|
|
443
494
|
const flags = new Set();
|
|
444
495
|
for (const opt of this.globalOptions) {
|
|
@@ -243,6 +243,7 @@ export class MonitoringHooks {
|
|
|
243
243
|
* Get metrics for a specific name
|
|
244
244
|
*/
|
|
245
245
|
getMetrics(name, since) {
|
|
246
|
+
this.expireMetrics();
|
|
246
247
|
let filtered = this.metrics.filter(m => m.name === name);
|
|
247
248
|
if (since) {
|
|
248
249
|
filtered = filtered.filter(m => m.timestamp >= since);
|
|
@@ -253,6 +254,7 @@ export class MonitoringHooks {
|
|
|
253
254
|
* Get all metrics summary
|
|
254
255
|
*/
|
|
255
256
|
getMetricsSummary() {
|
|
257
|
+
this.expireMetrics();
|
|
256
258
|
const summary = {};
|
|
257
259
|
for (const metric of this.metrics) {
|
|
258
260
|
if (!summary[metric.name]) {
|
|
@@ -320,11 +322,14 @@ export class MonitoringHooks {
|
|
|
320
322
|
}
|
|
321
323
|
}
|
|
322
324
|
}
|
|
325
|
+
/** Drop metrics older than retentionMs (also applied on reads, #3855). */
|
|
326
|
+
expireMetrics() {
|
|
327
|
+
const cutoff = Date.now() - this.config.retentionMs;
|
|
328
|
+
this.metrics = this.metrics.filter(m => m.timestamp > cutoff);
|
|
329
|
+
}
|
|
323
330
|
cleanupMetrics() {
|
|
324
|
-
const now = Date.now();
|
|
325
|
-
const cutoff = now - this.config.retentionMs;
|
|
326
331
|
// Remove old metrics
|
|
327
|
-
this.
|
|
332
|
+
this.expireMetrics();
|
|
328
333
|
// Limit total metrics
|
|
329
334
|
if (this.metrics.length > this.config.maxMetrics) {
|
|
330
335
|
this.metrics = this.metrics.slice(-this.config.maxMetrics);
|
|
@@ -41,12 +41,15 @@ export class RateLimiter {
|
|
|
41
41
|
const limits = this.getLimits(operation);
|
|
42
42
|
const now = Date.now();
|
|
43
43
|
// Get or create bucket
|
|
44
|
-
const
|
|
44
|
+
const trackedUser = userId && this.config.perUserLimits ? userId : undefined;
|
|
45
|
+
const bucketKey = trackedUser
|
|
45
46
|
? `${operation}:${userId}`
|
|
46
47
|
: `global:${operation}`;
|
|
47
48
|
let bucket = this.buckets.get(bucketKey);
|
|
48
49
|
if (!bucket) {
|
|
49
50
|
bucket = this.createBucket();
|
|
51
|
+
bucket.operation = operation;
|
|
52
|
+
bucket.userId = trackedUser;
|
|
50
53
|
this.buckets.set(bucketKey, bucket);
|
|
51
54
|
this.cleanupBuckets();
|
|
52
55
|
}
|
|
@@ -133,10 +136,10 @@ export class RateLimiter {
|
|
|
133
136
|
getStats() {
|
|
134
137
|
const operationCounts = new Map();
|
|
135
138
|
const users = new Set();
|
|
136
|
-
for (const
|
|
137
|
-
const
|
|
138
|
-
if (userId)
|
|
139
|
-
users.add(userId);
|
|
139
|
+
for (const bucket of this.buckets.values()) {
|
|
140
|
+
const operation = bucket.operation;
|
|
141
|
+
if (bucket.userId)
|
|
142
|
+
users.add(bucket.userId);
|
|
140
143
|
const current = operationCounts.get(operation) || 0;
|
|
141
144
|
operationCounts.set(operation, current + bucket.requests.length);
|
|
142
145
|
}
|
|
@@ -54,6 +54,8 @@ function calculateDelay(attempt, config, strategy = 'exponential') {
|
|
|
54
54
|
const jitterRange = delay * config.jitter;
|
|
55
55
|
delay += (Math.random() - 0.5) * 2 * jitterRange;
|
|
56
56
|
}
|
|
57
|
+
// Enforce the configured maximum on the final (jittered) delay
|
|
58
|
+
delay = Math.min(delay, config.maxDelayMs);
|
|
57
59
|
return Math.round(Math.max(0, delay));
|
|
58
60
|
}
|
|
59
61
|
/**
|
|
@@ -114,7 +116,7 @@ export async function withRetry(fn, config = {}, strategy = 'exponential') {
|
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
118
|
catch (error) {
|
|
117
|
-
lastError = error;
|
|
119
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
118
120
|
// Check if we should retry
|
|
119
121
|
if (!shouldRetryError(lastError, attempt, finalConfig)) {
|
|
120
122
|
return {
|
|
@@ -20,6 +20,9 @@ export async function runBoundedPool(tasks, options) {
|
|
|
20
20
|
const controller = new AbortController();
|
|
21
21
|
const onAbort = () => controller.abort(options.signal?.reason ?? new Error('cancelled'));
|
|
22
22
|
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
23
|
+
// Abort events are not replayed for signals cancelled before registration.
|
|
24
|
+
if (options.signal?.aborted)
|
|
25
|
+
onAbort();
|
|
23
26
|
const timer = options.timeoutMs && options.timeoutMs > 0
|
|
24
27
|
? setTimeout(() => controller.abort(new Error('worker-pool-timeout')), options.timeoutMs)
|
|
25
28
|
: undefined;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Shared JSON config file persistence with atomic writes and Zod validation
|
|
4
4
|
*/
|
|
5
5
|
export declare class ConfigFileManager {
|
|
6
|
+
private configCwd;
|
|
6
7
|
private configPath;
|
|
7
8
|
private config;
|
|
8
9
|
/** Find config file in search paths starting from cwd */
|
|
@@ -27,6 +28,8 @@ export declare class ConfigFileManager {
|
|
|
27
28
|
getConfigPath(): string | null;
|
|
28
29
|
/** Get default config */
|
|
29
30
|
getDefaults(): Record<string, unknown>;
|
|
31
|
+
/** Cached values and file paths belong to one resolved project directory. */
|
|
32
|
+
private selectProject;
|
|
30
33
|
/** Atomic write: write to .tmp then rename */
|
|
31
34
|
private writeAtomic;
|
|
32
35
|
}
|
|
@@ -54,6 +54,7 @@ const DEFAULT_CONFIG = {
|
|
|
54
54
|
},
|
|
55
55
|
};
|
|
56
56
|
export class ConfigFileManager {
|
|
57
|
+
configCwd = null;
|
|
57
58
|
configPath = null;
|
|
58
59
|
config = null;
|
|
59
60
|
/** Find config file in search paths starting from cwd */
|
|
@@ -73,6 +74,7 @@ export class ConfigFileManager {
|
|
|
73
74
|
}
|
|
74
75
|
/** Load config from file, returns null if not found */
|
|
75
76
|
load(cwd) {
|
|
77
|
+
this.selectProject(cwd);
|
|
76
78
|
this.configPath = this.findConfig(cwd);
|
|
77
79
|
if (!this.configPath) {
|
|
78
80
|
this.config = null;
|
|
@@ -101,10 +103,11 @@ export class ConfigFileManager {
|
|
|
101
103
|
}
|
|
102
104
|
/** Get the current config, loading if needed */
|
|
103
105
|
getConfig(cwd) {
|
|
106
|
+
this.selectProject(cwd);
|
|
104
107
|
if (this.config === null) {
|
|
105
108
|
this.load(cwd);
|
|
106
109
|
}
|
|
107
|
-
return this.config ??
|
|
110
|
+
return this.config ?? structuredClone(DEFAULT_CONFIG);
|
|
108
111
|
}
|
|
109
112
|
/** Get a nested config value by dot-separated key */
|
|
110
113
|
get(cwd, key) {
|
|
@@ -117,6 +120,7 @@ export class ConfigFileManager {
|
|
|
117
120
|
if (key.split('.').some(part => part === '__proto__' || part === 'constructor' || part === 'prototype')) {
|
|
118
121
|
throw new Error(`Unsafe configuration key: ${key}`);
|
|
119
122
|
}
|
|
123
|
+
this.selectProject(cwd);
|
|
120
124
|
// A targeted update must not persist unrelated defaults (notably the
|
|
121
125
|
// default memory path, which would relocate an existing memory store).
|
|
122
126
|
const config = this.config ?? this.load(cwd) ?? {};
|
|
@@ -137,17 +141,20 @@ export class ConfigFileManager {
|
|
|
137
141
|
if (fs.existsSync(targetPath) && !force) {
|
|
138
142
|
throw new Error(`Config file already exists: ${targetPath}. Use --force to overwrite.`);
|
|
139
143
|
}
|
|
140
|
-
const config = { ...DEFAULT_CONFIG, ...overrides };
|
|
144
|
+
const config = { ...structuredClone(DEFAULT_CONFIG), ...overrides };
|
|
141
145
|
this.writeAtomic(targetPath, config);
|
|
146
|
+
this.selectProject(cwd);
|
|
142
147
|
this.config = config;
|
|
143
148
|
this.configPath = targetPath;
|
|
144
149
|
return targetPath;
|
|
145
150
|
}
|
|
146
151
|
/** Reset config to defaults */
|
|
147
152
|
reset(cwd) {
|
|
148
|
-
|
|
149
|
-
this.
|
|
150
|
-
|
|
153
|
+
this.selectProject(cwd);
|
|
154
|
+
const targetPath = this.findConfig(cwd) ?? path.resolve(cwd, CONFIG_FILENAMES[0]);
|
|
155
|
+
const config = structuredClone(DEFAULT_CONFIG);
|
|
156
|
+
this.writeAtomic(targetPath, config);
|
|
157
|
+
this.config = config;
|
|
151
158
|
this.configPath = targetPath;
|
|
152
159
|
return targetPath;
|
|
153
160
|
}
|
|
@@ -174,7 +181,8 @@ export class ConfigFileManager {
|
|
|
174
181
|
if (typeof imported !== 'object' || imported === null || Array.isArray(imported)) {
|
|
175
182
|
throw new Error('Import file must contain a JSON object');
|
|
176
183
|
}
|
|
177
|
-
|
|
184
|
+
this.selectProject(cwd);
|
|
185
|
+
const targetPath = this.findConfig(cwd) ?? path.resolve(cwd, CONFIG_FILENAMES[0]);
|
|
178
186
|
this.writeAtomic(targetPath, imported);
|
|
179
187
|
this.config = imported;
|
|
180
188
|
this.configPath = targetPath;
|
|
@@ -185,7 +193,16 @@ export class ConfigFileManager {
|
|
|
185
193
|
}
|
|
186
194
|
/** Get default config */
|
|
187
195
|
getDefaults() {
|
|
188
|
-
return
|
|
196
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
197
|
+
}
|
|
198
|
+
/** Cached values and file paths belong to one resolved project directory. */
|
|
199
|
+
selectProject(cwd) {
|
|
200
|
+
const resolved = path.resolve(cwd);
|
|
201
|
+
if (this.configCwd !== resolved) {
|
|
202
|
+
this.configCwd = resolved;
|
|
203
|
+
this.config = null;
|
|
204
|
+
this.configPath = null;
|
|
205
|
+
}
|
|
189
206
|
}
|
|
190
207
|
/** Atomic write: write to .tmp then rename */
|
|
191
208
|
writeAtomic(filePath, data) {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { createHash } from 'crypto';
|
|
25
25
|
import { resolve } from 'path';
|
|
26
26
|
import * as fs from 'fs';
|
|
27
|
-
import { safeGitTextSync } from '@claude-flow/security/safe-git';
|
|
27
|
+
import { safeGitBufferSync, safeGitTextSync } from '@claude-flow/security/safe-git';
|
|
28
28
|
const GIT_TIMEOUT_MS = 3000;
|
|
29
29
|
function git(cwd, ...args) {
|
|
30
30
|
try {
|
|
@@ -37,6 +37,23 @@ function git(cwd, ...args) {
|
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Path-valued git output: git terminates the record with a single LF, so strip
|
|
42
|
+
* only that. A full trim() would corrupt valid paths ending in whitespace.
|
|
43
|
+
*/
|
|
44
|
+
function gitPath(cwd, ...args) {
|
|
45
|
+
try {
|
|
46
|
+
const out = safeGitBufferSync(cwd, args, {
|
|
47
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
48
|
+
stderr: 'ignore',
|
|
49
|
+
}).toString('utf8');
|
|
50
|
+
const rec = out.endsWith('\n') ? out.slice(0, -1) : out;
|
|
51
|
+
return rec.length > 0 ? rec : null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
40
57
|
function sha256(input) {
|
|
41
58
|
return createHash('sha256').update(input).digest('hex');
|
|
42
59
|
}
|
|
@@ -50,7 +67,7 @@ export function resolveGitWorkspaceIdentity(dir) {
|
|
|
50
67
|
const resolved = resolve(dir);
|
|
51
68
|
let stable = identityCache.get(resolved);
|
|
52
69
|
if (!stable) {
|
|
53
|
-
const worktreeRoot =
|
|
70
|
+
const worktreeRoot = gitPath(resolved, 'rev-parse', '--show-toplevel');
|
|
54
71
|
if (!worktreeRoot) {
|
|
55
72
|
stable = {
|
|
56
73
|
worktreeRoot: resolved,
|
|
@@ -63,7 +80,7 @@ export function resolveGitWorkspaceIdentity(dir) {
|
|
|
63
80
|
else {
|
|
64
81
|
// --git-common-dir may be relative to the worktree root (git < 2.31
|
|
65
82
|
// and some invocation contexts) — resolve against it.
|
|
66
|
-
const rawCommon =
|
|
83
|
+
const rawCommon = gitPath(worktreeRoot, 'rev-parse', '--git-common-dir') ?? '.git';
|
|
67
84
|
const commonGitDir = resolve(worktreeRoot, rawCommon);
|
|
68
85
|
stable = {
|
|
69
86
|
worktreeRoot,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PolicyState } from '@claude-flow/security';
|
|
2
|
+
export type AnchorEvent = 'append' | 'migrated-from-state' | 'establish-anchor';
|
|
3
|
+
export interface AnchorEntry {
|
|
4
|
+
seq: number;
|
|
5
|
+
length: number;
|
|
6
|
+
headHash: string | null;
|
|
7
|
+
prevAnchorHash: string | null;
|
|
8
|
+
ts: number;
|
|
9
|
+
event: AnchorEvent;
|
|
10
|
+
by?: string;
|
|
11
|
+
hash: string;
|
|
12
|
+
}
|
|
13
|
+
export type AnchorAssessment = {
|
|
14
|
+
kind: 'absent';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'ok';
|
|
17
|
+
last: AnchorEntry;
|
|
18
|
+
behind: boolean;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'fail';
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
export type AnchorWriter = (file: string, value: unknown) => Promise<void>;
|
|
24
|
+
export declare const anchorLogPath: (projectRoot: string) => string;
|
|
25
|
+
/**
|
|
26
|
+
* How the stored anchors relate to `state`; read-only. `rebuild` is the
|
|
27
|
+
* explicit-establish path: an unreadable log or mirror is treated as absent, but
|
|
28
|
+
* whatever readable anchor remains must still agree with the chain, so it can
|
|
29
|
+
* never bless a truncation the anchors still remember.
|
|
30
|
+
*/
|
|
31
|
+
export declare function assessAnchors(projectRoot: string, state: PolicyState, rebuild?: boolean): AnchorAssessment;
|
|
32
|
+
/**
|
|
33
|
+
* Append an entry for the current state if the newest entry does not already
|
|
34
|
+
* describe it. Must run under the policy lock. Throws if the log is corrupt:
|
|
35
|
+
* only an explicit establish may replace a corrupt log.
|
|
36
|
+
*/
|
|
37
|
+
export declare function recordAnchor(projectRoot: string, state: PolicyState, write: AnchorWriter, event?: AnchorEvent, by?: string): Promise<void>;
|
|
38
|
+
export declare function anchorPosture(projectRoot: string): {
|
|
39
|
+
log: boolean;
|
|
40
|
+
mirror: boolean;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=policy-ledger-anchor.d.ts.map
|