moflo 4.10.12 → 4.10.14
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/guidance/shipped/moflo-core-guidance.md +16 -0
- package/.claude/guidance/shipped/moflo-memory-protocol.md +171 -11
- package/.claude/helpers/gate.cjs +139 -14
- package/.claude/skills/publish/SKILL.md +46 -8
- package/bin/gate.cjs +139 -14
- package/bin/lib/moflo-paths.mjs +74 -4
- package/bin/session-start-launcher.mjs +173 -5
- package/dist/src/cli/commands/doctor-checks-config.js +126 -3
- package/dist/src/cli/commands/doctor-fixes.js +176 -1
- package/dist/src/cli/commands/doctor-registry.js +9 -1
- package/dist/src/cli/commands/init.js +33 -0
- package/dist/src/cli/init/claudemd-generator.js +6 -2
- package/dist/src/cli/init/helpers-generator.js +23 -3
- package/dist/src/cli/init/moflo-init.js +4 -2
- package/dist/src/cli/init/settings-generator.js +9 -3
- package/dist/src/cli/mcp-server.js +104 -2
- package/dist/src/cli/services/hook-block-hash.js +5 -2
- package/dist/src/cli/services/hook-wiring.js +38 -4
- package/dist/src/cli/services/moflo-paths.js +16 -0
- package/dist/src/cli/services/project-root.js +84 -25
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -62,6 +62,56 @@ function buildDefaultOptions() {
|
|
|
62
62
|
daemonize: false,
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Best-effort append to the MCP log file. Errors are swallowed — logging must
|
|
67
|
+
* never crash the MCP server. Used to capture server start, project root
|
|
68
|
+
* resolution, and per-request timing so we never repeat the 18-hour
|
|
69
|
+
* diagnostic blind window from #1174.
|
|
70
|
+
*
|
|
71
|
+
* Rotation: when the log exceeds {@link MCP_LOG_ROTATE_BYTES}, rename it to
|
|
72
|
+
* `<logFile>.1` (overwriting any previous rotated file). One rotation level
|
|
73
|
+
* keeps the most recent ~50MB of activity plus the previous ~50MB. A long-
|
|
74
|
+
* running session with heavy MCP traffic can otherwise write hundreds of MB.
|
|
75
|
+
*
|
|
76
|
+
* Cross-platform: uses fs.appendFileSync + fs.mkdirSync({recursive:true}) +
|
|
77
|
+
* fs.renameSync. All three work identically on Windows/macOS/Linux. Windows
|
|
78
|
+
* note: renameSync can fail with EBUSY if the file is open by another
|
|
79
|
+
* process; we use append-only here so no other writer should hold it, but
|
|
80
|
+
* the rename is wrapped in a try/catch so a transient rotation failure can't
|
|
81
|
+
* crash the MCP server (next append succeeds; rotation retries next time).
|
|
82
|
+
*/
|
|
83
|
+
const MCP_LOG_ROTATE_BYTES = 50 * 1024 * 1024;
|
|
84
|
+
// Throttle rotation checks: batch spell scenarios can write thousands of
|
|
85
|
+
// MCP requests per session. statSync per append is wasted syscalls — bucket
|
|
86
|
+
// the check every N writes (and always on the very first call so cold-start
|
|
87
|
+
// rotation still fires).
|
|
88
|
+
const MCP_LOG_ROTATE_CHECK_EVERY = 100;
|
|
89
|
+
let mcpAppendsSinceRotateCheck = MCP_LOG_ROTATE_CHECK_EVERY;
|
|
90
|
+
function safeAppendMcpLog(logFile, event) {
|
|
91
|
+
try {
|
|
92
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
93
|
+
// Rotate before append so the very write that crosses the threshold
|
|
94
|
+
// lands in the fresh file rather than the rotated one.
|
|
95
|
+
if (++mcpAppendsSinceRotateCheck >= MCP_LOG_ROTATE_CHECK_EVERY) {
|
|
96
|
+
mcpAppendsSinceRotateCheck = 0;
|
|
97
|
+
try {
|
|
98
|
+
const stats = fs.statSync(logFile);
|
|
99
|
+
if (stats.size >= MCP_LOG_ROTATE_BYTES) {
|
|
100
|
+
const rotated = `${logFile}.1`;
|
|
101
|
+
try {
|
|
102
|
+
fs.unlinkSync(rotated);
|
|
103
|
+
}
|
|
104
|
+
catch { /* may not exist */ }
|
|
105
|
+
fs.renameSync(logFile, rotated);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch { /* file may not exist yet; first write creates it */ }
|
|
109
|
+
}
|
|
110
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...event }) + '\n';
|
|
111
|
+
fs.appendFileSync(logFile, line, 'utf-8');
|
|
112
|
+
}
|
|
113
|
+
catch { /* logging must never throw */ }
|
|
114
|
+
}
|
|
65
115
|
/**
|
|
66
116
|
* MCP Server Manager
|
|
67
117
|
*
|
|
@@ -253,6 +303,27 @@ export class MCPServerManager extends EventEmitter {
|
|
|
253
303
|
sessionId,
|
|
254
304
|
version: VERSION,
|
|
255
305
|
}));
|
|
306
|
+
// Persistent log (#1174). The MCP server previously logged only to stderr,
|
|
307
|
+
// which Claude Code drops on the floor unless the user runs `claude
|
|
308
|
+
// --debug`. The 18-hour daemon-island incident took that long to diagnose
|
|
309
|
+
// partly because no on-disk log captured server start, the resolved
|
|
310
|
+
// project root, or the request stream. Default-on JSONL log fixes that.
|
|
311
|
+
const resolvedProjectRoot = findProjectRoot();
|
|
312
|
+
safeAppendMcpLog(this.options.logFile, {
|
|
313
|
+
event: 'server.start',
|
|
314
|
+
sessionId,
|
|
315
|
+
version: VERSION,
|
|
316
|
+
pid: process.pid,
|
|
317
|
+
ppid: process.ppid,
|
|
318
|
+
platform: process.platform,
|
|
319
|
+
arch: process.arch,
|
|
320
|
+
nodeVersion: process.version,
|
|
321
|
+
cwd: process.cwd(),
|
|
322
|
+
projectRoot: resolvedProjectRoot,
|
|
323
|
+
claudeProjectDir: process.env.CLAUDE_PROJECT_DIR || null,
|
|
324
|
+
pidFile: this.options.pidFile,
|
|
325
|
+
logFile: this.options.logFile,
|
|
326
|
+
});
|
|
256
327
|
// Send server initialization notification
|
|
257
328
|
console.log(JSON.stringify({
|
|
258
329
|
jsonrpc: '2.0',
|
|
@@ -380,8 +451,16 @@ export class MCPServerManager extends EventEmitter {
|
|
|
380
451
|
},
|
|
381
452
|
},
|
|
382
453
|
};
|
|
383
|
-
case 'tools/list':
|
|
454
|
+
case 'tools/list': {
|
|
455
|
+
const listStart = performance.now();
|
|
384
456
|
const tools = listMCPTools();
|
|
457
|
+
const durationMs = performance.now() - listStart;
|
|
458
|
+
safeAppendMcpLog(this.options.logFile, {
|
|
459
|
+
event: 'tools/list',
|
|
460
|
+
sessionId,
|
|
461
|
+
count: tools.length,
|
|
462
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
463
|
+
});
|
|
385
464
|
return {
|
|
386
465
|
jsonrpc: '2.0',
|
|
387
466
|
id: message.id,
|
|
@@ -393,18 +472,32 @@ export class MCPServerManager extends EventEmitter {
|
|
|
393
472
|
})),
|
|
394
473
|
},
|
|
395
474
|
};
|
|
396
|
-
|
|
475
|
+
}
|
|
476
|
+
case 'tools/call': {
|
|
397
477
|
const toolName = params.name;
|
|
398
478
|
const toolParams = (params.arguments || {});
|
|
399
479
|
if (!hasTool(toolName)) {
|
|
480
|
+
safeAppendMcpLog(this.options.logFile, {
|
|
481
|
+
event: 'tools/call.unknown',
|
|
482
|
+
sessionId,
|
|
483
|
+
toolName,
|
|
484
|
+
});
|
|
400
485
|
return {
|
|
401
486
|
jsonrpc: '2.0',
|
|
402
487
|
id: message.id,
|
|
403
488
|
error: { code: -32601, message: `Tool not found: ${toolName}` },
|
|
404
489
|
};
|
|
405
490
|
}
|
|
491
|
+
const callStart = performance.now();
|
|
406
492
|
try {
|
|
407
493
|
const result = await callMCPTool(toolName, toolParams, { sessionId });
|
|
494
|
+
const durationMs = performance.now() - callStart;
|
|
495
|
+
safeAppendMcpLog(this.options.logFile, {
|
|
496
|
+
event: 'tools/call.ok',
|
|
497
|
+
sessionId,
|
|
498
|
+
toolName,
|
|
499
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
500
|
+
});
|
|
408
501
|
return {
|
|
409
502
|
jsonrpc: '2.0',
|
|
410
503
|
id: message.id,
|
|
@@ -412,6 +505,14 @@ export class MCPServerManager extends EventEmitter {
|
|
|
412
505
|
};
|
|
413
506
|
}
|
|
414
507
|
catch (error) {
|
|
508
|
+
const durationMs = performance.now() - callStart;
|
|
509
|
+
safeAppendMcpLog(this.options.logFile, {
|
|
510
|
+
event: 'tools/call.error',
|
|
511
|
+
sessionId,
|
|
512
|
+
toolName,
|
|
513
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
514
|
+
error: error instanceof Error ? error.message : 'Tool execution failed',
|
|
515
|
+
});
|
|
415
516
|
return {
|
|
416
517
|
jsonrpc: '2.0',
|
|
417
518
|
id: message.id,
|
|
@@ -421,6 +522,7 @@ export class MCPServerManager extends EventEmitter {
|
|
|
421
522
|
},
|
|
422
523
|
};
|
|
423
524
|
}
|
|
525
|
+
}
|
|
424
526
|
case 'notifications/initialized':
|
|
425
527
|
// Client notification - no response needed
|
|
426
528
|
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Client initialized`);
|
|
@@ -56,7 +56,9 @@ export function getReferenceHookBlock() {
|
|
|
56
56
|
{ matcher: '^(Glob|Grep)$', hooks: [gateHook('check-before-scan', 3000)] },
|
|
57
57
|
{ matcher: '^Read$', hooks: [gateHook('check-before-read', 3000)] },
|
|
58
58
|
{
|
|
59
|
-
|
|
59
|
+
// #1171 — widened to cover `PowerShell` tool; without this PS-tool
|
|
60
|
+
// calls bypass the dangerous/pr/memory gates on Windows.
|
|
61
|
+
matcher: '^(Bash|PowerShell)$',
|
|
60
62
|
hooks: [
|
|
61
63
|
gateHook('check-dangerous-command', 2000),
|
|
62
64
|
gateHook('check-before-pr', 2000),
|
|
@@ -78,7 +80,8 @@ export function getReferenceHookBlock() {
|
|
|
78
80
|
{ matcher: '^TaskCreate$', hooks: [gateCjs('record-task-created', 2000)] },
|
|
79
81
|
{
|
|
80
82
|
// #1132 — check-bash-memory moved to PreToolUse (above).
|
|
81
|
-
|
|
83
|
+
// #1171 — widened to cover `PowerShell` tool.
|
|
84
|
+
matcher: '^(Bash|PowerShell)$',
|
|
82
85
|
hooks: [gateHook('record-test-run', 2000)],
|
|
83
86
|
},
|
|
84
87
|
{ matcher: '^Skill$', hooks: [gateHook('record-skill-run', 2000)] },
|
|
@@ -45,8 +45,10 @@ export const REQUIRED_HOOK_WIRING = [
|
|
|
45
45
|
export const HOOK_ENTRY_MAP = {
|
|
46
46
|
'check-before-scan': { event: 'PreToolUse', matcher: '^(Glob|Grep)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-before-scan', timeout: 3000 } },
|
|
47
47
|
'check-before-read': { event: 'PreToolUse', matcher: '^Read$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-before-read', timeout: 3000 } },
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// #1171 — matchers widened to cover `PowerShell` tool; the gate logic was
|
|
49
|
+
// always shell-agnostic but the matcher was Bash-anchored, leaving a bypass.
|
|
50
|
+
'check-dangerous-command': { event: 'PreToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-dangerous-command', timeout: 2000 } },
|
|
51
|
+
'check-before-pr': { event: 'PreToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-before-pr', timeout: 2000 } },
|
|
50
52
|
'record-task-created': { event: 'PostToolUse', matcher: '^TaskCreate$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" record-task-created', timeout: 2000 } },
|
|
51
53
|
// record-memory-searched MUST go through gate-hook.mjs (not gate.cjs directly)
|
|
52
54
|
// — the wrapper forwards Claude Code's session_id as HOOK_SESSION_ID, which
|
|
@@ -58,8 +60,10 @@ export const HOOK_ENTRY_MAP = {
|
|
|
58
60
|
'record-memory-searched': { event: 'PostToolUse', matcher: '^mcp__moflo__memory_(search|retrieve|list|stats|store)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-memory-searched', timeout: 3000 } },
|
|
59
61
|
'check-task-transition': { event: 'PostToolUse', matcher: '^TaskUpdate$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" check-task-transition', timeout: 2000 } },
|
|
60
62
|
'record-learnings-stored': { event: 'PostToolUse', matcher: '^mcp__moflo__memory_store$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" record-learnings-stored', timeout: 2000 } },
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
// #1171 — widened to ^(Bash|PowerShell)$ so PS reads / PS-invoked tests credit
|
|
64
|
+
// the same gates as Bash. Name kept as `check-bash-memory` for backwards compat.
|
|
65
|
+
'check-bash-memory': { event: 'PostToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-bash-memory', timeout: 2000 } },
|
|
66
|
+
'record-test-run': { event: 'PostToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-test-run', timeout: 2000 } },
|
|
63
67
|
'record-skill-run': { event: 'PostToolUse', matcher: '^Skill$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-skill-run', timeout: 2000 } },
|
|
64
68
|
'reset-edit-gates': { event: 'PostToolUse', matcher: '^(Write|Edit|MultiEdit)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" reset-edit-gates', timeout: 2000 } },
|
|
65
69
|
// #931 — Agent-time advisory; never blocks. Pulled the TaskCreate REMINDER
|
|
@@ -165,6 +169,36 @@ export const MATCHER_REWRITE_RULES = [
|
|
|
165
169
|
to: '^mcp__moflo__memory_(search|retrieve|list|stats|store)$',
|
|
166
170
|
cmdContains: 'record-memory-searched',
|
|
167
171
|
},
|
|
172
|
+
// Issue #1171 — widen Bash-only matchers to cover the dedicated `PowerShell`
|
|
173
|
+
// tool Claude Code exposes on Windows. The gate logic itself was already
|
|
174
|
+
// shell-agnostic (gate.cjs READ_LIKE_BASH_RE matched `Get-Content`/`Select-String`/etc.)
|
|
175
|
+
// but a Bash-anchored matcher meant PS-tool calls never reached the gate.
|
|
176
|
+
// One rewrite per gate command keeps the `cmdContains` guard precise, so an
|
|
177
|
+
// unrelated user-customised `^Bash$` block doesn't get widened.
|
|
178
|
+
{
|
|
179
|
+
name: '#1171: widen check-dangerous-command matcher to PowerShell',
|
|
180
|
+
from: '^Bash$',
|
|
181
|
+
to: '^(Bash|PowerShell)$',
|
|
182
|
+
cmdContains: 'check-dangerous-command',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: '#1171: widen check-before-pr matcher to PowerShell',
|
|
186
|
+
from: '^Bash$',
|
|
187
|
+
to: '^(Bash|PowerShell)$',
|
|
188
|
+
cmdContains: 'check-before-pr',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: '#1171: widen check-bash-memory matcher to PowerShell',
|
|
192
|
+
from: '^Bash$',
|
|
193
|
+
to: '^(Bash|PowerShell)$',
|
|
194
|
+
cmdContains: 'check-bash-memory',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: '#1171: widen record-test-run matcher to PowerShell',
|
|
198
|
+
from: '^Bash$',
|
|
199
|
+
to: '^(Bash|PowerShell)$',
|
|
200
|
+
cmdContains: 'record-test-run',
|
|
201
|
+
},
|
|
168
202
|
];
|
|
169
203
|
/**
|
|
170
204
|
* Apply HOOK_REWRITE_RULES to every hook command in `settings.hooks.*`.
|
|
@@ -104,4 +104,20 @@ export function memoryDbCandidatePaths(projectRoot) {
|
|
|
104
104
|
join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
|
|
105
105
|
];
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Common skip-list for any walk that enumerates a project's children looking
|
|
109
|
+
* for moflo state. Shared by `bin/session-start-launcher.mjs` (depth-1 walk)
|
|
110
|
+
* and `doctor-checks-config.ts` (depth-5 BFS) so the two can't silently
|
|
111
|
+
* diverge.
|
|
112
|
+
*
|
|
113
|
+
* Twin of `bin/lib/moflo-paths.mjs:COMMON_WALK_SKIP_NAMES`. Matched
|
|
114
|
+
* case-insensitively at every call site — Windows NTFS + macOS APFS are
|
|
115
|
+
* case-insensitive by default.
|
|
116
|
+
*/
|
|
117
|
+
export const COMMON_WALK_SKIP_NAMES = new Set([
|
|
118
|
+
'node_modules', '.git', '.svn', '.hg',
|
|
119
|
+
'dist', 'build', 'out', 'target', '.next', '.nuxt', '.cache',
|
|
120
|
+
'coverage', '.idea', '.vscode', '.turbo', '.svelte-kit',
|
|
121
|
+
'vendor', '__pycache__', '.venv', 'venv', '.tox',
|
|
122
|
+
]);
|
|
107
123
|
//# sourceMappingURL=moflo-paths.js.map
|
|
@@ -8,32 +8,73 @@
|
|
|
8
8
|
* resolve through this single algorithm or its JS twin — otherwise different
|
|
9
9
|
* writers land on different DBs and the bridge reads stale data.
|
|
10
10
|
*
|
|
11
|
-
* Algorithm (#1057) —
|
|
11
|
+
* Algorithm (#1057, #1174) — three-pass walk so memory markers always win
|
|
12
|
+
* across the ENTIRE ancestor chain (not just at the first level they appear):
|
|
12
13
|
* 1. `process.env.CLAUDE_PROJECT_DIR`, if set (Claude Code / explicit override).
|
|
13
|
-
* 2. **
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
14
|
+
* 2. **Pass A — memory markers (topmost wins).** Walk from
|
|
15
|
+
* `opts.cwd ?? process.cwd()` up to the filesystem root, collecting EVERY
|
|
16
|
+
* level that has `.moflo/moflo.db` OR `.swarm/memory.db`. Return the
|
|
17
|
+
* topmost (highest ancestor) match. This is the #1174 fix — pre-#1174 the
|
|
18
|
+
* walk stopped at the nearest hit, fragmenting monorepos into daemon
|
|
19
|
+
* islands.
|
|
20
|
+
* 3. **Pass B — project marker pair (nearest wins).** Only reached when no
|
|
21
|
+
* moflo state exists anywhere up the tree. Walk again looking for
|
|
22
|
+
* `<dir>/CLAUDE.md` AND `<dir>/package.json` at the same level; return
|
|
23
|
+
* the nearest match.
|
|
24
|
+
* 4. **Pass C — bare project markers (nearest wins).** Walk again looking
|
|
25
|
+
* for `<dir>/package.json` OR `<dir>/.git`; return the nearest match.
|
|
26
|
+
* 5. Fall back to `opts.cwd ?? process.cwd()`.
|
|
25
27
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
28
|
+
* `node_modules` segments are always skipped (npx run can land cwd inside one).
|
|
29
|
+
*
|
|
30
|
+
* Why topmost (Pass A)? When a monorepo has nested `.moflo/moflo.db` directories
|
|
31
|
+
* — typically because `flo init` was run from a subworkspace before #1174 — the
|
|
32
|
+
* MCP server, daemon, CLI, and gate hooks ALL must agree on a single anchor.
|
|
33
|
+
* Topmost wins means the root daemon is canonical; sub-daemons become
|
|
34
|
+
* detectable residue that `flo doctor --fix` archives. Nearest-wins fragments
|
|
35
|
+
* state silently because every cwd resolves to a different anchor.
|
|
36
|
+
*
|
|
37
|
+
* Why nearest (Pass B/C)? Pass B/C only fires when there's no moflo state at
|
|
38
|
+
* all. In a fresh checkout the user expects `flo init` to anchor at the
|
|
39
|
+
* project they're in, not at some ancestor `.git`/`package.json` directory.
|
|
31
40
|
*
|
|
32
41
|
* Story #229 history: this function was first extracted from workflow-tools.ts;
|
|
33
|
-
* #1057 brought it into alignment with bridge-core.getProjectRoot()
|
|
42
|
+
* #1057 brought it into alignment with bridge-core.getProjectRoot(); #1174
|
|
43
|
+
* changed Pass A from nearest-wins to topmost-wins to fix monorepo daemon
|
|
44
|
+
* fragmentation.
|
|
34
45
|
*/
|
|
35
46
|
import { existsSync } from 'node:fs';
|
|
36
47
|
import { resolve, dirname, parse, join, basename } from 'node:path';
|
|
48
|
+
/**
|
|
49
|
+
* Walk strictly upward from `dir` (exclusive) and return the nearest ancestor
|
|
50
|
+
* that has `.moflo/moflo.db`, or `null` if none exists below the filesystem
|
|
51
|
+
* root.
|
|
52
|
+
*
|
|
53
|
+
* Used by `flo init` and the session-start launcher to detect nested-.moflo
|
|
54
|
+
* situations (#1174). Post-resolver-fix `findProjectRoot` returns the topmost
|
|
55
|
+
* memory marker, so encountering an ancestor here means either:
|
|
56
|
+
* 1. `CLAUDE_PROJECT_DIR` explicitly overrode to a sub-directory
|
|
57
|
+
* (legitimate user action — log a warning but don't refuse), or
|
|
58
|
+
* 2. The caller is operating on a directory that's about to become a new
|
|
59
|
+
* nested .moflo/ island (e.g. `flo init` in a sub-workspace).
|
|
60
|
+
*
|
|
61
|
+
* Algorithmic twin of `bin/lib/moflo-paths.mjs:findAncestorMofloRoot()`.
|
|
62
|
+
*/
|
|
63
|
+
export function findAncestorMofloRoot(dir) {
|
|
64
|
+
const start = resolve(dir);
|
|
65
|
+
const fsRoot = parse(start).root;
|
|
66
|
+
let cursor = dirname(start);
|
|
67
|
+
while (cursor !== fsRoot) {
|
|
68
|
+
if (existsSync(join(cursor, '.moflo', 'moflo.db'))) {
|
|
69
|
+
return cursor;
|
|
70
|
+
}
|
|
71
|
+
const parent = dirname(cursor);
|
|
72
|
+
if (parent === cursor)
|
|
73
|
+
break;
|
|
74
|
+
cursor = parent;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
37
78
|
export function findProjectRoot(opts) {
|
|
38
79
|
const honorEnv = opts?.honorEnv !== false;
|
|
39
80
|
if (honorEnv && process.env.CLAUDE_PROJECT_DIR) {
|
|
@@ -42,17 +83,35 @@ export function findProjectRoot(opts) {
|
|
|
42
83
|
const startDir = opts?.cwd ?? process.cwd();
|
|
43
84
|
const start = resolve(startDir);
|
|
44
85
|
const fsRoot = parse(start).root;
|
|
45
|
-
//
|
|
86
|
+
// Pass A — memory markers, topmost wins (#1174).
|
|
87
|
+
// Collect every ancestor with `.moflo/moflo.db` or `.swarm/memory.db`, then
|
|
88
|
+
// return the highest one. Guarantees the root daemon is canonical in a
|
|
89
|
+
// monorepo with nested .moflo/ residue.
|
|
90
|
+
let topmostMemoryMarker = null;
|
|
46
91
|
let dir = start;
|
|
47
92
|
while (dir !== fsRoot) {
|
|
48
93
|
if (basename(dir) === 'node_modules') {
|
|
49
94
|
dir = dirname(dir);
|
|
50
95
|
continue;
|
|
51
96
|
}
|
|
52
|
-
if (existsSync(join(dir, '.moflo', 'moflo.db')))
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
97
|
+
if (existsSync(join(dir, '.moflo', 'moflo.db')) || existsSync(join(dir, '.swarm', 'memory.db'))) {
|
|
98
|
+
topmostMemoryMarker = dir;
|
|
99
|
+
}
|
|
100
|
+
const parent = dirname(dir);
|
|
101
|
+
if (parent === dir)
|
|
102
|
+
break;
|
|
103
|
+
dir = parent;
|
|
104
|
+
}
|
|
105
|
+
if (topmostMemoryMarker)
|
|
106
|
+
return topmostMemoryMarker;
|
|
107
|
+
// Pass B — project marker pair, nearest wins. Only reached when no moflo
|
|
108
|
+
// state exists anywhere up the tree.
|
|
109
|
+
dir = start;
|
|
110
|
+
while (dir !== fsRoot) {
|
|
111
|
+
if (basename(dir) === 'node_modules') {
|
|
112
|
+
dir = dirname(dir);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
56
115
|
if (existsSync(join(dir, 'CLAUDE.md')) && existsSync(join(dir, 'package.json'))) {
|
|
57
116
|
return dir;
|
|
58
117
|
}
|
|
@@ -61,7 +120,7 @@ export function findProjectRoot(opts) {
|
|
|
61
120
|
break;
|
|
62
121
|
dir = parent;
|
|
63
122
|
}
|
|
64
|
-
//
|
|
123
|
+
// Pass C — bare package.json or .git, nearest wins.
|
|
65
124
|
dir = start;
|
|
66
125
|
while (dir !== fsRoot) {
|
|
67
126
|
if (basename(dir) === 'node_modules') {
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.10.
|
|
3
|
+
"version": "4.10.14",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
96
96
|
"@typescript-eslint/parser": "^7.18.0",
|
|
97
97
|
"eslint": "^8.0.0",
|
|
98
|
-
"moflo": "^4.10.
|
|
98
|
+
"moflo": "^4.10.13",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|