@claude-flow/cli 3.68.0 → 3.70.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 +2 -2
- package/.claude/skills/agentdb-advanced/SKILL.md +1 -1
- package/.claude/skills/agentdb-learning/SKILL.md +1 -1
- package/.claude/skills/agentdb-memory-patterns/SKILL.md +1 -1
- package/.claude/skills/agentdb-optimization/SKILL.md +1 -1
- package/.claude/skills/agentdb-vector-search/SKILL.md +1 -1
- package/.claude/skills/hooks-automation/SKILL.md +1 -1
- package/.claude/skills/pair-programming/SKILL.md +1 -1
- package/.claude/skills/reasoningbank-agentdb/SKILL.md +1 -1
- package/.claude/skills/reasoningbank-intelligence/SKILL.md +1 -1
- package/.claude/skills/skill-builder/SKILL.md +1 -1
- package/.claude/skills/swarm-orchestration/SKILL.md +1 -1
- package/.claude/skills/v3-cli-modernization/SKILL.md +1 -1
- package/.claude/skills/v3-core-implementation/SKILL.md +1 -1
- package/.claude/skills/v3-ddd-architecture/SKILL.md +1 -1
- package/.claude/skills/v3-integration-deep/SKILL.md +1 -1
- package/.claude/skills/v3-mcp-optimization/SKILL.md +1 -1
- package/.claude/skills/v3-memory-unification/SKILL.md +1 -1
- package/.claude/skills/v3-performance-optimization/SKILL.md +1 -1
- package/.claude/skills/v3-security-overhaul/SKILL.md +1 -1
- package/.claude/skills/v3-swarm-coordination/SKILL.md +1 -1
- package/.claude/skills/verification-quality/SKILL.md +1 -1
- package/catalog-manifest.json +3 -3
- package/dist/src/commands/context-plan.d.ts +4 -0
- package/dist/src/commands/context-plan.js +36 -0
- package/dist/src/commands/hooks.js +20 -4
- package/dist/src/commands/workflow.js +3 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +3 -0
- package/dist/src/mcp-tools/agent-tools.js +31 -14
- package/dist/src/mcp-tools/agentdb-episode-tools.d.ts +16 -0
- package/dist/src/mcp-tools/agentdb-episode-tools.js +214 -0
- package/dist/src/mcp-tools/agentdb-tools.js +2 -0
- package/dist/src/mcp-tools/claims-tools.js +7 -3
- package/dist/src/mcp-tools/config-tools.js +48 -22
- package/dist/src/mcp-tools/coordination-tools.js +5 -2
- package/dist/src/mcp-tools/daa-tools.js +12 -5
- package/dist/src/mcp-tools/hooks-tools.js +46 -7
- package/dist/src/mcp-tools/session-tools.d.ts +10 -0
- package/dist/src/mcp-tools/session-tools.js +15 -2
- package/dist/src/mcp-tools/workflow-tools.js +9 -4
- package/dist/src/memory/episode-crud.d.ts +70 -0
- package/dist/src/memory/episode-crud.js +114 -0
- package/dist/src/memory/ewc-consolidation.d.ts +7 -0
- package/dist/src/memory/ewc-consolidation.js +19 -1
- package/dist/src/memory/memory-bridge.d.ts +5 -0
- package/dist/src/memory/memory-bridge.js +38 -3
- package/dist/src/mods/command-registry/schema.d.ts +6 -6
- package/dist/src/services/anthropic-context-adapter.d.ts +81 -0
- package/dist/src/services/anthropic-context-adapter.js +257 -0
- package/dist/src/services/context-delegation.d.ts +107 -0
- package/dist/src/services/context-delegation.js +193 -0
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.d.ts +1 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.js +8 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.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/entities/trust-level.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/entities/trust-level.js +9 -2
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/entities/trust-level.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
- package/package.json +5 -5
- package/.claude/memory.db +0 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentdb_episode-get / -list / -update / -delete (ADR-520).
|
|
3
|
+
*
|
|
4
|
+
* Episodes are the records session ends and `memory distill` write and that
|
|
5
|
+
* agentdb_episode-recall searches. Needs agentdb >= 3.3.0 (in-place update that keeps the id
|
|
6
|
+
* and re-indexes); older builds get a clear error instead of a partial operation.
|
|
7
|
+
*
|
|
8
|
+
* @module v3/cli/mcp-tools/agentdb-episode-tools
|
|
9
|
+
*/
|
|
10
|
+
import { validateText } from './validate-input.js';
|
|
11
|
+
const TEXT_MAX = 50_000;
|
|
12
|
+
const METADATA_MAX_BYTES = 64 * 1024;
|
|
13
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
14
|
+
let crud = null;
|
|
15
|
+
async function episodeCrud() {
|
|
16
|
+
crud ??= await import('../memory/episode-crud.js');
|
|
17
|
+
return crud;
|
|
18
|
+
}
|
|
19
|
+
const unavailable = { success: false, error: 'AgentDB ReflexionMemory not available. Use memory_* tools instead.' };
|
|
20
|
+
function idOf(value, parse) {
|
|
21
|
+
const id = parse(value);
|
|
22
|
+
return id ?? { error: 'id must be a positive integer' };
|
|
23
|
+
}
|
|
24
|
+
function hasForbiddenKey(value, depth = 0) {
|
|
25
|
+
if (depth > 16)
|
|
26
|
+
return true;
|
|
27
|
+
if (!value || typeof value !== 'object')
|
|
28
|
+
return false;
|
|
29
|
+
for (const [k, v] of Object.entries(value)) {
|
|
30
|
+
if (FORBIDDEN_KEYS.has(k) || hasForbiddenKey(v, depth + 1))
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
/** Optional text field: undefined = leave alone, null = clear, string = set. */
|
|
36
|
+
function optionalText(value, name, nullable) {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return {};
|
|
39
|
+
if (value === null)
|
|
40
|
+
return nullable ? { value: null } : { error: `${name} cannot be null` };
|
|
41
|
+
const v = validateText(value, name, TEXT_MAX);
|
|
42
|
+
if (!v.valid)
|
|
43
|
+
return { error: v.error };
|
|
44
|
+
if (!nullable && !v.sanitized.trim())
|
|
45
|
+
return { error: `${name} cannot be empty` };
|
|
46
|
+
return { value: v.sanitized };
|
|
47
|
+
}
|
|
48
|
+
export const agentdbEpisodeGet = {
|
|
49
|
+
name: 'agentdb_episode-get',
|
|
50
|
+
description: 'Get one past episode (a session summary or distilled trajectory) by id. Use when you already have an episode id from agentdb_episode-recall or agentdb_episode-list and need its full record; memory_retrieve is wrong here because episodes are not key-value entries. Returns episode: null when missing. Needs agentdb >= 3.3.0.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: { id: { type: 'number', description: 'Episode id' } },
|
|
54
|
+
required: ['id'],
|
|
55
|
+
},
|
|
56
|
+
handler: async (params) => {
|
|
57
|
+
try {
|
|
58
|
+
const m = await episodeCrud();
|
|
59
|
+
const id = idOf(params.id, m.parseEpisodeId);
|
|
60
|
+
if (typeof id !== 'number')
|
|
61
|
+
return { success: false, ...id };
|
|
62
|
+
return (await m.bridgeGetEpisode(id)) ?? unavailable;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return { success: false, error: error instanceof Error ? error.message.slice(0, 500) : 'Internal error' };
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
export const agentdbEpisodeList = {
|
|
70
|
+
name: 'agentdb_episode-list',
|
|
71
|
+
description: 'Page through past episodes, newest first, optionally for one session or only successes/failures, with a total count. Use when you need to browse or audit what was recorded (e.g. every episode from one session); use agentdb_episode-recall instead when you want the episodes most relevant to a task. Needs agentdb >= 3.3.0.',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
sessionId: { type: 'string', description: 'Only episodes from this session' },
|
|
76
|
+
success: { type: 'boolean', description: 'Only successes (true) or failures (false)' },
|
|
77
|
+
limit: { type: 'number', description: 'Page size (1-100)', default: 20 },
|
|
78
|
+
offset: { type: 'number', description: 'Episodes to skip', default: 0 },
|
|
79
|
+
order: { type: 'string', enum: ['newest', 'oldest'], default: 'newest' },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
handler: async (params) => {
|
|
83
|
+
try {
|
|
84
|
+
if (params.sessionId !== undefined) {
|
|
85
|
+
const v = validateText(params.sessionId, 'sessionId', 256);
|
|
86
|
+
if (!v.valid)
|
|
87
|
+
return { success: false, error: v.error };
|
|
88
|
+
}
|
|
89
|
+
if (params.success !== undefined && typeof params.success !== 'boolean') {
|
|
90
|
+
return { success: false, error: 'success must be a boolean' };
|
|
91
|
+
}
|
|
92
|
+
const limit = params.limit === undefined ? 20 : Number(params.limit);
|
|
93
|
+
const offset = params.offset === undefined ? 0 : Number(params.offset);
|
|
94
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
95
|
+
return { success: false, error: 'limit must be an integer 1-100' };
|
|
96
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
97
|
+
return { success: false, error: 'offset must be a non-negative integer' };
|
|
98
|
+
if (params.order !== undefined && params.order !== 'newest' && params.order !== 'oldest') {
|
|
99
|
+
return { success: false, error: 'order must be "newest" or "oldest"' };
|
|
100
|
+
}
|
|
101
|
+
const m = await episodeCrud();
|
|
102
|
+
return (await m.bridgeListEpisodes({
|
|
103
|
+
sessionId: params.sessionId,
|
|
104
|
+
success: params.success,
|
|
105
|
+
limit,
|
|
106
|
+
offset,
|
|
107
|
+
order: params.order ?? 'newest',
|
|
108
|
+
})) ?? unavailable;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return { success: false, error: error instanceof Error ? error.message.slice(0, 500) : 'Internal error' };
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
export const agentdbEpisodeUpdate = {
|
|
116
|
+
name: 'agentdb_episode-update',
|
|
117
|
+
description: 'Correct a past episode in place, keeping its id: task/critique/output text, reward, success flag, tags or metadata. Use when an outcome has been verified or an episode turned out wrong (e.g. success=false with a critique); deleting and re-recording is wrong because it changes the id. Text changes are re-embedded and re-indexed so recall sees them; metadata merges unless replaceMetadata is true. Needs agentdb >= 3.3.0.',
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
id: { type: 'number', description: 'Episode id' },
|
|
122
|
+
task: { type: 'string', description: 'New task text' },
|
|
123
|
+
critique: { type: ['string', 'null'], description: 'New critique, or null to clear' },
|
|
124
|
+
output: { type: ['string', 'null'], description: 'New output, or null to clear' },
|
|
125
|
+
reward: { type: 'number', description: 'New reward (0-1)' },
|
|
126
|
+
success: { type: 'boolean', description: 'New success flag' },
|
|
127
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Replacement tag list' },
|
|
128
|
+
metadata: { type: 'object', description: 'Metadata patch' },
|
|
129
|
+
replaceMetadata: { type: 'boolean', description: 'Replace metadata instead of merging', default: false },
|
|
130
|
+
},
|
|
131
|
+
required: ['id'],
|
|
132
|
+
},
|
|
133
|
+
handler: async (params) => {
|
|
134
|
+
try {
|
|
135
|
+
const allowed = new Set(['id', 'task', 'critique', 'output', 'reward', 'success', 'tags', 'metadata', 'replaceMetadata']);
|
|
136
|
+
const unknown = Object.keys(params).filter(k => !allowed.has(k));
|
|
137
|
+
if (unknown.length)
|
|
138
|
+
return { success: false, error: `unknown field(s): ${unknown.join(', ')}` };
|
|
139
|
+
const m = await episodeCrud();
|
|
140
|
+
const id = idOf(params.id, m.parseEpisodeId);
|
|
141
|
+
if (typeof id !== 'number')
|
|
142
|
+
return { success: false, ...id };
|
|
143
|
+
const patch = {};
|
|
144
|
+
for (const [field, nullable] of [['task', false], ['critique', true], ['output', true]]) {
|
|
145
|
+
const r = optionalText(params[field], field, nullable);
|
|
146
|
+
if (r.error)
|
|
147
|
+
return { success: false, error: r.error };
|
|
148
|
+
if (r.value !== undefined)
|
|
149
|
+
patch[field] = r.value;
|
|
150
|
+
}
|
|
151
|
+
if (params.reward !== undefined) {
|
|
152
|
+
if (typeof params.reward !== 'number' || !Number.isFinite(params.reward) || params.reward < 0 || params.reward > 1) {
|
|
153
|
+
return { success: false, error: 'reward must be a number between 0 and 1' };
|
|
154
|
+
}
|
|
155
|
+
patch.reward = params.reward;
|
|
156
|
+
}
|
|
157
|
+
if (params.success !== undefined) {
|
|
158
|
+
if (typeof params.success !== 'boolean')
|
|
159
|
+
return { success: false, error: 'success must be a boolean' };
|
|
160
|
+
patch.success = params.success;
|
|
161
|
+
}
|
|
162
|
+
if (params.tags !== undefined) {
|
|
163
|
+
if (!Array.isArray(params.tags) || params.tags.length > 64 || params.tags.some(t => typeof t !== 'string' || t.length > 128)) {
|
|
164
|
+
return { success: false, error: 'tags must be up to 64 strings of at most 128 characters' };
|
|
165
|
+
}
|
|
166
|
+
patch.tags = params.tags;
|
|
167
|
+
}
|
|
168
|
+
if (params.metadata !== undefined) {
|
|
169
|
+
const md = params.metadata;
|
|
170
|
+
if (md !== null && (typeof md !== 'object' || Array.isArray(md)))
|
|
171
|
+
return { success: false, error: 'metadata must be an object or null' };
|
|
172
|
+
if (md && hasForbiddenKey(md))
|
|
173
|
+
return { success: false, error: 'metadata contains a forbidden key (__proto__, constructor, prototype) or is nested too deeply' };
|
|
174
|
+
if (md && Buffer.byteLength(JSON.stringify(md), 'utf8') > METADATA_MAX_BYTES)
|
|
175
|
+
return { success: false, error: 'metadata exceeds 64 KB' };
|
|
176
|
+
patch.metadata = md;
|
|
177
|
+
}
|
|
178
|
+
if (Object.keys(patch).length === 0)
|
|
179
|
+
return { success: false, error: 'nothing to update' };
|
|
180
|
+
return (await m.bridgeUpdateEpisode(id, patch, { replaceMetadata: params.replaceMetadata === true })) ?? unavailable;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return { success: false, error: error instanceof Error ? error.message.slice(0, 500) : 'Internal error' };
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
export const agentdbEpisodeDelete = {
|
|
188
|
+
name: 'agentdb_episode-delete',
|
|
189
|
+
description: 'Delete a past episode by id, with its embedding and index entries, so recall stops returning it. Use when an episode must not be recalled at all (wrong, duplicate or sensitive); prefer agentdb_episode-update over deleting when it only needs correcting. Irreversible. Needs agentdb >= 3.3.0.',
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties: { id: { type: 'number', description: 'Episode id' } },
|
|
193
|
+
required: ['id'],
|
|
194
|
+
},
|
|
195
|
+
handler: async (params) => {
|
|
196
|
+
try {
|
|
197
|
+
const m = await episodeCrud();
|
|
198
|
+
const id = idOf(params.id, m.parseEpisodeId);
|
|
199
|
+
if (typeof id !== 'number')
|
|
200
|
+
return { success: false, ...id };
|
|
201
|
+
return (await m.bridgeDeleteEpisode(id)) ?? unavailable;
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
return { success: false, error: error instanceof Error ? error.message.slice(0, 500) : 'Internal error' };
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
export const agentdbEpisodeCrudTools = [
|
|
209
|
+
agentdbEpisodeGet,
|
|
210
|
+
agentdbEpisodeList,
|
|
211
|
+
agentdbEpisodeUpdate,
|
|
212
|
+
agentdbEpisodeDelete,
|
|
213
|
+
];
|
|
214
|
+
//# sourceMappingURL=agentdb-episode-tools.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { feedbackPatternsSchema, validateFeedbackPatterns } from '../memory/feedback-patterns.js';
|
|
2
2
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
3
|
+
import { agentdbEpisodeCrudTools } from './agentdb-episode-tools.js';
|
|
3
4
|
// ===== Shared validation helpers =====
|
|
4
5
|
const MAX_STRING_LENGTH = 100_000; // 100KB max for any string input
|
|
5
6
|
const MAX_BATCH_SIZE = 500; // Max entries per batch operation
|
|
@@ -1603,6 +1604,7 @@ export const agentdbTools = [
|
|
|
1603
1604
|
agentdbSessionStart,
|
|
1604
1605
|
agentdbSessionEnd,
|
|
1605
1606
|
agentdbEpisodeRecall, // ADR-519
|
|
1607
|
+
...agentdbEpisodeCrudTools, // ADR-520
|
|
1606
1608
|
agentdbHierarchicalStore,
|
|
1607
1609
|
agentdbHierarchicalRecall,
|
|
1608
1610
|
agentdbHierarchicalDelete,
|
|
@@ -220,7 +220,7 @@ export const claimsTools = [
|
|
|
220
220
|
},
|
|
221
221
|
progress: {
|
|
222
222
|
type: 'number',
|
|
223
|
-
description: 'Current progress percentage (0-100)',
|
|
223
|
+
description: 'Current progress percentage (0-100); omit to keep the recorded progress',
|
|
224
224
|
},
|
|
225
225
|
},
|
|
226
226
|
required: ['issueId', 'from', 'to'],
|
|
@@ -230,7 +230,7 @@ export const claimsTools = [
|
|
|
230
230
|
const fromStr = input.from;
|
|
231
231
|
const toStr = input.to;
|
|
232
232
|
const reason = input.reason;
|
|
233
|
-
const progress = input.progress
|
|
233
|
+
const progress = input.progress;
|
|
234
234
|
{
|
|
235
235
|
const v = validateIdentifier(issueId, 'issueId');
|
|
236
236
|
if (!v.valid)
|
|
@@ -269,7 +269,11 @@ export const claimsTools = [
|
|
|
269
269
|
claim.statusChangedAt = now;
|
|
270
270
|
claim.handoffTo = to;
|
|
271
271
|
claim.handoffReason = reason;
|
|
272
|
-
|
|
272
|
+
// Optional: keep the progress recorded with claims_status when it is
|
|
273
|
+
// left out, and clamp it to 0-100 the same way claims_status does.
|
|
274
|
+
if (progress !== undefined) {
|
|
275
|
+
claim.progress = Math.min(100, Math.max(0, progress));
|
|
276
|
+
}
|
|
273
277
|
store.claims[issueId] = claim;
|
|
274
278
|
saveClaims(store);
|
|
275
279
|
return {
|
|
@@ -186,6 +186,30 @@ function filterDangerousKeys(obj) {
|
|
|
186
186
|
}
|
|
187
187
|
return filtered;
|
|
188
188
|
}
|
|
189
|
+
// Scope names index store.scopes directly, so they must never resolve to
|
|
190
|
+
// Object.prototype (`__proto__`) or to inherited built-ins (`toString`).
|
|
191
|
+
function validateScope(scope) {
|
|
192
|
+
const v = validateIdentifier(scope, 'scope');
|
|
193
|
+
if (!v.valid)
|
|
194
|
+
return v;
|
|
195
|
+
if (DANGEROUS_KEYS.has(scope))
|
|
196
|
+
return { valid: false, error: 'scope uses a reserved name' };
|
|
197
|
+
return { valid: true };
|
|
198
|
+
}
|
|
199
|
+
function readScope(store, scope) {
|
|
200
|
+
if (!Object.hasOwn(store.scopes, scope))
|
|
201
|
+
return undefined;
|
|
202
|
+
const values = store.scopes[scope];
|
|
203
|
+
return isRecord(values) ? values : undefined;
|
|
204
|
+
}
|
|
205
|
+
function ensureScope(store, scope, reset = false) {
|
|
206
|
+
const existing = reset ? undefined : readScope(store, scope);
|
|
207
|
+
if (existing)
|
|
208
|
+
return existing;
|
|
209
|
+
const created = {};
|
|
210
|
+
store.scopes[scope] = created;
|
|
211
|
+
return created;
|
|
212
|
+
}
|
|
189
213
|
function setNestedValue(obj, key, value) {
|
|
190
214
|
const MAX_NESTING_DEPTH = 10;
|
|
191
215
|
const parts = key.split('.');
|
|
@@ -226,7 +250,7 @@ export const configTools = [
|
|
|
226
250
|
if (!vKey.valid)
|
|
227
251
|
return { success: false, error: vKey.error };
|
|
228
252
|
if (input.scope) {
|
|
229
|
-
const v =
|
|
253
|
+
const v = validateScope(input.scope);
|
|
230
254
|
if (!v.valid)
|
|
231
255
|
return { success: false, error: v.error };
|
|
232
256
|
}
|
|
@@ -235,8 +259,9 @@ export const configTools = [
|
|
|
235
259
|
const scope = input.scope || 'default';
|
|
236
260
|
let value;
|
|
237
261
|
// Check scope first, then default values
|
|
238
|
-
|
|
239
|
-
|
|
262
|
+
const scopeValues = scope !== 'default' ? readScope(store, scope) : undefined;
|
|
263
|
+
if (scopeValues && Object.hasOwn(scopeValues, key)) {
|
|
264
|
+
value = scopeValues[key];
|
|
240
265
|
}
|
|
241
266
|
if (value === undefined) {
|
|
242
267
|
value = readDefaultValue(store, key);
|
|
@@ -272,7 +297,7 @@ export const configTools = [
|
|
|
272
297
|
if (!vKey.valid)
|
|
273
298
|
return { success: false, error: vKey.error };
|
|
274
299
|
if (input.scope) {
|
|
275
|
-
const v =
|
|
300
|
+
const v = validateScope(input.scope);
|
|
276
301
|
if (!v.valid)
|
|
277
302
|
return { success: false, error: v.error };
|
|
278
303
|
}
|
|
@@ -285,10 +310,10 @@ export const configTools = [
|
|
|
285
310
|
writeDefaultValue(store, key, value);
|
|
286
311
|
}
|
|
287
312
|
else {
|
|
288
|
-
if (
|
|
289
|
-
|
|
313
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
314
|
+
return { success: false, error: 'key uses a reserved name' };
|
|
290
315
|
}
|
|
291
|
-
store
|
|
316
|
+
ensureScope(store, scope)[key] = value;
|
|
292
317
|
}
|
|
293
318
|
saveConfigStore(store);
|
|
294
319
|
return {
|
|
@@ -316,7 +341,7 @@ export const configTools = [
|
|
|
316
341
|
handler: async (input) => {
|
|
317
342
|
// Validate user-provided input (#1425)
|
|
318
343
|
if (input.scope) {
|
|
319
|
-
const v =
|
|
344
|
+
const v = validateScope(input.scope);
|
|
320
345
|
if (!v.valid)
|
|
321
346
|
return { success: false, error: v.error };
|
|
322
347
|
}
|
|
@@ -380,7 +405,7 @@ export const configTools = [
|
|
|
380
405
|
handler: async (input) => {
|
|
381
406
|
// Validate user-provided input (#1425)
|
|
382
407
|
if (input.scope) {
|
|
383
|
-
const v =
|
|
408
|
+
const v = validateScope(input.scope);
|
|
384
409
|
if (!v.valid)
|
|
385
410
|
return { success: false, error: v.error };
|
|
386
411
|
}
|
|
@@ -400,9 +425,12 @@ export const configTools = [
|
|
|
400
425
|
resetKeys.push(key);
|
|
401
426
|
}
|
|
402
427
|
}
|
|
403
|
-
else
|
|
404
|
-
|
|
405
|
-
|
|
428
|
+
else {
|
|
429
|
+
const scopeValues = readScope(store, scope);
|
|
430
|
+
if (scopeValues && Object.hasOwn(scopeValues, key)) {
|
|
431
|
+
delete scopeValues[key];
|
|
432
|
+
resetKeys.push(key);
|
|
433
|
+
}
|
|
406
434
|
}
|
|
407
435
|
}
|
|
408
436
|
else {
|
|
@@ -411,8 +439,8 @@ export const configTools = [
|
|
|
411
439
|
resetKeys = Object.keys(store.values);
|
|
412
440
|
replaceDefaultValues(store, { ...DEFAULT_CONFIG });
|
|
413
441
|
}
|
|
414
|
-
else if (store
|
|
415
|
-
resetKeys = Object.keys(store
|
|
442
|
+
else if (readScope(store, scope)) {
|
|
443
|
+
resetKeys = Object.keys(readScope(store, scope));
|
|
416
444
|
delete store.scopes[scope];
|
|
417
445
|
}
|
|
418
446
|
}
|
|
@@ -440,7 +468,7 @@ export const configTools = [
|
|
|
440
468
|
handler: async (input) => {
|
|
441
469
|
// Validate user-provided input (#1425)
|
|
442
470
|
if (input.scope) {
|
|
443
|
-
const v =
|
|
471
|
+
const v = validateScope(input.scope);
|
|
444
472
|
if (!v.valid)
|
|
445
473
|
return { success: false, error: v.error };
|
|
446
474
|
}
|
|
@@ -452,8 +480,9 @@ export const configTools = [
|
|
|
452
480
|
exportData = { ...DEFAULT_CONFIG };
|
|
453
481
|
}
|
|
454
482
|
Object.assign(exportData, store.values);
|
|
455
|
-
|
|
456
|
-
|
|
483
|
+
const scopeValues = scope !== 'default' ? readScope(store, scope) : undefined;
|
|
484
|
+
if (scopeValues) {
|
|
485
|
+
Object.assign(exportData, scopeValues);
|
|
457
486
|
}
|
|
458
487
|
return {
|
|
459
488
|
config: exportData,
|
|
@@ -480,7 +509,7 @@ export const configTools = [
|
|
|
480
509
|
handler: async (input) => {
|
|
481
510
|
// Validate user-provided input (#1425)
|
|
482
511
|
if (input.scope) {
|
|
483
|
-
const v =
|
|
512
|
+
const v = validateScope(input.scope);
|
|
484
513
|
if (!v.valid)
|
|
485
514
|
return { success: false, error: v.error };
|
|
486
515
|
}
|
|
@@ -499,10 +528,7 @@ export const configTools = [
|
|
|
499
528
|
}
|
|
500
529
|
}
|
|
501
530
|
else {
|
|
502
|
-
|
|
503
|
-
store.scopes[scope] = {};
|
|
504
|
-
}
|
|
505
|
-
Object.assign(store.scopes[scope], config);
|
|
531
|
+
Object.assign(ensureScope(store, scope, !merge), config);
|
|
506
532
|
}
|
|
507
533
|
saveConfigStore(store);
|
|
508
534
|
return {
|
|
@@ -200,8 +200,11 @@ export const coordinationTools = [
|
|
|
200
200
|
selectedNode = nodes.reduce((max, n) => (weights[n.id] || 1) > (weights[max.id] || 1) ? n : max);
|
|
201
201
|
}
|
|
202
202
|
else {
|
|
203
|
-
// Round robin
|
|
204
|
-
|
|
203
|
+
// Round robin: the active node after the one the last task went to,
|
|
204
|
+
// wrapping around (and starting over if that node is gone).
|
|
205
|
+
const last = nodes.findIndex(n => n.id === store.loadBalance.lastAssigned);
|
|
206
|
+
selectedNode = nodes[(last + 1) % nodes.length];
|
|
207
|
+
store.loadBalance.lastAssigned = selectedNode.id;
|
|
205
208
|
}
|
|
206
209
|
// Update load
|
|
207
210
|
selectedNode.load += 1;
|
|
@@ -221,10 +221,15 @@ export const daaTools = [
|
|
|
221
221
|
id,
|
|
222
222
|
name: input.name,
|
|
223
223
|
status: 'pending',
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
// The schema declares object steps; keep their `name` instead of
|
|
225
|
+
// renaming every one to "Step N".
|
|
226
|
+
steps: (input.steps || []).map((s, i) => {
|
|
227
|
+
const given = typeof s === 'string' ? s : s?.name;
|
|
228
|
+
return {
|
|
229
|
+
name: typeof given === 'string' && given.trim() ? given : `Step ${i + 1}`,
|
|
230
|
+
status: 'pending',
|
|
231
|
+
};
|
|
232
|
+
}),
|
|
228
233
|
strategy: input.strategy || 'adaptive',
|
|
229
234
|
createdAt: new Date().toISOString(),
|
|
230
235
|
};
|
|
@@ -323,7 +328,9 @@ export const daaTools = [
|
|
|
323
328
|
const sourceId = input.sourceAgentId;
|
|
324
329
|
const targetIds = input.targetAgentIds;
|
|
325
330
|
const domain = input.knowledgeDomain || 'general';
|
|
326
|
-
|
|
331
|
+
// A random suffix keeps two shares in the same millisecond from
|
|
332
|
+
// overwriting each other in store.knowledge (and in AgentDB).
|
|
333
|
+
const knowledgeId = `knowledge-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
327
334
|
const knowledgeEntry = {
|
|
328
335
|
domain,
|
|
329
336
|
content: input.knowledgeContent || {},
|
|
@@ -5,7 +5,7 @@ import { feedbackPatternsSchema, validateFeedbackPatterns } from '../memory/feed
|
|
|
5
5
|
*/
|
|
6
6
|
import { mkdirSync, writeFileSync, existsSync, readFileSync, statSync, unlinkSync, readdirSync } from 'fs';
|
|
7
7
|
import * as nodeFs from 'fs';
|
|
8
|
-
import { dirname, join, resolve, sep } from 'path';
|
|
8
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'path';
|
|
9
9
|
import { getProjectCwd } from './types.js';
|
|
10
10
|
import { validateIdentifier, validateText, validatePath } from './validate-input.js';
|
|
11
11
|
import { commandRecordingDisabled, redactCommandForStorage } from '../services/command-recording.js';
|
|
@@ -5109,6 +5109,38 @@ function codemodLangForExt(abs) {
|
|
|
5109
5109
|
return 'javascript';
|
|
5110
5110
|
return 'typescript';
|
|
5111
5111
|
}
|
|
5112
|
+
// Canonicalize a path that may not exist yet: realpath the nearest existing
|
|
5113
|
+
// ancestor and re-attach the missing tail, so a symlinked parent still
|
|
5114
|
+
// resolves to its real location (mirrors @claude-flow/security's
|
|
5115
|
+
// canonicalizeSync, which is not exported).
|
|
5116
|
+
function realpathNearest(p) {
|
|
5117
|
+
try {
|
|
5118
|
+
return nodeFs.realpathSync(p);
|
|
5119
|
+
}
|
|
5120
|
+
catch (err) {
|
|
5121
|
+
if (err?.code !== 'ENOENT')
|
|
5122
|
+
throw err;
|
|
5123
|
+
}
|
|
5124
|
+
const parent = dirname(p);
|
|
5125
|
+
if (parent === p)
|
|
5126
|
+
return p;
|
|
5127
|
+
return join(realpathNearest(parent), basename(p));
|
|
5128
|
+
}
|
|
5129
|
+
// CWE-59 (#4346): containment must compare real paths on BOTH sides — a
|
|
5130
|
+
// symlink inside the project pointing outside it must not pass, and a
|
|
5131
|
+
// symlinked project root (e.g. macOS /var -> /private/var) must not make
|
|
5132
|
+
// every path fail. Fails closed if canonicalization throws (ELOOP, EACCES).
|
|
5133
|
+
function isInsideRealRoot(realRoot, abs) {
|
|
5134
|
+
let real;
|
|
5135
|
+
try {
|
|
5136
|
+
real = realpathNearest(abs);
|
|
5137
|
+
}
|
|
5138
|
+
catch {
|
|
5139
|
+
return false;
|
|
5140
|
+
}
|
|
5141
|
+
const rel = relative(realRoot, real);
|
|
5142
|
+
return rel === '' || (rel !== '..' && !rel.startsWith('..' + sep) && !isAbsolute(rel));
|
|
5143
|
+
}
|
|
5112
5144
|
// Deterministic codemod execution — the real Tier-1 path (ADR-143)
|
|
5113
5145
|
export const hooksCodemod = {
|
|
5114
5146
|
name: 'hooks_codemod',
|
|
@@ -5154,6 +5186,13 @@ export const hooksCodemod = {
|
|
|
5154
5186
|
// Resolve the target file set (single / array / glob), with path containment.
|
|
5155
5187
|
const resolveTargets = () => {
|
|
5156
5188
|
const out = new Set();
|
|
5189
|
+
let realRoot;
|
|
5190
|
+
try {
|
|
5191
|
+
realRoot = realpathNearest(cwd);
|
|
5192
|
+
}
|
|
5193
|
+
catch (err) {
|
|
5194
|
+
return { abs: [], truncated: false, error: `cannot resolve project root: ${err.message}` };
|
|
5195
|
+
}
|
|
5157
5196
|
const addRaw = (p) => {
|
|
5158
5197
|
const v = validatePath(p, 'path');
|
|
5159
5198
|
if (!v.valid)
|
|
@@ -5164,13 +5203,11 @@ export const hooksCodemod = {
|
|
|
5164
5203
|
// path like `${cwd}-staging/x.ts` pass as "inside" `cwd`. Matches
|
|
5165
5204
|
// the pattern already used correctly elsewhere in this codebase
|
|
5166
5205
|
// (@claude-flow/security's `isWithinPrefix`, hooks/workers, plugins/security).
|
|
5167
|
-
// KNOWN LIMITATION (disclosed 2026-10-20, not fixed here): this
|
|
5168
|
-
// compares the resolved-but-not-realpath'd path, so a symlink
|
|
5169
|
-
// inside cwd pointing outside it still escapes containment
|
|
5170
|
-
// (CWE-59, a distinct/broader gap than the CWE-22 prefix bug this
|
|
5171
|
-
// anchors). Needs fs.realpathSync-based containment as a follow-up.
|
|
5172
5206
|
if (abs !== cwd && !abs.startsWith(cwd + sep))
|
|
5173
5207
|
return `path escapes project root: ${p}`;
|
|
5208
|
+
// The lexical check above cannot see symlinks; re-check on real paths.
|
|
5209
|
+
if (!isInsideRealRoot(realRoot, abs))
|
|
5210
|
+
return `path escapes project root via symlink: ${p}`;
|
|
5174
5211
|
out.add(abs);
|
|
5175
5212
|
return undefined;
|
|
5176
5213
|
};
|
|
@@ -5202,7 +5239,9 @@ export const hooksCodemod = {
|
|
|
5202
5239
|
}
|
|
5203
5240
|
for (const m of matches) {
|
|
5204
5241
|
const abs = resolve(cwd, m);
|
|
5205
|
-
|
|
5242
|
+
// Matches that resolve outside the root (via a symlinked file or
|
|
5243
|
+
// directory) are skipped silently, like other non-matching entries.
|
|
5244
|
+
if ((abs === cwd || abs.startsWith(cwd + sep)) && isInsideRealRoot(realRoot, abs) && CODEMOD_EXTENSIONS.has(abs.slice(abs.lastIndexOf('.')).toLowerCase())) {
|
|
5206
5245
|
out.add(abs);
|
|
5207
5246
|
}
|
|
5208
5247
|
}
|
|
@@ -19,5 +19,15 @@ export interface MemoryCaptureReport {
|
|
|
19
19
|
};
|
|
20
20
|
error?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* #4404: snapshot map ID for a live (namespace, key) row. A bare
|
|
24
|
+
* `namespace::key` join collides when either part contains `::`
|
|
25
|
+
* (`team::alice`/`profile` vs `team`/`alice::profile`), so one row was
|
|
26
|
+
* dropped on save and a distinct sibling row was skipped as a mirror. `%` and
|
|
27
|
+
* `:` are percent-escaped in each part, which keeps ordinary IDs unchanged.
|
|
28
|
+
* Restore reads the explicit namespace/key fields, never this ID, so older
|
|
29
|
+
* snapshots restore as before.
|
|
30
|
+
*/
|
|
31
|
+
export declare function memorySnapshotId(namespace: string, key: string): string;
|
|
22
32
|
export declare const sessionTools: MCPTool[];
|
|
23
33
|
//# sourceMappingURL=session-tools.d.ts.map
|
|
@@ -115,6 +115,19 @@ function listSessions() {
|
|
|
115
115
|
/** Snapshot format written by session_save since #3573. */
|
|
116
116
|
const MEMORY_SNAPSHOT_FORMAT = 'ruflo-session-memory/2';
|
|
117
117
|
const MEMORY_PAGE_SIZE = 500;
|
|
118
|
+
/**
|
|
119
|
+
* #4404: snapshot map ID for a live (namespace, key) row. A bare
|
|
120
|
+
* `namespace::key` join collides when either part contains `::`
|
|
121
|
+
* (`team::alice`/`profile` vs `team`/`alice::profile`), so one row was
|
|
122
|
+
* dropped on save and a distinct sibling row was skipped as a mirror. `%` and
|
|
123
|
+
* `:` are percent-escaped in each part, which keeps ordinary IDs unchanged.
|
|
124
|
+
* Restore reads the explicit namespace/key fields, never this ID, so older
|
|
125
|
+
* snapshots restore as before.
|
|
126
|
+
*/
|
|
127
|
+
export function memorySnapshotId(namespace, key) {
|
|
128
|
+
const escape = (part) => part.replace(/%/g, '%25').replace(/:/g, '%3A');
|
|
129
|
+
return `${escape(namespace)}::${escape(key)}`;
|
|
130
|
+
}
|
|
118
131
|
async function readLiveEntries(listEntries, dbPath, encryptWrites) {
|
|
119
132
|
const rows = [];
|
|
120
133
|
// Bounded: stop on a short page, once `total` rows are in hand, or after a
|
|
@@ -170,7 +183,7 @@ async function captureMemorySnapshot() {
|
|
|
170
183
|
if (existsSync(primary)) {
|
|
171
184
|
sawStore = true;
|
|
172
185
|
for (const row of await readLiveEntries(listEntries, primary)) {
|
|
173
|
-
const id =
|
|
186
|
+
const id = memorySnapshotId(row.namespace, row.key);
|
|
174
187
|
seen.add(id);
|
|
175
188
|
entries[id] = {
|
|
176
189
|
key: row.key, value: row.content ?? '', namespace: row.namespace,
|
|
@@ -189,7 +202,7 @@ async function captureMemorySnapshot() {
|
|
|
189
202
|
sawStore = true;
|
|
190
203
|
// The AgentDB store is read by native SQLite and must stay plaintext.
|
|
191
204
|
for (const row of await readLiveEntries(listEntries, sibling, false)) {
|
|
192
|
-
const id =
|
|
205
|
+
const id = memorySnapshotId(row.namespace, row.key);
|
|
193
206
|
if (seen.has(id))
|
|
194
207
|
continue;
|
|
195
208
|
seen.add(id);
|
|
@@ -173,11 +173,13 @@ export const workflowTools = [
|
|
|
173
173
|
name: task || `${templateName} workflow`,
|
|
174
174
|
description: task,
|
|
175
175
|
steps,
|
|
176
|
-
|
|
176
|
+
// #4609: nothing is dispatched here, so this is not a run yet. Saving it
|
|
177
|
+
// as 'running' made workflow_execute and workflow_delete refuse it.
|
|
178
|
+
// 'ready' matches workflow_create; workflow_execute starts it.
|
|
179
|
+
status: 'ready',
|
|
177
180
|
currentStep: 0,
|
|
178
181
|
variables: { template: templateName, ...options },
|
|
179
182
|
createdAt: new Date().toISOString(),
|
|
180
|
-
startedAt: new Date().toISOString(),
|
|
181
183
|
};
|
|
182
184
|
store.workflows[workflowId] = workflow;
|
|
183
185
|
saveWorkflowStore(store);
|
|
@@ -185,14 +187,17 @@ export const workflowTools = [
|
|
|
185
187
|
return {
|
|
186
188
|
workflowId,
|
|
187
189
|
template: templateName,
|
|
188
|
-
status: dryRun ? 'validated' : '
|
|
190
|
+
status: dryRun ? 'validated' : 'ready',
|
|
189
191
|
stages,
|
|
190
192
|
metrics: {
|
|
191
193
|
totalStages: stages.length,
|
|
192
194
|
completedStages: 0,
|
|
193
195
|
agentsSpawned: 0,
|
|
194
|
-
estimatedDuration:
|
|
196
|
+
estimatedDuration: 'unknown',
|
|
195
197
|
},
|
|
198
|
+
...(dryRun ? {} : {
|
|
199
|
+
nextStep: 'Start it with workflow_execute; task steps need config.agentId or options.defaultAgentId.',
|
|
200
|
+
}),
|
|
196
201
|
};
|
|
197
202
|
},
|
|
198
203
|
},
|