moflo 4.12.3 → 4.12.4-rc.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.
- package/.claude/agents/core/coder.md +2 -3
- package/.claude/agents/core/researcher.md +3 -4
- package/.claude/agents/core/reviewer.md +6 -8
- package/.claude/guidance/shipped/moflo-spell-engine.md +13 -10
- package/.claude/helpers/gate-hook.mjs +67 -2
- package/.claude/helpers/gate.cjs +173 -12
- package/.claude/helpers/hook-handler.cjs +0 -0
- package/.claude/helpers/prompt-hook.mjs +0 -0
- package/.claude/skills/connector-builder/SKILL.md +5 -4
- package/.claude/skills/fl/phases.md +1 -1
- package/.claude/skills/fl/sdd.md +1 -1
- package/.claude/skills/spell-builder/SKILL.md +5 -4
- package/.claude/skills/spell-builder/architecture.md +4 -4
- package/.claude/skills/spell-builder/connectors/github-cli/README.md +22 -17
- package/.claude/skills/spell-builder/connectors/http/README.md +18 -11
- package/.claude/skills/spell-builder/connectors/local-outlook/README.md +8 -7
- package/.claude/skills/spell-builder/connectors/playwright/README.md +10 -8
- package/.claude/skills/spell-builder/steps/agent/README.md +25 -21
- package/.claude/skills/verify/SKILL.md +24 -3
- package/bin/gate-hook.mjs +67 -2
- package/bin/gate.cjs +173 -12
- package/bin/generate-code-map.mjs +70 -17
- package/bin/index-all.mjs +38 -0
- package/bin/index-guidance.mjs +8 -41
- package/bin/index-patterns.mjs +2 -2
- package/bin/index-tests.mjs +6 -4
- package/bin/lib/guidance-config.mjs +119 -0
- package/bin/lib/index-fingerprint.mjs +0 -0
- package/dist/src/cli/commands/agent.js +1 -89
- package/dist/src/cli/commands/analyze.js +1 -1
- package/dist/src/cli/commands/benchmark.js +11 -8
- package/dist/src/cli/commands/claims.js +2 -2
- package/dist/src/cli/commands/completions.js +1 -1
- package/dist/src/cli/commands/config.js +256 -96
- package/dist/src/cli/commands/deployment.js +6 -6
- package/dist/src/cli/commands/diagnose.js +1 -1
- package/dist/src/cli/commands/doctor-checks-config.js +18 -22
- package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
- package/dist/src/cli/commands/doctor-fixes.js +36 -11
- package/dist/src/cli/commands/doctor.js +1 -1
- package/dist/src/cli/commands/embeddings.js +64 -23
- package/dist/src/cli/commands/github.js +1 -1
- package/dist/src/cli/commands/guidance.js +10 -10
- package/dist/src/cli/commands/hive-mind.js +8 -70
- package/dist/src/cli/commands/hooks.js +32 -219
- package/dist/src/cli/commands/mcp.js +5 -2
- package/dist/src/cli/commands/memory.js +59 -46
- package/dist/src/cli/commands/neural.js +21 -21
- package/dist/src/cli/commands/performance.js +14 -7
- package/dist/src/cli/commands/plugins.js +2 -2
- package/dist/src/cli/commands/providers.js +2 -2
- package/dist/src/cli/commands/retire.js +1 -1
- package/dist/src/cli/commands/route.js +1 -1
- package/dist/src/cli/commands/security.js +4 -4
- package/dist/src/cli/commands/status.js +18 -13
- package/dist/src/cli/commands/task.js +8 -0
- package/dist/src/cli/config/cli-config-store.js +326 -0
- package/dist/src/cli/hooks/workers/index.js +15 -9
- package/dist/src/cli/index.js +43 -33
- package/dist/src/cli/init/claudemd-generator.js +3 -3
- package/dist/src/cli/init/helpers-generator.js +203 -20
- package/dist/src/cli/init/moflo-yaml-template.js +11 -3
- package/dist/src/cli/init/settings-generator.js +10 -2
- package/dist/src/cli/mcp-client.js +12 -2
- package/dist/src/cli/mcp-server.js +5 -23
- package/dist/src/cli/mcp-tools/analysis-tools.js +103 -0
- package/dist/src/cli/mcp-tools/coverage-tools.js +206 -0
- package/dist/src/cli/mcp-tools/hive-mind-tools.js +71 -0
- package/dist/src/cli/mcp-tools/hooks-tools.js +25 -9
- package/dist/src/cli/mcp-tools/memory-admin-tools.js +538 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +26 -5
- package/dist/src/cli/mcp-tools/neural-tools.js +56 -87
- package/dist/src/cli/mcp-tools/performance-tools.js +76 -39
- package/dist/src/cli/mcp-tools/progress-tools.js +142 -0
- package/dist/src/cli/mcp-tools/security-tools.js +1 -1
- package/dist/src/cli/mcp-tools/session-tools.js +116 -0
- package/dist/src/cli/mcp-tools/synthetic.js +74 -0
- package/dist/src/cli/mcp-tools/system-tools.js +126 -78
- package/dist/src/cli/mcp-tools/task-tools.js +101 -1
- package/dist/src/cli/movector/coverage-router.js +48 -6
- package/dist/src/cli/movector/diff-classifier.js +8 -6
- package/dist/src/cli/movector/flash-attention.js +1 -1
- package/dist/src/cli/movector/vector-db.js +1 -1
- package/dist/src/cli/parser.js +81 -12
- package/dist/src/cli/services/daemon-lock.js +5 -2
- package/dist/src/cli/services/hook-block-hash.js +3 -1
- package/dist/src/cli/services/moflo-version.js +62 -0
- package/dist/src/cli/services/movector-training.js +1 -1
- package/dist/src/cli/services/spell-gate.js +6 -4
- package/dist/src/cli/services/worker-daemon.js +37 -7
- package/dist/src/cli/shared/utils/load-average.js +59 -0
- package/dist/src/cli/spells/commands/agent-command.js +55 -25
- package/dist/src/cli/suggest.js +1 -1
- package/dist/src/cli/swarm/swarm-persistence.js +112 -0
- package/dist/src/cli/swarm/unified-coordinator.js +118 -3
- package/dist/src/cli/version.js +1 -1
- package/package.json +5 -3
- package/dist/src/cli/mcp-tools/github-tools.js +0 -318
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
* V3 CLI Performance Command
|
|
3
3
|
* Performance profiling, benchmarking, optimization, metrics
|
|
4
4
|
*
|
|
5
|
-
* Created with ❤️ by
|
|
5
|
+
* Created with ❤️ by cielolimitada.com
|
|
6
6
|
*/
|
|
7
7
|
import { output } from '../output.js';
|
|
8
|
+
import { readLoadAverage, NOT_MEASURED } from '../shared/utils/load-average.js';
|
|
8
9
|
// Benchmark subcommand - REAL measurements
|
|
9
10
|
const benchmarkCommand = {
|
|
10
11
|
name: 'benchmark',
|
|
11
12
|
description: 'Run performance benchmarks',
|
|
12
13
|
options: [
|
|
13
14
|
{ name: 'suite', short: 's', type: 'string', description: 'Benchmark suite: all, wasm, neural, memory, search', default: 'all' },
|
|
14
|
-
{ name: 'iterations', short: 'i', type: 'number', description: 'Number of iterations', default:
|
|
15
|
-
{ name: 'warmup', short: 'w', type: 'number', description: 'Warmup iterations', default:
|
|
15
|
+
{ name: 'iterations', short: 'i', type: 'number', description: 'Number of iterations', default: 100 },
|
|
16
|
+
{ name: 'warmup', short: 'w', type: 'number', description: 'Warmup iterations', default: 10 },
|
|
16
17
|
{ name: 'output', short: 'o', type: 'string', description: 'Output format: text, json, csv', default: 'text' },
|
|
17
18
|
],
|
|
18
19
|
examples: [
|
|
@@ -213,7 +214,7 @@ const profileCommand = {
|
|
|
213
214
|
description: 'Profile application performance',
|
|
214
215
|
options: [
|
|
215
216
|
{ name: 'type', short: 't', type: 'string', description: 'Profile type: cpu, memory, io, all', default: 'all' },
|
|
216
|
-
{ name: 'duration', short: 'd', type: 'number', description: 'Duration in seconds', default:
|
|
217
|
+
{ name: 'duration', short: 'd', type: 'number', description: 'Duration in seconds', default: 30 },
|
|
217
218
|
{ name: 'output', short: 'o', type: 'string', description: 'Output file for profile data' },
|
|
218
219
|
],
|
|
219
220
|
examples: [
|
|
@@ -302,7 +303,10 @@ const metricsCommand = {
|
|
|
302
303
|
const memUsage = process.memoryUsage();
|
|
303
304
|
const cpuUsage = process.cpuUsage();
|
|
304
305
|
const uptime = process.uptime();
|
|
305
|
-
|
|
306
|
+
// null where the platform has none (#1358). Rendered as "not measured"
|
|
307
|
+
// below rather than as 0.00 — a reading that does not exist must not read
|
|
308
|
+
// as an idle machine.
|
|
309
|
+
const loadAvg = readLoadAverage(os.loadavg(), os.platform());
|
|
306
310
|
const freeMem = os.freemem();
|
|
307
311
|
const totalMem = os.totalmem();
|
|
308
312
|
// Calculate real metrics
|
|
@@ -454,7 +458,10 @@ const metricsCommand = {
|
|
|
454
458
|
],
|
|
455
459
|
});
|
|
456
460
|
output.writeln();
|
|
457
|
-
|
|
461
|
+
const loadAvgLine = loadAvg === null
|
|
462
|
+
? `${NOT_MEASURED} (${os.platform()} has no load average)`
|
|
463
|
+
: loadAvg.map(l => l.toFixed(2)).join(', ');
|
|
464
|
+
output.writeln(output.dim(`Load Average: ${loadAvgLine}`));
|
|
458
465
|
output.writeln(output.dim(`CPUs: ${os.cpus().length} | Platform: ${os.platform()} ${os.release()}`));
|
|
459
466
|
return { success: true };
|
|
460
467
|
},
|
|
@@ -571,7 +578,7 @@ export const performanceCommand = {
|
|
|
571
578
|
'Memory: Int8 quantized weight storage',
|
|
572
579
|
]);
|
|
573
580
|
output.writeln();
|
|
574
|
-
output.writeln(output.dim('Created with ❤️ by
|
|
581
|
+
output.writeln(output.dim('Created with ❤️ by cielolimitada.com'));
|
|
575
582
|
return { success: true };
|
|
576
583
|
},
|
|
577
584
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Plugin management, installation, and lifecycle
|
|
4
4
|
* Now uses IPFS-based decentralized registry for discovery
|
|
5
5
|
*
|
|
6
|
-
* Created with ❤️ by
|
|
6
|
+
* Created with ❤️ by cielolimitada.com
|
|
7
7
|
*/
|
|
8
8
|
import { output } from '../output.js';
|
|
9
9
|
import { createPluginDiscoveryService, searchPlugins, getPluginSearchSuggestions, getFeaturedPlugins, getOfficialPlugins, } from '../plugins/store/index.js';
|
|
@@ -807,7 +807,7 @@ export const pluginsCommand = {
|
|
|
807
807
|
]);
|
|
808
808
|
output.writeln();
|
|
809
809
|
output.writeln(output.dim('Run "flo plugins list --official" to see all official plugins'));
|
|
810
|
-
output.writeln(output.dim('Created with ❤️ by
|
|
810
|
+
output.writeln(output.dim('Created with ❤️ by cielolimitada.com'));
|
|
811
811
|
return { success: true };
|
|
812
812
|
},
|
|
813
813
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* V3 CLI Providers Command
|
|
3
3
|
* Manage AI providers, models, and configurations
|
|
4
4
|
*
|
|
5
|
-
* Created with ❤️ by
|
|
5
|
+
* Created with ❤️ by cielolimitada.com
|
|
6
6
|
*/
|
|
7
7
|
import { output } from '../output.js';
|
|
8
8
|
// List subcommand
|
|
@@ -224,7 +224,7 @@ export const providersCommand = {
|
|
|
224
224
|
'Agentic Flow (optimized ONNX with SIMD)',
|
|
225
225
|
]);
|
|
226
226
|
output.writeln();
|
|
227
|
-
output.writeln(output.dim('Created with ❤️ by
|
|
227
|
+
output.writeln(output.dim('Created with ❤️ by cielolimitada.com'));
|
|
228
228
|
return { success: true };
|
|
229
229
|
},
|
|
230
230
|
};
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `retired-files.json` live at the moflo package root and don't ship to
|
|
14
14
|
* consumer projects.
|
|
15
15
|
*
|
|
16
|
-
* Created with
|
|
16
|
+
* Created with cielolimitada.com
|
|
17
17
|
*/
|
|
18
18
|
import { output } from '../output.js';
|
|
19
19
|
import { spawnSync } from 'child_process';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* V3 CLI Security Command
|
|
3
3
|
* Security scanning, CVE detection, threat modeling, vulnerability management
|
|
4
4
|
*
|
|
5
|
-
* Created with ❤️ by
|
|
5
|
+
* Created with ❤️ by cielolimitada.com
|
|
6
6
|
*/
|
|
7
7
|
import { output } from '../output.js';
|
|
8
8
|
// Scan subcommand
|
|
@@ -331,7 +331,7 @@ const auditCommand = {
|
|
|
331
331
|
description: 'Security audit logging and compliance',
|
|
332
332
|
options: [
|
|
333
333
|
{ name: 'action', short: 'a', type: 'string', description: 'Action: log, list, export, clear', default: 'list' },
|
|
334
|
-
{ name: 'limit', short: 'l', type: 'number', description: 'Number of entries to show', default:
|
|
334
|
+
{ name: 'limit', short: 'l', type: 'number', description: 'Number of entries to show', default: 20 },
|
|
335
335
|
{ name: 'filter', short: 'f', type: 'string', description: 'Filter by event type' },
|
|
336
336
|
],
|
|
337
337
|
examples: [
|
|
@@ -412,7 +412,7 @@ const defendCommand = {
|
|
|
412
412
|
{ name: 'input', short: 'i', type: 'string', description: 'Input text to scan for threats' },
|
|
413
413
|
{ name: 'file', short: 'f', type: 'string', description: 'File to scan for threats' },
|
|
414
414
|
{ name: 'quick', short: 'Q', type: 'boolean', description: 'Quick scan (faster, less detailed)' },
|
|
415
|
-
{ name: 'learn', short: 'l', type: 'boolean', description: 'Enable learning mode', default:
|
|
415
|
+
{ name: 'learn', short: 'l', type: 'boolean', description: 'Enable learning mode', default: true },
|
|
416
416
|
{ name: 'stats', short: 's', type: 'boolean', description: 'Show detection statistics' },
|
|
417
417
|
{ name: 'output', short: 'o', type: 'string', description: 'Output format: text, json', default: 'text' },
|
|
418
418
|
],
|
|
@@ -566,7 +566,7 @@ export const securityCommand = {
|
|
|
566
566
|
output.writeln();
|
|
567
567
|
output.writeln('Use --help with subcommands for more info');
|
|
568
568
|
output.writeln();
|
|
569
|
-
output.writeln(output.dim('Created with ❤️ by
|
|
569
|
+
output.writeln(output.dim('Created with ❤️ by cielolimitada.com'));
|
|
570
570
|
return { success: true };
|
|
571
571
|
},
|
|
572
572
|
};
|
|
@@ -79,8 +79,11 @@ async function getSystemStatus() {
|
|
|
79
79
|
catch {
|
|
80
80
|
// MCP not running
|
|
81
81
|
}
|
|
82
|
-
//
|
|
83
|
-
|
|
82
|
+
// #1349 — this used to call `memory_stats`, which returns none of
|
|
83
|
+
// `entries` / `size` / `performance`; dereferencing `.performance` threw
|
|
84
|
+
// and dropped the whole command into its all-zeros fallback. Use the
|
|
85
|
+
// detailed-stats tool, whose shape this actually needs.
|
|
86
|
+
const memoryStatus = await callMCPTool('memory_detailed-stats', {});
|
|
84
87
|
// Get task status
|
|
85
88
|
const taskStatus = await callMCPTool('task_summary', {});
|
|
86
89
|
return {
|
|
@@ -103,8 +106,7 @@ async function getSystemStatus() {
|
|
|
103
106
|
size: formatBytes(memoryStatus.size),
|
|
104
107
|
backend: memoryStatus.backend,
|
|
105
108
|
performance: {
|
|
106
|
-
searchTime: memoryStatus.performance.avgSearchTime
|
|
107
|
-
cacheHitRate: memoryStatus.performance.cacheHitRate
|
|
109
|
+
searchTime: memoryStatus.performance.avgSearchTime
|
|
108
110
|
}
|
|
109
111
|
},
|
|
110
112
|
tasks: taskStatus,
|
|
@@ -131,7 +133,7 @@ async function getSystemStatus() {
|
|
|
131
133
|
entries: 0,
|
|
132
134
|
size: '0 B',
|
|
133
135
|
backend: 'none',
|
|
134
|
-
performance: { searchTime:
|
|
136
|
+
performance: { searchTime: null }
|
|
135
137
|
},
|
|
136
138
|
tasks: { total: 0, pending: 0, running: 0, completed: 0, failed: 0 },
|
|
137
139
|
performance: {
|
|
@@ -211,8 +213,8 @@ function displayStatus(status) {
|
|
|
211
213
|
{ property: 'Backend', value: status.memory.backend },
|
|
212
214
|
{ property: 'Entries', value: status.memory.entries },
|
|
213
215
|
{ property: 'Size', value: status.memory.size },
|
|
214
|
-
|
|
215
|
-
{ property: '
|
|
216
|
+
// null means the probe never ran — say so rather than print 0.00ms.
|
|
217
|
+
{ property: 'Search Time', value: status.memory.performance.searchTime === null ? 'not measured' : `${status.memory.performance.searchTime.toFixed(2)}ms` }
|
|
216
218
|
]
|
|
217
219
|
});
|
|
218
220
|
output.writeln();
|
|
@@ -456,7 +458,9 @@ const tasksCommand = {
|
|
|
456
458
|
{ key: 'progress', header: 'Progress', width: 10 }
|
|
457
459
|
],
|
|
458
460
|
data: result.tasks.map(t => ({
|
|
459
|
-
id
|
|
461
|
+
// task_list projects the coordinator id as `taskId`; reading `id`
|
|
462
|
+
// left the ID column blank on every row (#1349).
|
|
463
|
+
id: t.taskId,
|
|
460
464
|
type: t.type,
|
|
461
465
|
status: formatHealth(t.status),
|
|
462
466
|
priority: t.priority,
|
|
@@ -483,7 +487,7 @@ const memoryCommand = {
|
|
|
483
487
|
description: 'Show detailed memory status',
|
|
484
488
|
action: async (ctx) => {
|
|
485
489
|
try {
|
|
486
|
-
const result = await callMCPTool('memory_detailed-stats', {});
|
|
490
|
+
const result = await callMCPTool('memory_detailed-stats', { measureLatency: true });
|
|
487
491
|
if (ctx.flags.format === 'json') {
|
|
488
492
|
output.printJson(result);
|
|
489
493
|
return { success: true, data: result };
|
|
@@ -500,7 +504,7 @@ const memoryCommand = {
|
|
|
500
504
|
{ property: 'Backend', value: result.backend },
|
|
501
505
|
{ property: 'Total Entries', value: result.entries.toLocaleString() },
|
|
502
506
|
{ property: 'Storage Size', value: formatBytes(result.size) },
|
|
503
|
-
{ property: 'HNSW Index', value: result.performance.hnswEnabled ? '
|
|
507
|
+
{ property: 'HNSW Index', value: result.performance.hnswEnabled ? 'Active' : 'Not built (search falls back to scan)' }
|
|
504
508
|
]
|
|
505
509
|
});
|
|
506
510
|
output.writeln();
|
|
@@ -511,9 +515,10 @@ const memoryCommand = {
|
|
|
511
515
|
{ key: 'value', header: 'Value', width: 20, align: 'right' }
|
|
512
516
|
],
|
|
513
517
|
data: [
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
{ metric: '
|
|
518
|
+
// Only metrics the store actually measures — write latency and
|
|
519
|
+
// cache hit rate are not tracked anywhere, so they are not shown.
|
|
520
|
+
{ metric: 'Avg Search Time', value: result.performance.avgSearchTime === null ? 'unavailable' : `${result.performance.avgSearchTime.toFixed(2)}ms` },
|
|
521
|
+
{ metric: 'Indexed Vectors', value: result.performance.indexedVectors.toLocaleString() }
|
|
517
522
|
]
|
|
518
523
|
});
|
|
519
524
|
return { success: true, data: result };
|
|
@@ -594,6 +594,14 @@ const retryCommand = {
|
|
|
594
594
|
taskId,
|
|
595
595
|
resetState
|
|
596
596
|
});
|
|
597
|
+
// The handler reports an unknown/unretryable task by returning
|
|
598
|
+
// success:false, not by throwing — so the catch below never sees it.
|
|
599
|
+
// Without this check, `task retry does-not-exist` printed
|
|
600
|
+
// "Task retried / New task ID: undefined" and exited 0 (#1349).
|
|
601
|
+
if (!result.success) {
|
|
602
|
+
output.printError(`Failed to retry task: ${result.error ?? 'unknown error'}`);
|
|
603
|
+
return { success: false, exitCode: 1, data: result };
|
|
604
|
+
}
|
|
597
605
|
output.writeln();
|
|
598
606
|
output.printSuccess(`Task ${taskId} retried`);
|
|
599
607
|
output.printInfo(`New task ID: ${result.newTaskId}`);
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI JSON config store — the file behind `flo config init|get|set|reset|
|
|
3
|
+
* export|import|providers`, and the file the healer's `Config File` check
|
|
4
|
+
* looks for.
|
|
5
|
+
*
|
|
6
|
+
* Two neighbouring config surfaces exist; this module is deliberately none of
|
|
7
|
+
* them:
|
|
8
|
+
* - `src/cli/config/moflo-config.ts` owns **moflo.yaml** (gates, sdd,
|
|
9
|
+
* daemon, memory backend). `flo config show|generate` operate on that.
|
|
10
|
+
* - `src/cli/shared/core/config/loader.ts` owns the `SystemConfig` schema
|
|
11
|
+
* loaded from bare `moflo.config.json`-style files in cwd / `~/.moflo`.
|
|
12
|
+
*
|
|
13
|
+
* This module owns the project-local `.moflo/config.json` — the first entry in
|
|
14
|
+
* the doctor's `Config File` candidate list and the path its auto-fix creates.
|
|
15
|
+
*
|
|
16
|
+
* Before this module existed the whole `flo config` family was display-only:
|
|
17
|
+
* `init` printed "Creating claude-flow.config.json..." and returned
|
|
18
|
+
* `success: true` without a single filesystem call, so `flo doctor --fix`
|
|
19
|
+
* reported the `Config File` warning as fixed on every run while the warning
|
|
20
|
+
* never cleared. Same defect class as the synthetic MCP responses labelled at
|
|
21
|
+
* the call site in #1324/#1325: a success report for work that never happened.
|
|
22
|
+
*
|
|
23
|
+
* @module moflo/cli/config/cli-config-store
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { dirname, join } from 'node:path';
|
|
27
|
+
import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
|
|
28
|
+
import { mofloDir } from '../services/moflo-paths.js';
|
|
29
|
+
/**
|
|
30
|
+
* Candidate filenames, canonical first. `doctor-checks-config.ts:checkConfigFile`
|
|
31
|
+
* imports this list rather than keeping its own, so the healer's check and this
|
|
32
|
+
* store cannot disagree about whether a config exists — the mismatch that let
|
|
33
|
+
* the old auto-fix "succeed" against a file the check couldn't see.
|
|
34
|
+
*
|
|
35
|
+
* `claude-flow.*` entries are LEGACY-CONFIG: pre-#699 names, still read so
|
|
36
|
+
* consumers upgrading from older moflo builds keep their existing file.
|
|
37
|
+
*/
|
|
38
|
+
export const CLI_CONFIG_CANDIDATES = [
|
|
39
|
+
join('.moflo', 'config.json'),
|
|
40
|
+
'moflo.config.json',
|
|
41
|
+
'claude-flow.config.json', // LEGACY-CONFIG: pre-#699 fallback
|
|
42
|
+
'.claude-flow.json', // LEGACY-CONFIG: pre-#699 fallback
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* YAML config filenames. Existence-checked only — no yaml parse — and shared
|
|
46
|
+
* with the doctor for the same anti-drift reason as the JSON list above.
|
|
47
|
+
*/
|
|
48
|
+
export const CLI_CONFIG_YAML_CANDIDATES = [
|
|
49
|
+
join('.moflo', 'config.yaml'),
|
|
50
|
+
join('.moflo', 'config.yml'),
|
|
51
|
+
'moflo.config.yaml',
|
|
52
|
+
'claude-flow.config.yaml', // LEGACY-CONFIG: pre-#699 fallback
|
|
53
|
+
];
|
|
54
|
+
/** Absolute path of the canonical config file for a project root. */
|
|
55
|
+
export function cliConfigPath(projectRoot) {
|
|
56
|
+
return join(mofloDir(projectRoot), 'config.json');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Absolute path of the config file that actually exists, canonical first, or
|
|
60
|
+
* `null` when the project has none.
|
|
61
|
+
*/
|
|
62
|
+
export function findCliConfigFile(projectRoot) {
|
|
63
|
+
for (const candidate of CLI_CONFIG_CANDIDATES) {
|
|
64
|
+
const path = join(projectRoot, candidate);
|
|
65
|
+
if (existsSync(path))
|
|
66
|
+
return path;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/** Default configuration. Values match what `flo config` reported pre-fix. */
|
|
71
|
+
export function defaultCliConfig(opts = {}) {
|
|
72
|
+
return {
|
|
73
|
+
version: '3.0.0',
|
|
74
|
+
v3Mode: opts.v3 ?? true,
|
|
75
|
+
sparc: opts.sparc ?? false,
|
|
76
|
+
agents: {
|
|
77
|
+
defaultType: 'coder',
|
|
78
|
+
maxConcurrent: 15,
|
|
79
|
+
autoSpawn: true,
|
|
80
|
+
timeout: 300,
|
|
81
|
+
},
|
|
82
|
+
swarm: {
|
|
83
|
+
topology: 'hybrid',
|
|
84
|
+
maxAgents: 15,
|
|
85
|
+
autoScale: true,
|
|
86
|
+
coordinationStrategy: 'consensus',
|
|
87
|
+
},
|
|
88
|
+
memory: {
|
|
89
|
+
backend: 'hybrid',
|
|
90
|
+
path: './data/memory',
|
|
91
|
+
cacheSize: 256,
|
|
92
|
+
enableHNSW: true,
|
|
93
|
+
},
|
|
94
|
+
mcp: {
|
|
95
|
+
transport: 'stdio',
|
|
96
|
+
autoStart: true,
|
|
97
|
+
tools: 'all',
|
|
98
|
+
},
|
|
99
|
+
providers: [
|
|
100
|
+
{ name: 'anthropic', model: 'claude-sonnet-5', priority: 1, enabled: true },
|
|
101
|
+
{ name: 'openrouter', model: 'claude-sonnet-5', priority: 2, enabled: false },
|
|
102
|
+
{ name: 'ollama', model: 'llama3.2', priority: 3, enabled: false },
|
|
103
|
+
{ name: 'gemini', model: 'gemini-2.0-flash', priority: 4, enabled: false },
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Thrown when a config file exists but cannot be parsed. */
|
|
108
|
+
export class CliConfigParseError extends Error {
|
|
109
|
+
path;
|
|
110
|
+
constructor(path, cause) {
|
|
111
|
+
super(`Invalid JSON in ${path}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
112
|
+
this.path = path;
|
|
113
|
+
this.name = 'CliConfigParseError';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function isPlainObject(value) {
|
|
117
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
118
|
+
}
|
|
119
|
+
/** Deep-merge `patch` over `base`. Arrays are replaced wholesale, not merged. */
|
|
120
|
+
function deepMerge(base, patch) {
|
|
121
|
+
if (!isPlainObject(patch))
|
|
122
|
+
return base;
|
|
123
|
+
if (!isPlainObject(base))
|
|
124
|
+
return patch;
|
|
125
|
+
const result = { ...base };
|
|
126
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
127
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype')
|
|
128
|
+
continue;
|
|
129
|
+
const current = result[key];
|
|
130
|
+
result[key] = isPlainObject(value) && isPlainObject(current) ? deepMerge(current, value) : value;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Load the project's config, merged over defaults. `path` is `null` when no
|
|
136
|
+
* file exists — callers get usable defaults either way, and can tell the
|
|
137
|
+
* difference.
|
|
138
|
+
*
|
|
139
|
+
* @throws {CliConfigParseError} when a config file exists but is not valid JSON.
|
|
140
|
+
*/
|
|
141
|
+
export function loadCliConfig(projectRoot) {
|
|
142
|
+
const path = findCliConfigFile(projectRoot);
|
|
143
|
+
if (!path)
|
|
144
|
+
return { config: defaultCliConfig(), path: null };
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
throw new CliConfigParseError(path, err);
|
|
151
|
+
}
|
|
152
|
+
return { config: coerceShape(deepMerge(defaultCliConfig(), parsed)), path };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Restore any section whose type the file contradicts. `deepMerge` is
|
|
156
|
+
* structural, not validating, so a hand-edited `"providers": "x"` would
|
|
157
|
+
* survive as a `CliConfig` that lies about its own type and throws the first
|
|
158
|
+
* time something calls `.find` on it. Falling back to the default for just the
|
|
159
|
+
* malformed section keeps the rest of the user's file intact.
|
|
160
|
+
*/
|
|
161
|
+
function coerceShape(config) {
|
|
162
|
+
const defaults = defaultCliConfig();
|
|
163
|
+
const repaired = { ...config };
|
|
164
|
+
if (!Array.isArray(repaired.providers))
|
|
165
|
+
repaired.providers = defaults.providers;
|
|
166
|
+
for (const section of ['agents', 'swarm', 'memory', 'mcp']) {
|
|
167
|
+
if (!isPlainObject(repaired[section])) {
|
|
168
|
+
repaired[section] = defaults[section];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return repaired;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Write `config` and return the path written. Writes to the existing file when
|
|
175
|
+
* there is one (so a consumer's root-level `moflo.config.json` keeps being the
|
|
176
|
+
* file they edit), otherwise creates the canonical `.moflo/config.json`.
|
|
177
|
+
*/
|
|
178
|
+
export function saveCliConfig(projectRoot, config, opts = {}) {
|
|
179
|
+
const target = opts.path ?? findCliConfigFile(projectRoot) ?? cliConfigPath(projectRoot);
|
|
180
|
+
writeJsonFile(target, config);
|
|
181
|
+
return target;
|
|
182
|
+
}
|
|
183
|
+
/** Write `content` to `path` atomically, creating parent dirs as needed. */
|
|
184
|
+
export function writeTextFile(path, content) {
|
|
185
|
+
// recursive mkdir is already idempotent — no existence check needed.
|
|
186
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
187
|
+
atomicWriteFileSync(path, content);
|
|
188
|
+
}
|
|
189
|
+
/** Serialize `value` as pretty JSON to `path`, creating parent dirs. */
|
|
190
|
+
export function writeJsonFile(path, value) {
|
|
191
|
+
writeTextFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Split a dot-notation key, rejecting prototype-polluting segments and empty
|
|
195
|
+
* ones. Empty segments are rejected rather than filtered out: silently
|
|
196
|
+
* accepting `swarm..topology` contradicts the loud-failure contract that
|
|
197
|
+
* `setConfigValue` relies on to catch typos.
|
|
198
|
+
*/
|
|
199
|
+
function splitKey(key) {
|
|
200
|
+
const segments = key.split('.');
|
|
201
|
+
if (segments.length === 0 || segments.some((s) => s.length === 0))
|
|
202
|
+
return null;
|
|
203
|
+
if (segments.some((s) => s === '__proto__' || s === 'constructor' || s === 'prototype'))
|
|
204
|
+
return null;
|
|
205
|
+
return segments;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Read a dot-notation key (`swarm.topology`, `providers.0.enabled`).
|
|
209
|
+
* `found: false` for keys the config does not define.
|
|
210
|
+
*/
|
|
211
|
+
export function getConfigValue(config, key) {
|
|
212
|
+
const segments = splitKey(key);
|
|
213
|
+
if (!segments)
|
|
214
|
+
return { found: false };
|
|
215
|
+
let current = config;
|
|
216
|
+
for (const segment of segments) {
|
|
217
|
+
if (current === null || typeof current !== 'object')
|
|
218
|
+
return { found: false };
|
|
219
|
+
if (Array.isArray(current)) {
|
|
220
|
+
const index = Number(segment);
|
|
221
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length)
|
|
222
|
+
return { found: false };
|
|
223
|
+
current = current[index];
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment))
|
|
227
|
+
return { found: false };
|
|
228
|
+
current = current[segment];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { found: true, value: current };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Coerce a CLI string to the type of the value already at that key, so
|
|
235
|
+
* `flo config set swarm.maxAgents 20` stores the number 20, not "20".
|
|
236
|
+
*/
|
|
237
|
+
function coerceValue(raw, current) {
|
|
238
|
+
if (typeof current === 'number') {
|
|
239
|
+
const parsed = Number(raw);
|
|
240
|
+
if (!Number.isFinite(parsed))
|
|
241
|
+
return { ok: false, error: `expected a number, got "${raw}"` };
|
|
242
|
+
return { ok: true, value: parsed };
|
|
243
|
+
}
|
|
244
|
+
if (typeof current === 'boolean') {
|
|
245
|
+
if (/^(true|1|yes|on)$/i.test(raw))
|
|
246
|
+
return { ok: true, value: true };
|
|
247
|
+
if (/^(false|0|no|off)$/i.test(raw))
|
|
248
|
+
return { ok: true, value: false };
|
|
249
|
+
return { ok: false, error: `expected a boolean, got "${raw}"` };
|
|
250
|
+
}
|
|
251
|
+
if (typeof current === 'object' && current !== null) {
|
|
252
|
+
return { ok: false, error: 'is a section, not a value — set one of its keys instead' };
|
|
253
|
+
}
|
|
254
|
+
return { ok: true, value: raw };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Set a dot-notation key in place. Rejects keys the config does not already
|
|
258
|
+
* define — a typo should fail loudly, not silently create a key nothing reads.
|
|
259
|
+
*/
|
|
260
|
+
export function setConfigValue(config, key, raw) {
|
|
261
|
+
const segments = splitKey(key);
|
|
262
|
+
if (!segments)
|
|
263
|
+
return { ok: false, error: `invalid key: "${key}"` };
|
|
264
|
+
const existing = getConfigValue(config, key);
|
|
265
|
+
if (!existing.found)
|
|
266
|
+
return { ok: false, error: `unknown key: ${key}` };
|
|
267
|
+
const coerced = coerceValue(raw, existing.value);
|
|
268
|
+
if (!coerced.ok)
|
|
269
|
+
return coerced;
|
|
270
|
+
const parentSegments = segments.slice(0, -1);
|
|
271
|
+
const leaf = segments[segments.length - 1];
|
|
272
|
+
const parent = parentSegments.length === 0
|
|
273
|
+
? { found: true, value: config }
|
|
274
|
+
: getConfigValue(config, parentSegments.join('.'));
|
|
275
|
+
if (!parent.found || parent.value === null || typeof parent.value !== 'object') {
|
|
276
|
+
return { ok: false, error: `unknown key: ${key}` };
|
|
277
|
+
}
|
|
278
|
+
if (Array.isArray(parent.value)) {
|
|
279
|
+
parent.value[Number(leaf)] = coerced.value;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
parent.value[leaf] = coerced.value;
|
|
283
|
+
}
|
|
284
|
+
return { ok: true, value: coerced.value };
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Flatten to dotted leaf keys for tabular display. An EMPTY object or array is
|
|
288
|
+
* itself a leaf — recursing over zero entries would otherwise drop the key
|
|
289
|
+
* entirely, so `providers: []` would vanish from `flo config get` and read as
|
|
290
|
+
* "not configured" rather than "configured empty".
|
|
291
|
+
*/
|
|
292
|
+
export function flattenConfig(value, prefix = '') {
|
|
293
|
+
const flat = {};
|
|
294
|
+
const key = prefix.replace(/\.$/, '');
|
|
295
|
+
if (Array.isArray(value)) {
|
|
296
|
+
if (value.length === 0) {
|
|
297
|
+
flat[key] = '[]';
|
|
298
|
+
return flat;
|
|
299
|
+
}
|
|
300
|
+
value.forEach((item, index) => Object.assign(flat, flattenConfig(item, `${prefix}${index}.`)));
|
|
301
|
+
return flat;
|
|
302
|
+
}
|
|
303
|
+
if (isPlainObject(value)) {
|
|
304
|
+
const entries = Object.entries(value);
|
|
305
|
+
if (entries.length === 0) {
|
|
306
|
+
flat[key] = '{}';
|
|
307
|
+
return flat;
|
|
308
|
+
}
|
|
309
|
+
for (const [childKey, child] of entries) {
|
|
310
|
+
Object.assign(flat, flattenConfig(child, `${prefix}${childKey}.`));
|
|
311
|
+
}
|
|
312
|
+
return flat;
|
|
313
|
+
}
|
|
314
|
+
flat[key] = value;
|
|
315
|
+
return flat;
|
|
316
|
+
}
|
|
317
|
+
/** Config sections `flo config reset --section` accepts. */
|
|
318
|
+
export const RESETTABLE_SECTIONS = ['agents', 'swarm', 'memory', 'mcp', 'providers', 'all'];
|
|
319
|
+
/** Return `config` with `section` (or everything) restored to defaults. */
|
|
320
|
+
export function resetSection(config, section) {
|
|
321
|
+
const defaults = defaultCliConfig({ v3: config.v3Mode, sparc: config.sparc });
|
|
322
|
+
if (section === 'all')
|
|
323
|
+
return defaults;
|
|
324
|
+
return { ...config, [section]: defaults[section] };
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=cli-config-store.js.map
|
|
@@ -9,6 +9,7 @@ import * as os from 'os';
|
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as fs from 'fs/promises';
|
|
11
11
|
import { errorDetail } from '../../shared/utils/error-detail.js';
|
|
12
|
+
import { readLoadAverage } from '../../shared/utils/load-average.js';
|
|
12
13
|
// ============================================================================
|
|
13
14
|
// Security Constants
|
|
14
15
|
// ============================================================================
|
|
@@ -502,7 +503,9 @@ export class WorkerManager extends EventEmitter {
|
|
|
502
503
|
issues: securityResult?.totalIssues ?? 0,
|
|
503
504
|
},
|
|
504
505
|
performance: {
|
|
505
|
-
|
|
506
|
+
// Null both when the worker has not run yet and when the platform has
|
|
507
|
+
// no load average — in either case there is nothing measured to show.
|
|
508
|
+
loadAvg: perfResult?.cpu?.loadAvg ?? null,
|
|
506
509
|
},
|
|
507
510
|
alerts: this.alerts.filter(a => a.severity === AlertSeverity.Critical).slice(-5),
|
|
508
511
|
lastUpdate: new Date().toISOString(),
|
|
@@ -537,8 +540,10 @@ export class WorkerManager extends EventEmitter {
|
|
|
537
540
|
const secIcon = data.security.status === 'critical' ? '🚨' :
|
|
538
541
|
data.security.status === 'warning' ? '⚠️' : '🛡️';
|
|
539
542
|
parts.push(`${secIcon}${data.security.issues}`);
|
|
540
|
-
// Performance
|
|
541
|
-
|
|
543
|
+
// Performance — 'n/a' rather than a zero, for the same reason the field
|
|
544
|
+
// is nullable: a statusline is exactly where an unmeasured value gets
|
|
545
|
+
// read as a measured one.
|
|
546
|
+
parts.push(`⚡${data.performance.loadAvg ?? 'n/a'}`);
|
|
542
547
|
return parts.join(' │ ');
|
|
543
548
|
}
|
|
544
549
|
// =========================================================================
|
|
@@ -770,9 +775,9 @@ export function createPerformanceWorker(projectRoot) {
|
|
|
770
775
|
const totalMem = os.totalmem();
|
|
771
776
|
const freeMem = os.freemem();
|
|
772
777
|
const memPct = Math.round((1 - freeMem / totalMem) * 100);
|
|
773
|
-
// CPU load
|
|
778
|
+
// CPU load — null where the platform has none (#1358), never "0.00"
|
|
774
779
|
const cpus = os.cpus();
|
|
775
|
-
const loadAvg = os.loadavg()
|
|
780
|
+
const loadAvg = readLoadAverage(os.loadavg(), os.platform());
|
|
776
781
|
return {
|
|
777
782
|
worker: 'performance',
|
|
778
783
|
success: true,
|
|
@@ -786,9 +791,8 @@ export function createPerformanceWorker(projectRoot) {
|
|
|
786
791
|
},
|
|
787
792
|
cpu: {
|
|
788
793
|
cores: cpus.length,
|
|
789
|
-
loadAvg: loadAvg.toFixed(2),
|
|
794
|
+
loadAvg: loadAvg === null ? null : loadAvg[0].toFixed(2),
|
|
790
795
|
},
|
|
791
|
-
speedup: '1.0x', // Placeholder
|
|
792
796
|
},
|
|
793
797
|
};
|
|
794
798
|
};
|
|
@@ -800,7 +804,9 @@ export function createHealthWorker(projectRoot) {
|
|
|
800
804
|
const freeMem = os.freemem();
|
|
801
805
|
const memPct = Math.round((1 - freeMem / totalMem) * 100);
|
|
802
806
|
const uptime = os.uptime();
|
|
803
|
-
|
|
807
|
+
// null where the platform has none (#1358) — three zeros would read as an
|
|
808
|
+
// idle machine rather than as an absent reading.
|
|
809
|
+
const loadAvg = readLoadAverage(os.loadavg(), os.platform());
|
|
804
810
|
// Disk space (cross-platform approximation)
|
|
805
811
|
let diskPct = 0;
|
|
806
812
|
let diskFree = 'N/A';
|
|
@@ -825,7 +831,7 @@ export function createHealthWorker(projectRoot) {
|
|
|
825
831
|
disk: { usedPct: diskPct, free: diskFree },
|
|
826
832
|
system: {
|
|
827
833
|
uptime: Math.round(uptime / 3600),
|
|
828
|
-
loadAvg: loadAvg.map(l => l.toFixed(2)),
|
|
834
|
+
loadAvg: loadAvg === null ? null : loadAvg.map(l => l.toFixed(2)),
|
|
829
835
|
platform: os.platform(),
|
|
830
836
|
arch: os.arch(),
|
|
831
837
|
},
|