@claude-flow/cli 3.71.1 → 3.71.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.71.1",
3
+ "version": "3.71.2",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
6
6
  "hook-handler.cjs": "0ce70981aa33f76d008b6f49626effaf4269918c4094f1a28c1ef6fceb097d6f",
@@ -10,6 +10,6 @@
10
10
  "learning-loop.cjs": "d046cd6f8efb7dd40e1d2451516c181fc8b26477aefb1124c0df5f52a23f9190"
11
11
  }
12
12
  },
13
- "signature": "Ocl6MP3QExazQtRc7kRkNgyaKBUY3Y8YBX1mNYxfHzV/K9DHoZMefO89yD6WRsKf8XJUIXtfRUc4J4g45wMlCQ==",
13
+ "signature": "awy0GR0jee+KKCI+7BgzocA29zGZTAZcNYRAwp7IDQ6xwzc6Rby9hqhTnk+68AHde2YvpNpuO8AyqU/P9JDFBw==",
14
14
  "algorithm": "ed25519"
15
15
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-11-04T09:54:05.169Z",
5
- "gitSha": "91e383bd",
4
+ "generatedAt": "2026-11-04T18:54:59.719Z",
5
+ "gitSha": "af95dd71",
6
6
  "catalog": {
7
7
  "agents": 177,
8
8
  "tools": 450,
@@ -121,7 +121,7 @@ const searchCommand = {
121
121
  const threshold = rawThreshold === undefined || rawThreshold === null
122
122
  ? 0.5
123
123
  : parseFloat(String(rawThreshold));
124
- const dbPath = ctx.flags['db-path'] || '.swarm/memory.db';
124
+ const dbPath = ctx.flags.dbPath || '.swarm/memory.db';
125
125
  if (!query) {
126
126
  output.printError('Query is required');
127
127
  return { success: false, exitCode: 1 };
@@ -394,7 +394,7 @@ const collectionsCommand = {
394
394
  ],
395
395
  action: async (ctx) => {
396
396
  const action = ctx.flags.action || 'list';
397
- const dbPath = ctx.flags['db-path'] || '.swarm/memory.db';
397
+ const dbPath = ctx.flags.dbPath || '.swarm/memory.db';
398
398
  output.writeln();
399
399
  output.writeln(output.bold('Embedding Collections (Namespaces)'));
400
400
  output.writeln(output.dim('─'.repeat(60)));
@@ -500,7 +500,7 @@ const indexCommand = {
500
500
  action: async (ctx) => {
501
501
  const action = ctx.flags.action || 'status';
502
502
  const collection = ctx.flags.collection;
503
- const efConstruction = parseInt(ctx.flags['ef-construction'] || '200', 10);
503
+ const efConstruction = parseInt(ctx.flags.efConstruction || '200', 10);
504
504
  const m = parseInt(ctx.flags.m || '16', 10);
505
505
  output.writeln();
506
506
  output.writeln(output.bold(`HNSW Index: ${action}`));
@@ -850,7 +850,7 @@ const chunkCommand = {
850
850
  action: async (ctx) => {
851
851
  const embeddings = await getEmbeddings();
852
852
  const text = ctx.flags.text || '';
853
- const maxSize = parseInt(ctx.flags['max-size'] || '512', 10);
853
+ const maxSize = parseInt(ctx.flags.maxSize || '512', 10);
854
854
  const overlap = parseInt(ctx.flags.overlap || '50', 10);
855
855
  const strategy = ctx.flags.strategy || 'sentence';
856
856
  output.writeln();
@@ -1275,7 +1275,7 @@ const cacheCommand = {
1275
1275
  ],
1276
1276
  action: async (ctx) => {
1277
1277
  const action = ctx.flags.action || 'stats';
1278
- const dbPath = ctx.flags['db-path'] || '.cache/embeddings.db';
1278
+ const dbPath = ctx.flags.dbPath || '.cache/embeddings.db';
1279
1279
  output.writeln();
1280
1280
  output.writeln(output.bold('Embedding Cache'));
1281
1281
  output.writeln(output.dim('─'.repeat(50)));
@@ -1479,7 +1479,7 @@ const benchmarkCommand = {
1479
1479
  ],
1480
1480
  action: async (ctx) => {
1481
1481
  const iterations = parseInt(ctx.flags.iterations || '10', 10);
1482
- const batchSize = parseInt(ctx.flags['batch-size'] || '5', 10);
1482
+ const batchSize = parseInt(ctx.flags.batchSize || '5', 10);
1483
1483
  const full = ctx.flags.full === true;
1484
1484
  output.writeln();
1485
1485
  output.writeln(output.bold('Embedding Performance Benchmark'));
@@ -1091,6 +1091,7 @@ const wizardCommand = {
1091
1091
  { value: 'github', label: 'GitHub', hint: 'GitHub integration skills', selected: true },
1092
1092
  { value: 'flowNexus', label: 'Flow Nexus', hint: 'Cloud platform skills', selected: false },
1093
1093
  { value: 'v3', label: 'V3', hint: 'V3 implementation skills', selected: true },
1094
+ { value: 'legacy', label: 'Legacy', hint: 'Skills that document commands v3 does not ship', selected: false },
1094
1095
  ],
1095
1096
  });
1096
1097
  options.skills.core = skillSets.includes('core');
@@ -1098,6 +1099,7 @@ const wizardCommand = {
1098
1099
  options.skills.github = skillSets.includes('github');
1099
1100
  options.skills.flowNexus = skillSets.includes('flowNexus');
1100
1101
  options.skills.v3 = skillSets.includes('v3');
1102
+ options.skills.legacy = skillSets.includes('legacy');
1101
1103
  }
1102
1104
  // Hooks selection
1103
1105
  if (options.components.settings) {
@@ -1354,6 +1356,7 @@ const skillsCommand = {
1354
1356
  { name: 'agentdb', description: 'Install AgentDB skills', type: 'boolean', default: false },
1355
1357
  { name: 'github', description: 'Install GitHub skills', type: 'boolean', default: false },
1356
1358
  { name: 'v3', description: 'Install V3 skills', type: 'boolean', default: false },
1359
+ { name: 'legacy', description: 'Install skills that document commands v3 does not ship (stream-chain, sparc)', type: 'boolean', default: false },
1357
1360
  ],
1358
1361
  action: async (ctx) => {
1359
1362
  const options = {
@@ -1380,6 +1383,7 @@ const skillsCommand = {
1380
1383
  browser: false,
1381
1384
  v3: ctx.flags.v3,
1382
1385
  dualMode: false,
1386
+ legacy: ctx.flags.legacy,
1383
1387
  },
1384
1388
  };
1385
1389
  const spinner = output.createSpinner({ text: 'Installing skills...' });
@@ -211,9 +211,9 @@ const saveCommand = {
211
211
  const result = await callMCPTool('session_save', {
212
212
  name: sessionName,
213
213
  description,
214
- includeMemory: ctx.flags['include-memory'] !== false,
215
- includeAgents: ctx.flags['include-agents'] !== false,
216
- includeTasks: ctx.flags['include-tasks'] !== false
214
+ includeMemory: ctx.flags.includeMemory !== false && ctx.flags['include-memory'] !== false,
215
+ includeAgents: ctx.flags.includeAgents !== false && ctx.flags['include-agents'] !== false,
216
+ includeTasks: ctx.flags.includeTasks !== false && ctx.flags['include-tasks'] !== false
217
217
  });
218
218
  const failure = toolError(result);
219
219
  if (failure) {
@@ -342,9 +342,12 @@ const restoreCommand = {
342
342
  spinner.start();
343
343
  try {
344
344
  // Determine what to restore
345
- const restoreMemory = !ctx.flags['agents-only'] && !ctx.flags['tasks-only'];
346
- const restoreAgents = !ctx.flags['memory-only'] && !ctx.flags['tasks-only'];
347
- const restoreTasks = !ctx.flags['memory-only'] && !ctx.flags['agents-only'];
345
+ const memoryOnly = ctx.flags.memoryOnly === true || ctx.flags['memory-only'] === true;
346
+ const agentsOnly = ctx.flags.agentsOnly === true || ctx.flags['agents-only'] === true;
347
+ const tasksOnly = ctx.flags.tasksOnly === true || ctx.flags['tasks-only'] === true;
348
+ const restoreMemory = !agentsOnly && !tasksOnly;
349
+ const restoreAgents = !memoryOnly && !tasksOnly;
350
+ const restoreTasks = !memoryOnly && !agentsOnly;
348
351
  const result = await callMCPTool('session_restore', {
349
352
  sessionId,
350
353
  restoreMemory,
@@ -552,7 +555,7 @@ const exportCommand = {
552
555
  try {
553
556
  const result = await callMCPTool('session_export', {
554
557
  sessionId,
555
- includeMemory: ctx.flags['include-memory'] !== false
558
+ includeMemory: ctx.flags.includeMemory !== false && ctx.flags['include-memory'] !== false
556
559
  });
557
560
  const failure = toolError(result);
558
561
  if (failure || result.data === undefined) {
@@ -29,11 +29,9 @@ const SKILLS_MAP = {
29
29
  core: [
30
30
  'swarm-orchestration',
31
31
  'swarm-advanced',
32
- 'sparc-methodology',
33
32
  'hooks-automation',
34
33
  'pair-programming',
35
34
  'verification-quality',
36
- 'stream-chain',
37
35
  'skill-builder',
38
36
  ],
39
37
  browser: ['browser'], // agent-browser integration
@@ -47,12 +45,11 @@ const SKILLS_MAP = {
47
45
  'reasoningbank-agentdb',
48
46
  'reasoningbank-intelligence',
49
47
  ],
48
+ // github-code-review and github-project-management only use hooks, which v3
49
+ // ships; the other three drive `ruflo github`, so they live in skills.legacy.
50
50
  github: [
51
51
  'github-code-review',
52
- 'github-multi-repo',
53
52
  'github-project-management',
54
- 'github-release-management',
55
- 'github-workflow-automation',
56
53
  ],
57
54
  flowNexus: [
58
55
  'flow-nexus-neural',
@@ -70,6 +67,17 @@ const SKILLS_MAP = {
70
67
  'v3-security-overhaul',
71
68
  'v3-swarm-coordination',
72
69
  ],
70
+ // ADR-128 / #4539: skills that document v2-era verbs v3 does not ship
71
+ // (`stream-chain`, `sparc`, `github`). Their examples fail with `Unknown
72
+ // command`, so they are opt-in like skills.v3 instead of being installed by
73
+ // default.
74
+ legacy: [
75
+ 'github-multi-repo',
76
+ 'github-release-management',
77
+ 'github-workflow-automation',
78
+ 'sparc-methodology',
79
+ 'stream-chain',
80
+ ],
73
81
  };
74
82
  /**
75
83
  * Commands to copy based on configuration
@@ -1077,6 +1085,8 @@ async function copySkills(targetDir, options, result) {
1077
1085
  skillsToCopy.push(...SKILLS_MAP.v3);
1078
1086
  if (skillsConfig.dualMode)
1079
1087
  skillsToCopy.push(...SKILLS_MAP.dualMode);
1088
+ if (skillsConfig.legacy)
1089
+ skillsToCopy.push(...SKILLS_MAP.legacy);
1080
1090
  }
1081
1091
  // Find source skills directory
1082
1092
  const sourceSkillsDir = findSourceDir('skills', options.sourceBaseDir);
@@ -76,6 +76,8 @@ export interface SkillsConfig {
76
76
  v3: boolean;
77
77
  /** Include dual-mode skills (Claude Code + Codex hybrid) */
78
78
  dualMode: boolean;
79
+ /** Include legacy skills that document commands v3 does not ship (opt-in) */
80
+ legacy: boolean;
79
81
  /** Include all available skills */
80
82
  all: boolean;
81
83
  }
@@ -73,11 +73,12 @@ export const DEFAULT_INIT_OPTIONS = {
73
73
  skills: {
74
74
  core: true,
75
75
  agentdb: true,
76
- github: true,
76
+ github: true, // #4539: only the hook-only GitHub skills remain; the rest are skills.legacy
77
77
  flowNexus: false,
78
78
  browser: true,
79
79
  v3: false, // ADR-128: opt-in like agents.v3; use --full or `init skills --v3`
80
80
  dualMode: false, // Optional: enable with --dual flag
81
+ legacy: false, // #4539: stream-chain/sparc document verbs v3 does not ship
81
82
  all: false,
82
83
  },
83
84
  commands: {
@@ -186,6 +187,7 @@ export const MINIMAL_INIT_OPTIONS = {
186
187
  browser: false,
187
188
  v3: false,
188
189
  dualMode: false,
190
+ legacy: false,
189
191
  all: false,
190
192
  },
191
193
  agents: {
@@ -246,6 +248,7 @@ export const FULL_INIT_OPTIONS = {
246
248
  browser: true,
247
249
  v3: true,
248
250
  dualMode: true, // Include in full init
251
+ legacy: true, // Include in full init
249
252
  all: true,
250
253
  },
251
254
  commands: {
@@ -402,6 +402,10 @@ export const claimsTools = [
402
402
  if (progress !== undefined) {
403
403
  claim.progress = Math.min(100, Math.max(0, progress));
404
404
  }
405
+ // claims_status never sets 'stealable', so a claim it updates is no
406
+ // longer up for grabs: drop it from the list claims_stealable and
407
+ // claims_steal read.
408
+ delete store.stealable[issueId];
405
409
  store.claims[issueId] = claim;
406
410
  saveClaims(store);
407
411
  return {
@@ -592,7 +596,9 @@ export const claimsTools = [
592
596
  claim.claimant = stealer;
593
597
  claim.status = 'active';
594
598
  claim.statusChangedAt = now;
595
- claim.context = stealableInfo.context;
599
+ // The context given to claims_mark-stealable is the handoff note for the
600
+ // stealer; without one, keep the context recorded with the claim.
601
+ claim.context = stealableInfo.context ?? claim.context;
596
602
  delete store.stealable[issueId];
597
603
  store.claims[issueId] = claim;
598
604
  saveClaims(store);
@@ -28,17 +28,37 @@ function ensureDAADir() {
28
28
  mkdirSync(dir, { recursive: true });
29
29
  }
30
30
  }
31
+ /**
32
+ * SECURITY: record maps are keyed by ids taken from MCP tool input, and
33
+ * validateIdentifier accepts names like `__proto__` and `constructor`. On a
34
+ * plain object those resolve to Object.prototype or a built-in, which a handler
35
+ * would then write into. Null-prototype maps only ever return own records.
36
+ */
37
+ function withRecordMaps(store, keys) {
38
+ const maps = store;
39
+ for (const key of keys) {
40
+ const map = maps[key];
41
+ if (map && typeof map === 'object' && !Array.isArray(map))
42
+ Object.setPrototypeOf(map, null);
43
+ else
44
+ maps[key] = Object.create(null);
45
+ }
46
+ return store;
47
+ }
48
+ const DAA_MAPS = ['agents', 'workflows', 'knowledge'];
31
49
  function loadDAAStore() {
32
50
  try {
33
51
  const path = getDAAPath();
34
52
  if (existsSync(path)) {
35
- return JSON.parse(readFileSync(path, 'utf-8'));
53
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
54
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
55
+ return withRecordMaps(parsed, DAA_MAPS);
36
56
  }
37
57
  }
38
58
  catch {
39
59
  // Return empty store
40
60
  }
41
- return { agents: {}, workflows: {}, knowledge: {}, version: '3.0.0' };
61
+ return withRecordMaps({ agents: {}, workflows: {}, knowledge: {}, version: '3.0.0' }, DAA_MAPS);
42
62
  }
43
63
  function saveDAAStore(store) {
44
64
  ensureDAADir();
@@ -425,7 +425,8 @@ export const embeddingsTools = [
425
425
  }
426
426
  const query = input.query;
427
427
  const topK = input.topK || 5;
428
- const threshold = input.threshold || 0.5;
428
+ // `??`, not `||`: threshold 0 is a valid request for every match, as in memory_search.
429
+ const threshold = input.threshold ?? 0.5;
429
430
  const namespace = input.namespace;
430
431
  {
431
432
  const v = validateText(query, 'query');
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Tool definitions for session management with file persistence.
5
5
  */
6
- import { existsSync, readFileSync, readdirSync, unlinkSync, statSync, writeFileSync } from 'node:fs';
6
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, unlinkSync, statSync, writeFileSync } from 'node:fs';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { gunzipSync } from 'node:zlib';
9
- import { join } from 'node:path';
9
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
10
10
  import { getProjectCwd } from './types.js';
11
11
  import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
12
12
  import { validateIdentifier, validateText } from './validate-input.js';
@@ -351,6 +351,41 @@ function isSessionRecordLike(value) {
351
351
  const v = value;
352
352
  return 'data' in v || 'stats' in v || 'sessionId' in v || 'savedAt' in v;
353
353
  }
354
+ /**
355
+ * SECURITY: `outputPath` comes from MCP tool input. Resolve it against the
356
+ * project root and require the real (symlink-resolved) parent directory to stay
357
+ * inside the real project root, and refuse an existing symlink as the target,
358
+ * so session_export cannot write files elsewhere on the host. The CLI's
359
+ * `session export` writes its own file and does not pass outputPath.
360
+ */
361
+ function containedExportPath(outputPath) {
362
+ if (!outputPath || outputPath.includes('\0'))
363
+ return null;
364
+ try {
365
+ const root = realpathSync(getProjectCwd());
366
+ const abs = resolve(root, outputPath);
367
+ const parent = realpathSync(dirname(abs));
368
+ const target = join(parent, basename(abs));
369
+ const rel = relative(root, target);
370
+ if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
371
+ return null;
372
+ const existing = lstatOrNull(target);
373
+ if (existing && (existing.isSymbolicLink() || !existing.isFile()))
374
+ return null;
375
+ return target;
376
+ }
377
+ catch {
378
+ return null;
379
+ }
380
+ }
381
+ function lstatOrNull(p) {
382
+ try {
383
+ return lstatSync(p);
384
+ }
385
+ catch {
386
+ return null;
387
+ }
388
+ }
354
389
  export const sessionTools = [
355
390
  {
356
391
  name: 'session_save',
@@ -705,9 +740,12 @@ export const sessionTools = [
705
740
  let path = null;
706
741
  const outputPath = input.outputPath ? String(input.outputPath) : null;
707
742
  if (outputPath) {
743
+ const target = containedExportPath(outputPath);
744
+ if (!target)
745
+ return { sessionId, error: 'outputPath must be a file inside the project directory (no symlinks)' };
708
746
  try {
709
- writeFileSync(outputPath, JSON.stringify(session, null, 2), 'utf-8');
710
- path = outputPath;
747
+ writeFileSync(target, JSON.stringify(session, null, 2), 'utf-8');
748
+ path = target;
711
749
  }
712
750
  catch (e) {
713
751
  return { sessionId, error: `Could not write ${outputPath}: ${e.message}` };
@@ -132,6 +132,11 @@ export const terminalTools = [
132
132
  const store = loadTerminalStore();
133
133
  const sessionId = input.sessionId;
134
134
  const command = input.command;
135
+ // An explicit sessionId must name a stored session; only fall back to
136
+ // (or create) the default session when no sessionId was given.
137
+ if (sessionId && !store.sessions[sessionId]) {
138
+ return { success: false, error: 'Session not found' };
139
+ }
135
140
  // Find or create default session
136
141
  let session = sessionId ? store.sessions[sessionId] : Object.values(store.sessions).find(s => s.status === 'active');
137
142
  if (!session) {
@@ -55,18 +55,37 @@ function ensureWorkflowDir() {
55
55
  mkdirSync(dir, { recursive: true });
56
56
  }
57
57
  }
58
+ /**
59
+ * SECURITY: record maps are keyed by ids taken from MCP tool input, and
60
+ * validateIdentifier accepts names like `__proto__` and `constructor`. On a
61
+ * plain object those resolve to Object.prototype or a built-in, which a handler
62
+ * would then write into. Null-prototype maps only ever return own records.
63
+ */
64
+ function withRecordMaps(store, keys) {
65
+ const maps = store;
66
+ for (const key of keys) {
67
+ const map = maps[key];
68
+ if (map && typeof map === 'object' && !Array.isArray(map))
69
+ Object.setPrototypeOf(map, null);
70
+ else
71
+ maps[key] = Object.create(null);
72
+ }
73
+ return store;
74
+ }
75
+ const WORKFLOW_MAPS = ['workflows', 'templates'];
58
76
  function loadWorkflowStore() {
59
77
  try {
60
78
  const path = getWorkflowPath();
61
79
  if (existsSync(path)) {
62
- const data = readFileSync(path, 'utf-8');
63
- return JSON.parse(data);
80
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
81
+ if (data && typeof data === 'object' && !Array.isArray(data))
82
+ return withRecordMaps(data, WORKFLOW_MAPS);
64
83
  }
65
84
  }
66
85
  catch {
67
86
  // Return default store on error
68
87
  }
69
- return { workflows: {}, templates: {}, version: '3.0.0' };
88
+ return withRecordMaps({ workflows: {}, templates: {}, version: '3.0.0' }, WORKFLOW_MAPS);
70
89
  }
71
90
  function saveWorkflowStore(store) {
72
91
  ensureWorkflowDir();
@@ -1540,15 +1540,39 @@ export async function bridgeListEntries(options) {
1540
1540
  // List
1541
1541
  const entries = [];
1542
1542
  try {
1543
- // Include tags so export/list can round-trip them.
1544
- const stmt = ctx.db.prepare(`
1545
- SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type, expires_at, tags
1543
+ const columns = 'id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type, expires_at, tags';
1544
+ const listLimit = options.includeEmbedding ? Math.min(safeLimit, MAX_LIST_EMBEDDINGS) : safeLimit;
1545
+ // #4686: sort only row identities first, then fetch the payload for the
1546
+ // page. The old query sorted full `content`/`embedding` payloads for
1547
+ // every row and discarded all but the requested page.
1548
+ const pagedSql = `
1549
+ SELECT ${columns}
1550
+ FROM memory_entries
1551
+ WHERE _rowid_ IN (
1552
+ SELECT _rowid_
1553
+ FROM memory_entries
1554
+ WHERE ${statusFilter} ${extraFilter}
1555
+ ORDER BY updated_at DESC
1556
+ LIMIT ? OFFSET ?
1557
+ )
1558
+ ORDER BY updated_at DESC
1559
+ `;
1560
+ const originalSql = `
1561
+ SELECT ${columns}
1546
1562
  FROM memory_entries
1547
1563
  WHERE ${statusFilter} ${extraFilter}
1548
1564
  ORDER BY updated_at DESC
1549
1565
  LIMIT ? OFFSET ?
1550
- `);
1551
- const rows = stmt.all(...filterParams, options.includeEmbedding ? Math.min(safeLimit, MAX_LIST_EMBEDDINGS) : safeLimit, safeOffset);
1566
+ `;
1567
+ let rows;
1568
+ try {
1569
+ rows = ctx.db.prepare(pagedSql).all(...filterParams, listLimit, safeOffset);
1570
+ }
1571
+ catch {
1572
+ // WITHOUT ROWID/legacy tables have no _rowid_: preserve the original
1573
+ // query rather than failing the page.
1574
+ rows = ctx.db.prepare(originalSql).all(...filterParams, listLimit, safeOffset);
1575
+ }
1552
1576
  for (const row of rows) {
1553
1577
  let tags = [];
1554
1578
  if (row.tags) {
@@ -3115,16 +3115,38 @@ export async function listEntries(options) {
3115
3115
  const total = countRows[0]?.[0] || 0;
3116
3116
  // Get entries. ADR-472: embedding rows are capped at MAX_LIST_EMBEDDINGS.
3117
3117
  const listLimit = options.includeEmbedding ? Math.min(safeLimit, MAX_LIST_EMBEDDINGS) : safeLimit;
3118
- // Include tags so export/list can round-trip them.
3119
- const listStmt = db.prepare(`SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type, expires_at, tags
3120
- FROM memory_entries WHERE ${whereSql}
3121
- ORDER BY updated_at DESC LIMIT ? OFFSET ?`);
3122
- listStmt.bind([...whereParams, listLimit, safeOffset]);
3123
- const listRows = [];
3124
- while (listStmt.step()) {
3125
- listRows.push(listStmt.get());
3126
- }
3127
- listStmt.free();
3118
+ const columns = 'id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type, expires_at, tags';
3119
+ // #4686: page row identities first so the temporary sort B-tree does not
3120
+ // carry full content/embedding payloads for rows outside the page.
3121
+ const pagedSql = `SELECT ${columns} FROM memory_entries
3122
+ WHERE _rowid_ IN (
3123
+ SELECT _rowid_ FROM memory_entries WHERE ${whereSql}
3124
+ ORDER BY updated_at DESC LIMIT ? OFFSET ?
3125
+ )
3126
+ ORDER BY updated_at DESC`;
3127
+ const originalSql = `SELECT ${columns} FROM memory_entries WHERE ${whereSql}
3128
+ ORDER BY updated_at DESC LIMIT ? OFFSET ?`;
3129
+ let listRows = [];
3130
+ const runList = (sql) => {
3131
+ const stmt = db.prepare(sql);
3132
+ try {
3133
+ stmt.bind([...whereParams, listLimit, safeOffset]);
3134
+ const rows = [];
3135
+ while (stmt.step())
3136
+ rows.push(stmt.get());
3137
+ return rows;
3138
+ }
3139
+ finally {
3140
+ stmt.free();
3141
+ }
3142
+ };
3143
+ try {
3144
+ listRows = runList(pagedSql);
3145
+ }
3146
+ catch {
3147
+ // Legacy WITHOUT ROWID tables have no _rowid_: keep the original query.
3148
+ listRows = runList(originalSql);
3149
+ }
3128
3150
  const entries = [];
3129
3151
  for (const row of listRows) {
3130
3152
  const [id, key, ns, content, embedding, accessCount, createdAt, updatedAt, provenanceTypeVal, expiresAtVal, tagsJson] = row;
@@ -11,7 +11,7 @@ export function readClaimsStore(file) {
11
11
  }
12
12
  catch (error) {
13
13
  if (error.code === 'ENOENT')
14
- return { claims: {}, stealable: {}, contests: {} };
14
+ return { claims: Object.create(null), stealable: Object.create(null), contests: Object.create(null) };
15
15
  throw error;
16
16
  }
17
17
  const data = JSON.parse(text);
@@ -35,6 +35,9 @@ export function readClaimsStore(file) {
35
35
  if (data[key] !== undefined && !record(data[key]))
36
36
  throw new Error(`Invalid claims store: ${key} must be an object`);
37
37
  }
38
- return { ...data, claims, stealable: data.stealable ?? {}, contests: data.contests ?? {} };
38
+ // SECURITY: issue ids come from tool input; null-prototype maps keep a lookup
39
+ // by `constructor`/`__proto__` from resolving to an inherited built-in.
40
+ const nullProto = (map) => Object.assign(Object.create(null), map ?? {});
41
+ return { ...data, claims, stealable: nullProto(data.stealable), contests: nullProto(data.contests) };
39
42
  }
40
43
  //# sourceMappingURL=claims-store.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.71.1",
3
+ "version": "3.71.2",
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",