claude-flow 3.48.0 → 3.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/hook-handler.cjs +20 -4
  3. package/.claude/helpers/memory.cjs +1 -1
  4. package/.claude/helpers/router.cjs +1 -1
  5. package/.claude/helpers/session.cjs +1 -1
  6. package/.claude/proven-config.json +42 -0
  7. package/.claude-plugin/marketplace.json +26 -1
  8. package/README.md +1 -53
  9. package/README.zh-CN.md +1 -53
  10. package/node_modules/@claude-flow/codex/package.json +1 -1
  11. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
  12. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  13. package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
  14. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  15. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
  16. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  17. package/node_modules/@claude-flow/security/package.json +1 -1
  18. package/package.json +2 -2
  19. package/v3/@claude-flow/cli/README.md +3 -53
  20. package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
  21. package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
  22. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +88 -9
  23. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +7 -4
  24. package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
  25. package/v3/@claude-flow/cli/dist/src/commands/init.js +20 -0
  26. package/v3/@claude-flow/cli/dist/src/commands/memory.js +30 -7
  27. package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +13 -0
  28. package/v3/@claude-flow/cli/dist/src/commands/mods.js +126 -0
  29. package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
  30. package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
  31. package/v3/@claude-flow/cli/dist/src/commands/session.js +128 -21
  32. package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
  33. package/v3/@claude-flow/cli/dist/src/index.js +10 -1
  34. package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
  35. package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
  36. package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
  37. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
  38. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
  39. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
  40. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
  41. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +22 -10
  42. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -3
  43. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +87 -14
  44. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
  45. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +43 -6
  46. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
  47. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +236 -50
  48. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +6 -3
  49. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.d.ts +29 -0
  50. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +83 -0
  51. package/v3/@claude-flow/cli/dist/src/mods/install.d.ts +64 -0
  52. package/v3/@claude-flow/cli/dist/src/mods/install.js +135 -0
  53. package/v3/@claude-flow/cli/dist/src/mods/policy-projection.d.ts +39 -0
  54. package/v3/@claude-flow/cli/dist/src/mods/policy-projection.js +65 -0
  55. package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +28 -0
  56. package/v3/@claude-flow/cli/dist/src/mods/probe.js +118 -0
  57. package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
  58. package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
  59. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
  60. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
  61. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
  62. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +42 -2
  63. package/v3/@claude-flow/cli/package.json +2 -2
@@ -69,17 +69,126 @@ function listSessions() {
69
69
  }
70
70
  return sessions;
71
71
  }
72
- // Load related stores for session data
73
- function loadRelatedStores(options) {
74
- const data = {};
75
- if (options.includeMemory) {
72
+ /** Snapshot format written by session_save since #3573. */
73
+ const MEMORY_SNAPSHOT_FORMAT = 'ruflo-session-memory/2';
74
+ const MEMORY_PAGE_SIZE = 500;
75
+ async function readLiveEntries(listEntries, dbPath, encryptWrites) {
76
+ const rows = [];
77
+ // Bounded: stop on a short page, once `total` rows are in hand, or after a
78
+ // hard cap, so a backend that ignores `offset` cannot loop forever.
79
+ for (let page = 0, offset = 0; page < 10_000; page++, offset += MEMORY_PAGE_SIZE) {
80
+ const result = await listEntries({
81
+ dbPath, includeContent: true, limit: MEMORY_PAGE_SIZE, offset,
82
+ ...(encryptWrites === undefined ? {} : { encryptWrites }),
83
+ });
84
+ if (!result.success)
85
+ throw new Error(result.error || `could not list ${dbPath}`);
86
+ rows.push(...result.entries);
87
+ if (result.entries.length < MEMORY_PAGE_SIZE || rows.length >= result.total)
88
+ break;
89
+ }
90
+ return rows;
91
+ }
92
+ /**
93
+ * #3573: capture the memory the user actually has.
94
+ *
95
+ * Before this, session_save read only the pre-SQLite `.claude-flow/memory/store.json`,
96
+ * which a current install never writes, so `--include-memory` saved nothing and
97
+ * reported "Memory Entries: 0". The snapshot now covers the same store `memory
98
+ * list` reads (resolveDbPath), plus rows that exist only in the sibling AgentDB
99
+ * store written by the MCP path, de-duplicated by namespace+key (default CLI
100
+ * writes are mirrored into AgentDB, so most rows are in both). A legacy
101
+ * store.json is still captured for backward compatibility.
102
+ */
103
+ async function captureMemorySnapshot() {
104
+ const report = {
105
+ requested: true, status: 'captured', entries: 0,
106
+ sources: { memoryDb: 0, agentdb: 0, legacyJson: 0 },
107
+ };
108
+ const entries = {};
109
+ let legacy;
110
+ const legacyPath = join(getProjectCwd(), STORAGE_DIR, 'memory', 'store.json');
111
+ if (existsSync(legacyPath)) {
76
112
  try {
77
- const memoryPath = join(getProjectCwd(), STORAGE_DIR, 'memory', 'store.json');
78
- if (existsSync(memoryPath)) {
79
- data.memory = JSON.parse(readFileSync(memoryPath, 'utf-8'));
113
+ legacy = JSON.parse(readFileSync(legacyPath, 'utf-8'));
114
+ const legacyEntries = legacy.entries || {};
115
+ for (const [id, entry] of Object.entries(legacyEntries)) {
116
+ entries[id] = entry;
117
+ report.sources.legacyJson++;
80
118
  }
81
119
  }
82
- catch { /* ignore */ }
120
+ catch { /* unreadable legacy file is not memory we can restore */ }
121
+ }
122
+ let sawStore = legacy !== undefined;
123
+ try {
124
+ const { listEntries, resolveDbPath } = await import('../memory/memory-initializer.js');
125
+ const primary = resolveDbPath();
126
+ const seen = new Set();
127
+ if (existsSync(primary)) {
128
+ sawStore = true;
129
+ for (const row of await readLiveEntries(listEntries, primary)) {
130
+ const id = `${row.namespace}::${row.key}`;
131
+ seen.add(id);
132
+ entries[id] = {
133
+ key: row.key, value: row.content ?? '', namespace: row.namespace,
134
+ provenanceType: row.provenanceType, source: 'memory-db',
135
+ };
136
+ report.sources.memoryDb++;
137
+ }
138
+ }
139
+ let sibling = null;
140
+ try {
141
+ const { siblingAgentDbPath } = await import('../memory/memory-bridge.js');
142
+ sibling = siblingAgentDbPath(primary);
143
+ }
144
+ catch { /* no bridge, no sibling store */ }
145
+ if (sibling && existsSync(sibling)) {
146
+ sawStore = true;
147
+ // The AgentDB store is read by native SQLite and must stay plaintext.
148
+ for (const row of await readLiveEntries(listEntries, sibling, false)) {
149
+ const id = `${row.namespace}::${row.key}`;
150
+ if (seen.has(id))
151
+ continue;
152
+ seen.add(id);
153
+ entries[id] = {
154
+ key: row.key, value: row.content ?? '', namespace: row.namespace,
155
+ provenanceType: row.provenanceType, source: 'agentdb',
156
+ };
157
+ report.sources.agentdb++;
158
+ }
159
+ }
160
+ }
161
+ catch (e) {
162
+ report.status = 'error';
163
+ report.error = e.message;
164
+ }
165
+ report.entries = Object.keys(entries).length;
166
+ if (report.status !== 'error' && !sawStore)
167
+ report.status = 'no-store';
168
+ if (report.entries === 0 && legacy === undefined) {
169
+ return { report };
170
+ }
171
+ return {
172
+ memory: {
173
+ format: MEMORY_SNAPSHOT_FORMAT,
174
+ entries,
175
+ ...(legacy !== undefined ? { legacy } : {}),
176
+ },
177
+ report,
178
+ };
179
+ }
180
+ // Load related stores for session data
181
+ async function loadRelatedStores(options) {
182
+ const data = {};
183
+ let memoryCapture = {
184
+ requested: false, status: 'not-requested', entries: 0,
185
+ sources: { memoryDb: 0, agentdb: 0, legacyJson: 0 },
186
+ };
187
+ if (options.includeMemory) {
188
+ const captured = await captureMemorySnapshot();
189
+ memoryCapture = captured.report;
190
+ if (captured.memory)
191
+ data.memory = captured.memory;
83
192
  }
84
193
  if (options.includeTasks) {
85
194
  try {
@@ -99,7 +208,92 @@ function loadRelatedStores(options) {
99
208
  }
100
209
  catch { /* ignore */ }
101
210
  }
102
- return data;
211
+ return { data, memoryCapture };
212
+ }
213
+ /** Count entries in a memory snapshot of either format. */
214
+ function countMemoryEntries(memory) {
215
+ if (!memory || typeof memory !== 'object')
216
+ return 0;
217
+ return Object.keys(memory.entries || {}).length;
218
+ }
219
+ /**
220
+ * Restore a memory snapshot into the live store and report what was actually
221
+ * written. #3573: the previous restore swallowed every write failure and then
222
+ * echoed the count recorded at save time.
223
+ */
224
+ async function restoreMemorySnapshot(memory) {
225
+ const outcome = { restored: 0, failed: 0, errors: [] };
226
+ const isV2 = memory.format === MEMORY_SNAPSHOT_FORMAT;
227
+ // Legacy snapshots are the old store.json itself; v2 snapshots carry it
228
+ // under `legacy` only when one existed at save time.
229
+ const legacyJson = isV2 ? memory.legacy : memory;
230
+ if (legacyJson !== undefined) {
231
+ const memoryDir = join(getProjectCwd(), STORAGE_DIR, 'memory');
232
+ if (!existsSync(memoryDir))
233
+ mkdirRestricted(memoryDir);
234
+ writeFileRestricted(join(memoryDir, 'store.json'), JSON.stringify(legacyJson, null, 2));
235
+ }
236
+ const entries = memory.entries;
237
+ if (!entries)
238
+ return outcome;
239
+ let storeEntry;
240
+ let isValidProvenanceType = () => false;
241
+ try {
242
+ const mod = await import('../memory/memory-initializer.js');
243
+ storeEntry = mod.storeEntry;
244
+ try {
245
+ if (typeof mod.isValidProvenanceType === 'function')
246
+ isValidProvenanceType = mod.isValidProvenanceType;
247
+ }
248
+ catch { /* optional: without it, provenance is simply not carried over */ }
249
+ }
250
+ catch (e) {
251
+ outcome.failed = Object.keys(entries).length;
252
+ outcome.errors.push(`memory store unavailable: ${e.message}`);
253
+ return outcome;
254
+ }
255
+ for (const entry of Object.values(entries)) {
256
+ const key = entry.key || entry.id || '';
257
+ const value = entry.value || entry.content || '';
258
+ if (!key || !value) {
259
+ outcome.failed++;
260
+ continue;
261
+ }
262
+ try {
263
+ // A snapshot from another version may carry a provenance value this one
264
+ // rejects; drop it rather than fail an otherwise-good row.
265
+ const provenance = entry.provenanceType && entry.provenanceType !== 'unknown'
266
+ && isValidProvenanceType(entry.provenanceType) ? entry.provenanceType : undefined;
267
+ const result = await storeEntry({
268
+ key,
269
+ value,
270
+ namespace: entry.namespace || 'restored',
271
+ upsert: true,
272
+ ...(provenance ? { provenanceType: provenance } : {}),
273
+ });
274
+ if (result && result.success === false) {
275
+ outcome.failed++;
276
+ if (result.error && outcome.errors.length < 5)
277
+ outcome.errors.push(`${key}: ${result.error}`);
278
+ }
279
+ else {
280
+ outcome.restored++;
281
+ }
282
+ }
283
+ catch (e) {
284
+ outcome.failed++;
285
+ if (outcome.errors.length < 5)
286
+ outcome.errors.push(`${key}: ${e.message}`);
287
+ }
288
+ }
289
+ return outcome;
290
+ }
291
+ /** A session record is an object that carries at least one of its defining fields. */
292
+ function isSessionRecordLike(value) {
293
+ if (!value || typeof value !== 'object' || Array.isArray(value))
294
+ return false;
295
+ const v = value;
296
+ return 'data' in v || 'stats' in v || 'sessionId' in v || 'savedAt' in v;
103
297
  }
104
298
  export const sessionTools = [
105
299
  {
@@ -129,7 +323,7 @@ export const sessionTools = [
129
323
  }
130
324
  const sessionId = `session-${Date.now()}-${randomUUID().slice(0, 8)}`;
131
325
  // Load related data based on options
132
- const data = loadRelatedStores({
326
+ const { data, memoryCapture } = await loadRelatedStores({
133
327
  includeMemory: input.includeMemory,
134
328
  includeTasks: input.includeTasks,
135
329
  includeAgents: input.includeAgents,
@@ -138,7 +332,7 @@ export const sessionTools = [
138
332
  const stats = {
139
333
  tasks: data.tasks ? Object.keys(data.tasks.tasks || {}).length : 0,
140
334
  agents: data.agents ? Object.keys(data.agents.agents || {}).length : 0,
141
- memoryEntries: data.memory ? Object.keys(data.memory.entries || {}).length : 0,
335
+ memoryEntries: countMemoryEntries(data.memory),
142
336
  totalSize: 0,
143
337
  };
144
338
  const session = {
@@ -158,6 +352,7 @@ export const sessionTools = [
158
352
  name: session.name,
159
353
  savedAt: session.savedAt,
160
354
  stats: session.stats,
355
+ memoryCapture,
161
356
  path: getSessionPath(sessionId),
162
357
  };
163
358
  },
@@ -207,35 +402,11 @@ export const sessionTools = [
207
402
  }
208
403
  }
209
404
  if (session) {
210
- // Restore data to respective stores (legacy JSON for backward compat).
211
- // audit_1776853149979: tighten perms on the restored stores too.
405
+ // Restore data to respective stores. audit_1776853149979: tighten
406
+ // perms on the restored stores too.
407
+ let memoryRestore;
212
408
  if (input.restoreMemory !== false && session.data?.memory) {
213
- const memoryDir = join(getProjectCwd(), STORAGE_DIR, 'memory');
214
- if (!existsSync(memoryDir))
215
- mkdirRestricted(memoryDir);
216
- writeFileRestricted(join(memoryDir, 'store.json'), JSON.stringify(session.data.memory, null, 2));
217
- // Also populate active sql.js SQLite database so memory-tools can find entries
218
- try {
219
- const { storeEntry } = await import('../memory/memory-initializer.js');
220
- const memoryData = session.data.memory;
221
- if (memoryData.entries) {
222
- for (const entry of Object.values(memoryData.entries)) {
223
- const key = entry.key || entry.id || '';
224
- const value = entry.value || entry.content || '';
225
- if (key && value) {
226
- await storeEntry({
227
- key,
228
- value,
229
- namespace: entry.namespace || 'restored',
230
- upsert: true,
231
- });
232
- }
233
- }
234
- }
235
- }
236
- catch {
237
- // Legacy JSON restore is the fallback -- sql.js import may not be available
238
- }
409
+ memoryRestore = await restoreMemorySnapshot(session.data.memory);
239
410
  }
240
411
  if (input.restoreTasks !== false && session.data?.tasks) {
241
412
  const taskDir = join(getProjectCwd(), STORAGE_DIR, 'tasks');
@@ -260,6 +431,11 @@ export const sessionTools = [
260
431
  },
261
432
  restoredAt: new Date().toISOString(),
262
433
  stats: session.stats,
434
+ // #3573: counts actually written, not the count recorded at save time.
435
+ ...(memoryRestore ? {
436
+ memoryRestore,
437
+ stats: { ...session.stats, memoryEntriesRestored: memoryRestore.restored },
438
+ } : {}),
263
439
  };
264
440
  }
265
441
  return {
@@ -484,30 +660,40 @@ export const sessionTools = [
484
660
  // #1916: `ruflo session import <file>` referenced an unregistered
485
661
  // `session_import` tool. Reads a session JSON and re-saves it locally.
486
662
  name: 'session_import',
487
- description: 'Import a session JSON file (produced by session_export) into the local session store and optionally activate it. Use when native Read is wrong because the file is a structured session record that must be re-registered (new id, stats recomputed) rather than just read. For reading the file, native Read is fine. Pair with session_export on the source.',
663
+ description: 'Import a session (produced by session_export) into the local session store and optionally activate it. Pass either inputPath (a session JSON file) or data (the session record itself). Use when native Read is wrong because the file is a structured session record that must be re-registered (new id, stats recomputed) rather than just read. For reading the file, native Read is fine. Pair with session_export on the source.',
488
664
  category: 'session',
489
665
  inputSchema: {
490
666
  type: 'object',
491
667
  properties: {
492
- inputPath: { type: 'string', description: 'Path to the session JSON file to import' },
668
+ inputPath: { type: 'string', description: 'Path to the session JSON file to import (or pass data)' },
669
+ data: { type: 'object', description: 'The session record itself, as written by session_export (or pass inputPath)' },
493
670
  name: { type: 'string', description: 'Override the imported session name' },
494
671
  activate: { type: 'boolean', description: 'Restore the imported session into the active stores' },
495
672
  },
496
- required: ['inputPath'],
497
673
  },
498
674
  handler: async (input) => {
499
- const inputPath = String(input.inputPath ?? '');
500
- if (!inputPath || !existsSync(inputPath))
501
- return { error: `File not found: ${inputPath || '(empty)'}` };
502
675
  let parsed;
503
- try {
504
- parsed = JSON.parse(readFileSync(inputPath, 'utf-8'));
676
+ if (input.data !== undefined && input.data !== null) {
677
+ parsed = input.data;
678
+ }
679
+ else {
680
+ const inputPath = String(input.inputPath ?? '');
681
+ if (!inputPath)
682
+ return { error: 'Provide inputPath (a session JSON file) or data (a session record)' };
683
+ if (!existsSync(inputPath))
684
+ return { error: `File not found: ${inputPath}` };
685
+ try {
686
+ parsed = JSON.parse(readFileSync(inputPath, 'utf-8'));
687
+ }
688
+ catch (e) {
689
+ return { error: `Invalid session JSON: ${e.message}` };
690
+ }
505
691
  }
506
- catch (e) {
507
- return { error: `Invalid session JSON: ${e.message}` };
692
+ if (!isSessionRecordLike(parsed)) {
693
+ return { error: 'Not a session record: expected an object produced by session export' };
508
694
  }
509
695
  const newId = `session-${Date.now()}-${randomUUID().slice(0, 8)}`;
510
- const stats = parsed.stats || { tasks: 0, agents: 0, memoryEntries: 0, totalSize: 0 };
696
+ const stats = { tasks: 0, agents: 0, memoryEntries: 0, totalSize: 0, ...(parsed.stats || {}) };
511
697
  const session = {
512
698
  sessionId: newId,
513
699
  name: input.name ? String(input.name) : (parsed.name || 'imported-session'),
@@ -15,6 +15,7 @@ import { AsyncLocalStorage } from 'node:async_hooks';
15
15
  import { createRequire } from 'node:module';
16
16
  import { readFileMaybeEncrypted, writeFileAtomic, writeFileRestricted } from '../fs-secure.js';
17
17
  import { restoreMemoryDbFromBackup } from '../services/memory-backup.js';
18
+ import { validateIdentifier } from '../mcp-tools/validate-input.js';
18
19
  /**
19
20
  * ADR-323 — typed memory provenance. Distinguishes WHO/WHAT wrote a memory
20
21
  * entry (a user's stated claim vs an agent's own output vs a tool result vs
@@ -3392,11 +3393,13 @@ export async function withMemoryDbLock(dbPath, fn) {
3392
3393
  }
3393
3394
  }
3394
3395
  }
3395
- const NAMESPACE_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
3396
3396
  export async function purgeNamespace(options) {
3397
3397
  const { namespace, dbPath: customPath } = options;
3398
- if (!NAMESPACE_PATTERN.test(namespace)) {
3399
- return { success: false, deletedCount: 0, remainingEntries: 0, error: `Invalid namespace: ${namespace}` };
3398
+ // #3570: the same validator store, import and export use, so any namespace
3399
+ // that can be written can also be purged (`team:alice` included).
3400
+ const vNs = validateIdentifier(namespace, 'namespace');
3401
+ if (!vNs.valid) {
3402
+ return { success: false, deletedCount: 0, remainingEntries: 0, error: `Invalid namespace: ${vNs.error}` };
3400
3403
  }
3401
3404
  const swarmDir = getMemoryRoot();
3402
3405
  const dbPath = customPath ? path.resolve(customPath) : path.join(swarmDir, 'memory.db');
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Which Claude Code binaries a shell would find, and whether the one it runs
3
+ * can load mods (ADR-404). Mods are on by default from Claude Code 2.1.287;
4
+ * from 2.1.277 they load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1; older
5
+ * builds predate them. A server-side rollout switch can still hold them off
6
+ * on any version (reported separately). A stale install earlier on PATH
7
+ * (a 2.1.107 npm-global beside a current native one, say) silently decides.
8
+ */
9
+ export declare const MODS_DEFAULT_ON = "2.1.287";
10
+ export declare const MODS_FIRST = "2.1.277";
11
+ export interface ClaudeInstall {
12
+ path: string;
13
+ version: string | null;
14
+ }
15
+ export type VersionOf = (path: string) => string | null;
16
+ /** a < b for dotted numeric versions. */
17
+ export declare function versionLess(a: string, b: string): boolean;
18
+ /** `claude --version` of one binary, bounded; null when it cannot say. */
19
+ export declare const defaultVersionOf: VersionOf;
20
+ /** Every `claude` on PATH, in PATH order (the first is what runs), plus ~/.local/bin. */
21
+ export declare function findClaudeInstalls(env: NodeJS.ProcessEnv, home: string, versionOf?: VersionOf): ClaudeInstall[];
22
+ export interface InstallsFinding {
23
+ status: 'pass' | 'warn';
24
+ message: string;
25
+ fix?: string;
26
+ }
27
+ /** The doctor's reading of what `claude` resolves to and what else is installed. */
28
+ export declare function judgeInstalls(installs: readonly ClaudeInstall[], enableEnvSet: boolean): InstallsFinding;
29
+ //# sourceMappingURL=claude-installs.d.ts.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Which Claude Code binaries a shell would find, and whether the one it runs
3
+ * can load mods (ADR-404). Mods are on by default from Claude Code 2.1.287;
4
+ * from 2.1.277 they load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1; older
5
+ * builds predate them. A server-side rollout switch can still hold them off
6
+ * on any version (reported separately). A stale install earlier on PATH
7
+ * (a 2.1.107 npm-global beside a current native one, say) silently decides.
8
+ */
9
+ import { execFileSync } from 'node:child_process';
10
+ import { existsSync, realpathSync, statSync } from 'node:fs';
11
+ import { delimiter, join } from 'node:path';
12
+ export const MODS_DEFAULT_ON = '2.1.287';
13
+ export const MODS_FIRST = '2.1.277';
14
+ const parse = (v) => v.split('.').map((n) => Number(n));
15
+ /** a < b for dotted numeric versions. */
16
+ export function versionLess(a, b) {
17
+ const x = parse(a);
18
+ const y = parse(b);
19
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
20
+ if ((x[i] ?? 0) !== (y[i] ?? 0))
21
+ return (x[i] ?? 0) < (y[i] ?? 0);
22
+ }
23
+ return false;
24
+ }
25
+ /** `claude --version` of one binary, bounded; null when it cannot say. */
26
+ export const defaultVersionOf = (path) => {
27
+ try {
28
+ const out = execFileSync(path, ['--version'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
29
+ return out.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ };
35
+ /** Every `claude` on PATH, in PATH order (the first is what runs), plus ~/.local/bin. */
36
+ export function findClaudeInstalls(env, home, versionOf = defaultVersionOf) {
37
+ const names = process.platform === 'win32' ? ['claude.exe', 'claude.cmd', 'claude'] : ['claude'];
38
+ const dirs = [...(env.PATH ?? '').split(delimiter).filter(Boolean), join(home, '.local', 'bin')];
39
+ const seen = new Set();
40
+ const installs = [];
41
+ for (const dir of dirs) {
42
+ for (const name of names) {
43
+ const path = join(dir, name);
44
+ let real;
45
+ try {
46
+ if (!existsSync(path) || !statSync(path).isFile())
47
+ continue;
48
+ real = realpathSync(path);
49
+ }
50
+ catch {
51
+ continue;
52
+ }
53
+ if (seen.has(real))
54
+ continue;
55
+ seen.add(real);
56
+ installs.push({ path, version: versionOf(path) });
57
+ }
58
+ }
59
+ return installs;
60
+ }
61
+ /** The doctor's reading of what `claude` resolves to and what else is installed. */
62
+ export function judgeInstalls(installs, enableEnvSet) {
63
+ if (installs.length === 0)
64
+ return { status: 'warn', message: 'no claude binary on PATH', fix: 'install Claude Code >= 2.1.287' };
65
+ const [first] = installs;
66
+ const others = installs.slice(1).filter((i) => i.version !== first.version);
67
+ const mixed = others.length
68
+ ? `; also installed: ${others.map((i) => `${i.path} (${i.version ?? 'unknown'})`).join(', ')}. The first on PATH runs; a stale one can shadow a current one.`
69
+ : '';
70
+ const runs = `${first.path} (${first.version ?? 'version unknown'})`;
71
+ if (!first.version)
72
+ return { status: 'warn', message: `claude resolves to ${runs}${mixed}` };
73
+ if (!versionLess(first.version, MODS_DEFAULT_ON)) {
74
+ return { status: others.length ? 'warn' : 'pass', message: `claude resolves to ${runs}: mods on by default${mixed}` };
75
+ }
76
+ if (!versionLess(first.version, MODS_FIRST)) {
77
+ return enableEnvSet
78
+ ? { status: others.length ? 'warn' : 'pass', message: `claude resolves to ${runs}: mods load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (set)${mixed}` }
79
+ : { status: 'warn', message: `claude resolves to ${runs}: below ${MODS_DEFAULT_ON}, mods need CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (not set)${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON}, or export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` };
80
+ }
81
+ return { status: 'warn', message: `claude resolves to ${runs}: predates mods (< ${MODS_FIRST}); classic hooks handle every event${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON} and remove the stale install from PATH` };
82
+ }
83
+ //# sourceMappingURL=claude-installs.js.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `ruflo mods install|uninstall` (ADR-404): enable the ruflo-mods plugin for
3
+ * one project, opt-in, and take back exactly what was added.
4
+ *
5
+ * Writes three keys of a Claude Code settings file (`.claude/settings.local.json`
6
+ * by default, so the early-access feature is one person's choice, not the
7
+ * repository's): `enabledPlugins["ruflo-mods@ruflo"]`, the `ruflo` entry of
8
+ * `extraKnownMarketplaces` when absent, and `env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS`.
9
+ * What was added is recorded in `.claude-flow/mods/install.json`, so uninstall
10
+ * removes those and nothing a person set themselves. Classic hooks are never
11
+ * touched: they stay the fallback (the mod takes an event over at runtime only).
12
+ */
13
+ export declare const MOD_PLUGIN_ID = "ruflo-mods@ruflo";
14
+ export declare const MARKETPLACE_NAME = "ruflo";
15
+ export declare const MARKETPLACE_SOURCE: {
16
+ readonly source: {
17
+ readonly source: "github";
18
+ readonly repo: "ruvnet/ruflo";
19
+ };
20
+ };
21
+ export declare const ENABLE_ENV = "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS";
22
+ export declare const INSTALL_RECORD: string;
23
+ export type Scope = 'local' | 'project';
24
+ export interface InstallRecord {
25
+ version: 1;
26
+ settingsFile: string;
27
+ installedAt: string;
28
+ added: {
29
+ plugin: boolean;
30
+ marketplace: boolean;
31
+ env: boolean;
32
+ };
33
+ }
34
+ type Settings = Record<string, unknown> & {
35
+ enabledPlugins?: Record<string, unknown>;
36
+ extraKnownMarketplaces?: Record<string, unknown>;
37
+ env?: Record<string, unknown>;
38
+ };
39
+ export declare function settingsFileFor(projectRoot: string, scope: Scope): string;
40
+ /** Reads a settings file; absent is `{}`, anything unparseable throws (never overwritten). */
41
+ export declare function readSettingsFile(path: string): Settings;
42
+ /** The settings after install, and what install added (pure). */
43
+ export declare function withModEnabled(settings: Settings): {
44
+ next: Settings;
45
+ added: InstallRecord['added'];
46
+ };
47
+ /** The settings after uninstall: only what the record says install added (pure). */
48
+ export declare function withModRemoved(settings: Settings, added: InstallRecord['added']): Settings;
49
+ export interface InstallResult {
50
+ settingsFile: string;
51
+ backup?: string;
52
+ added: InstallRecord['added'];
53
+ dryRun: boolean;
54
+ next: Settings;
55
+ }
56
+ export declare function installMod(projectRoot: string, scope: Scope, dryRun?: boolean): InstallResult;
57
+ export declare function readRecord(projectRoot: string): InstallRecord | null;
58
+ export declare function uninstallMod(projectRoot: string, dryRun?: boolean): {
59
+ settingsFile?: string;
60
+ removed: boolean;
61
+ dryRun: boolean;
62
+ };
63
+ export {};
64
+ //# sourceMappingURL=install.d.ts.map