moflo 4.10.9 → 4.10.11
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/skills/luminarium/SKILL.md +66 -0
- package/README.md +30 -1
- package/dist/src/cli/commands/doctor-checks-memory-access.js +7 -0
- package/dist/src/cli/init/executor.js +1 -0
- package/dist/src/cli/memory/bridge-embedder.js +10 -7
- package/dist/src/cli/services/daemon-dashboard.js +22 -2
- package/dist/src/cli/services/ephemeral-namespace-purge.js +47 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: luminarium
|
|
3
|
+
description: |
|
|
4
|
+
Print the localhost URL for The Luminarium — moflo's daemon dashboard — for the current project.
|
|
5
|
+
Use when the user asks for "the luminarium link", "the moflo dashboard", "the daemon UI", or anything synonymous.
|
|
6
|
+
Each project gets a deterministic port in 33000–33999; the actual bound port is recorded in `.moflo/daemon.lock`.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# /luminarium — Project Dashboard Link
|
|
10
|
+
|
|
11
|
+
Surface the URL for The Luminarium (the moflo daemon's localhost UI) for the project that this session is running in. No prompts, no confirmations — print the link and stop.
|
|
12
|
+
|
|
13
|
+
## Procedure
|
|
14
|
+
|
|
15
|
+
1. **Find the project root.** Walk up from `process.cwd()` looking for a `.moflo/` directory. The session's cwd is almost always the project root, so check there first.
|
|
16
|
+
|
|
17
|
+
2. **Read `.moflo/daemon.lock`.** It's a JSON file written by the daemon at bind time. The dashboard port is the `port` field:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{ "pid": 12345, "port": 33421, "startedAt": "...", ... }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- If the file exists and `port` is a valid number → the daemon is running and bound. Use that port.
|
|
24
|
+
- If the file is missing or `port` is absent/invalid → the daemon is not running. See step 4.
|
|
25
|
+
|
|
26
|
+
3. **Print the link** in a single line, with the path verbatim — Claude Code renders it as clickable:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
The Luminarium: http://localhost:<port>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Nothing else. No banner, no follow-up question, no "what would you like to do?".
|
|
33
|
+
|
|
34
|
+
4. **If the daemon isn't running** (no lock file, or unparseable), say so in one line and offer the start command — don't run it:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
The moflo daemon isn't running for this project. Start it with: npx flo daemon start
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Why read the lock, not compute the port
|
|
41
|
+
|
|
42
|
+
The port is project-deterministic (sha256(projectRoot) mapped into 33000–33999), but if the deterministic port was already taken at bind time the daemon scans forward and binds an alternate. The lock file is the only source of truth for what's actually bound. Do not compute the hash yourself — read the file.
|
|
43
|
+
|
|
44
|
+
## Don't
|
|
45
|
+
|
|
46
|
+
- Don't fall back to any hardcoded port — there is no project-agnostic dashboard port; a literal would route to a foreign daemon on a multi-project machine. If the lock is missing, report "not running".
|
|
47
|
+
- Don't compute the deterministic port and report it as the link when the lock is missing — the daemon may be down, or bound to an alternate port. Report "not running" instead.
|
|
48
|
+
- Don't run `flo daemon start` automatically — the user asked for a link, not for daemon management. Leave starting to `/healer` or the user.
|
|
49
|
+
- Don't open a browser. Print the URL; let the user click.
|
|
50
|
+
|
|
51
|
+
## Output
|
|
52
|
+
|
|
53
|
+
A single line. Examples:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
The Luminarium: http://localhost:33421
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
The moflo daemon isn't running for this project. Start it with: npx flo daemon start
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## See Also
|
|
64
|
+
|
|
65
|
+
- `/healer` — diagnoses and (with `--fix`) starts the daemon if it's not running.
|
|
66
|
+
- `src/cli/services/daemon-port.ts` (and its JS twin `bin/lib/daemon-port.mjs`) — canonical port-resolution helpers; `resolveClientPort()` is what the rest of moflo uses.
|
package/README.md
CHANGED
|
@@ -419,7 +419,7 @@ flo daemon status # shows whether the service is registered AND running
|
|
|
419
419
|
|
|
420
420
|
`flo spell schedule create` warns when the daemon isn't installed so you don't quietly miss runs.
|
|
421
421
|
|
|
422
|
-
**Monitoring.** **The Luminarium
|
|
422
|
+
**Monitoring.** **[The Luminarium](#the-luminarium)** — moflo's localhost daemon dashboard — surfaces live schedules, recent executions, and per-schedule controls (disable / re-enable / run now), alongside worker health, memory stats, and Claude Code session stats. Each project gets its own deterministic port (33000–33999) recorded in `.moflo/daemon.lock`; ask `/luminarium` in your Claude session and it'll print the link.
|
|
423
423
|
|
|
424
424
|
For full configuration (`scheduler:` block in `moflo.yaml`), event types, and the catch-up window after restarts, see [docs/SPELLS.md#scheduling](docs/SPELLS.md#scheduling).
|
|
425
425
|
|
|
@@ -459,6 +459,35 @@ flo epic reset 42 # Reset state for re-run
|
|
|
459
459
|
|
|
460
460
|
See the [Epic handling](#epic-handling) section above for detection criteria and the comparison between `/flo <epic>` and `flo epic run`.
|
|
461
461
|
|
|
462
|
+
## The Luminarium
|
|
463
|
+
|
|
464
|
+
The Luminarium is moflo's localhost daemon dashboard. It boots automatically with the background daemon (no extra service to install) and stays running as long as the daemon is up.
|
|
465
|
+
|
|
466
|
+
### Finding the URL
|
|
467
|
+
|
|
468
|
+
Each project gets a deterministic port in the range 33000–33999, derived from a hash of the project root so two projects never collide on the same machine. The actual bound port is written to `.moflo/daemon.lock` when the daemon starts — if the deterministic port is already taken the daemon scans forward, so the lock file is the source of truth, not the hash.
|
|
469
|
+
|
|
470
|
+
Three ways to get the URL:
|
|
471
|
+
|
|
472
|
+
- **`/luminarium`** — inside a Claude Code session in a moflo project, this skill reads `.moflo/daemon.lock` and prints `http://localhost:<port>`. Fastest path.
|
|
473
|
+
- **`flo daemon status`** — prints the URL alongside the health summary.
|
|
474
|
+
- **`cat .moflo/daemon.lock`** — read the JSON directly: `{ "pid": ..., "port": 33421, ... }`.
|
|
475
|
+
|
|
476
|
+
### What it shows
|
|
477
|
+
|
|
478
|
+
| Tab | What you see |
|
|
479
|
+
|-----|--------------|
|
|
480
|
+
| **Workers** | Live agent processes the daemon is running (statusline updater, indexer, embedder, etc.) |
|
|
481
|
+
| **Schedules** | All registered spell schedules (cron / interval / one-time), with run-now and disable controls |
|
|
482
|
+
| **Executions** | Recent spell runs — duration, exit code, step-by-step output |
|
|
483
|
+
| **Memory** | Memory namespace breakdown, vector count, embedder backend, HNSW index health |
|
|
484
|
+
| **Claude Stats** | Per-session Claude Code transcript stats — tokens, tools called, files touched (local primary sessions only) |
|
|
485
|
+
|
|
486
|
+
### Flags
|
|
487
|
+
|
|
488
|
+
- `flo daemon start --no-dashboard` — disable the HTTP server entirely (the daemon itself still runs)
|
|
489
|
+
- `flo daemon start --dashboard-port <N>` — pin to a specific port, overriding the deterministic resolver. Also accepts the `MOFLO_DAEMON_PORT` env var, which the rest of moflo respects when talking to the daemon
|
|
490
|
+
|
|
462
491
|
## Commands
|
|
463
492
|
|
|
464
493
|
You don't need to run these for normal use — `flo init` sets everything up, and the hooks handle memory, routing, and learning automatically. These commands are here for manual setup, debugging, and tweaking.
|
|
@@ -26,6 +26,7 @@ import { existsSync } from 'fs';
|
|
|
26
26
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
27
27
|
import { memoryDbPath } from '../services/moflo-paths.js';
|
|
28
28
|
import { findProjectRoot } from '../services/project-root.js';
|
|
29
|
+
import { purgeMemoryProbeNamespaces } from '../services/ephemeral-namespace-purge.js';
|
|
29
30
|
import { loadToolArrays, getTool, pushDetail, summarizeFunctional, } from './doctor-checks-functional-shared.js';
|
|
30
31
|
const MEMORY_ACCESS_CHECK = 'Memory Access Functional';
|
|
31
32
|
const MEMORY_ACCESS_FAIL_FIX = 'Run `flo doctor --json` for per-subcheck details. Common fixes: ensure fastembed installed (memory_store.hasEmbedding=false), explicit threshold:0 honored (#837), or rebuild HNSW index (`flo memory rebuild-index`)';
|
|
@@ -563,6 +564,12 @@ export async function checkMemoryAccessFunctional() {
|
|
|
563
564
|
}
|
|
564
565
|
catch { /* ignore */ }
|
|
565
566
|
}
|
|
567
|
+
// #1166 — namespace-level sweep backstop for the per-key safeDelete
|
|
568
|
+
// loop above (see purgeMemoryProbeNamespaces docstring for why).
|
|
569
|
+
try {
|
|
570
|
+
await purgeMemoryProbeNamespaces({ dbPath: memoryDbPath(findProjectRoot()) });
|
|
571
|
+
}
|
|
572
|
+
catch { /* best-effort */ }
|
|
566
573
|
}
|
|
567
574
|
return summarizeFunctional(MEMORY_ACCESS_CHECK, details, {
|
|
568
575
|
passSuffix: '(memory_store + memory_search round-trip verified across subagent, swarm-agent, and hive-mind contexts)',
|
|
@@ -116,13 +116,16 @@ export const PURGE_ON_SESSION_START_NAMESPACES = new Set([
|
|
|
116
116
|
* spawns a NEW namespace, so namespace pollution grows linearly with
|
|
117
117
|
* healer-run count if cleanup races fail.
|
|
118
118
|
*
|
|
119
|
-
* Both probes register an explicit cleanup via `safeDelete
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
119
|
+
* Both probes register an explicit cleanup via `safeDelete`. Post-#1166
|
|
120
|
+
* the doctor also runs an in-process namespace sweep over these prefixes
|
|
121
|
+
* inside `checkMemoryAccessFunctional`'s finally block, so a healthy
|
|
122
|
+
* doctor run leaves zero rows behind. The session-start launcher's
|
|
123
|
+
* prefix-purge is now strictly a safety net for crashed-process residue
|
|
124
|
+
* (the doctor never reached its finally) or pre-#1166 consumer DBs that
|
|
125
|
+
* still carry accumulated probe rows. Auto-purging matches the pattern
|
|
126
|
+
* for `hive-mind`/`epic-state`/`test-bridge-fix`. These rows MUST still
|
|
127
|
+
* get embeddings (see {@link EPHEMERAL_NAMESPACE_PREFIXES} for why) —
|
|
128
|
+
* only their persistence across sessions is curtailed.
|
|
126
129
|
*/
|
|
127
130
|
export const PURGE_ON_SESSION_START_PREFIXES = new Set([
|
|
128
131
|
'doctor-memprobe-',
|
|
@@ -798,6 +798,14 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
798
798
|
.btn-primary { background: #238636; border-color: #2ea043; color: #fff; }
|
|
799
799
|
.btn-primary:hover { background: #2ea043; border-color: #3fb950; }
|
|
800
800
|
.dim { color: #484f58; font-size: 0.75rem; font-style: italic; }
|
|
801
|
+
/* Loading state for tabs whose data is slow on first paint (currently
|
|
802
|
+
Claude Stats, which walks the user's transcript dir — can take 10–15s
|
|
803
|
+
on a long history). Pure-CSS spinner; no image, no framework. */
|
|
804
|
+
.loading-block { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; padding: 48px 16px; color: #8b949e; }
|
|
805
|
+
.loading-block .spinner { width: 28px; height: 28px; border: 3px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: lum-spin 0.85s linear infinite; }
|
|
806
|
+
.loading-block .msg { font-size: 0.9rem; color: #c9d1d9; }
|
|
807
|
+
.loading-block .hint { font-size: 0.8rem; color: #8b949e; font-style: italic; max-width: 480px; text-align: center; line-height: 1.5; }
|
|
808
|
+
@keyframes lum-spin { to { transform: rotate(360deg); } }
|
|
801
809
|
</style>
|
|
802
810
|
</head>
|
|
803
811
|
<body>
|
|
@@ -811,7 +819,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
811
819
|
<div id="panel-schedules" class="panel" style="display:none"><div id="schedules-active"></div><div id="schedules-events"></div></div>
|
|
812
820
|
<div id="panel-executions" class="panel" style="display:none"></div>
|
|
813
821
|
<div id="panel-memory" class="panel" style="display:none"></div>
|
|
814
|
-
<div id="panel-claude-stats" class="panel" style="display:none"></div>
|
|
822
|
+
<div id="panel-claude-stats" class="panel" style="display:none"><div class="loading-block" role="status" aria-label="Loading Claude Code transcripts"><div class="spinner"></div><div class="msg">Reading Claude Code transcripts…</div><div class="hint">First load can take 10–15 seconds — moflo walks every session file in this project's transcript directory. Subsequent loads in this tab are much faster.</div></div></div>
|
|
815
823
|
<div id="poll-indicator" class="poll-indicator"></div>
|
|
816
824
|
<script>
|
|
817
825
|
// Tab navigation — plain DOM, no framework
|
|
@@ -1139,7 +1147,19 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
1139
1147
|
};
|
|
1140
1148
|
function renderClaudeStats(cs) {
|
|
1141
1149
|
const el = document.getElementById('panel-claude-stats');
|
|
1142
|
-
|
|
1150
|
+
// cs is null on first paint AND on fetch error (Promise chain uses
|
|
1151
|
+
// .catch(() => null)). Render the spinner block on both so the user
|
|
1152
|
+
// sees motion during the 10–15s transcript walk and during a transient
|
|
1153
|
+
// network blip — better than a static "Loading..." that looks frozen.
|
|
1154
|
+
if (!cs) {
|
|
1155
|
+
el.innerHTML =
|
|
1156
|
+
'<div class="loading-block" role="status" aria-label="Loading Claude Code transcripts">' +
|
|
1157
|
+
'<div class="spinner"></div>' +
|
|
1158
|
+
'<div class="msg">Reading Claude Code transcripts…</div>' +
|
|
1159
|
+
'<div class="hint">First load can take 10–15 seconds — moflo walks every session file in this project\\'s transcript directory. Subsequent loads in this tab are much faster.</div>' +
|
|
1160
|
+
'</div>';
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1143
1163
|
|
|
1144
1164
|
// Always-visible disclaimer banner — keeps the scope and limits in
|
|
1145
1165
|
// view so the numbers aren't read as account-wide truth.
|
|
@@ -105,4 +105,51 @@ export async function purgeEphemeralNamespaces(options = {}) {
|
|
|
105
105
|
db.close();
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Hard-delete rows whose namespace matches one of
|
|
110
|
+
* {@link PURGE_ON_SESSION_START_PREFIXES} — currently `doctor-memprobe-*`
|
|
111
|
+
* and `doctor-neighbors-*`. Scoped down from {@link purgeEphemeralNamespaces}:
|
|
112
|
+
* no exact-namespace pass, no tasklist trim, no VACUUM. Returns
|
|
113
|
+
* `{ purged: 0 }` on a missing DB / missing `memory_entries` / clean state.
|
|
114
|
+
*
|
|
115
|
+
* Intended for the doctor's Memory Access functional check finally block
|
|
116
|
+
* (#1166). Only the doctor writes to these namespaces in production, so
|
|
117
|
+
* sweeping by prefix at the end of every healer run kills the
|
|
118
|
+
* `populated:ephemeral-purged` flake class — a per-key `safeDelete` that
|
|
119
|
+
* silently no-ops (row not visible at delete time, MCP transport error,
|
|
120
|
+
* `memory_delete` returning `success: true, deleted: false`) no longer
|
|
121
|
+
* leaks a row into the next assertion. The launcher's session-start
|
|
122
|
+
* purge stays in place as a defence-in-depth safety net for residue from
|
|
123
|
+
* crashed-process scenarios where the doctor never reached its finally.
|
|
124
|
+
*
|
|
125
|
+
* Errors propagate to the caller (the doctor absorbs them so a failed
|
|
126
|
+
* sweep never poisons the check return value).
|
|
127
|
+
*/
|
|
128
|
+
export async function purgeMemoryProbeNamespaces(options = {}) {
|
|
129
|
+
const fs = await import('fs');
|
|
130
|
+
const path = await import('path');
|
|
131
|
+
const dbPath = path.resolve(options.dbPath ?? memoryDbPath(process.cwd()));
|
|
132
|
+
if (!fs.existsSync(dbPath))
|
|
133
|
+
return { purged: 0 };
|
|
134
|
+
const prefixes = Array.from(PURGE_ON_SESSION_START_PREFIXES);
|
|
135
|
+
if (prefixes.length === 0)
|
|
136
|
+
return { purged: 0 };
|
|
137
|
+
const db = openDaemonDatabase(dbPath);
|
|
138
|
+
try {
|
|
139
|
+
const probe = db.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries' LIMIT 1`);
|
|
140
|
+
if (!probe[0]?.values?.[0])
|
|
141
|
+
return { purged: 0 };
|
|
142
|
+
const whereClause = prefixes.map(() => 'namespace LIKE ?').join(' OR ');
|
|
143
|
+
const bindings = prefixes.map((p) => `${p}%`);
|
|
144
|
+
const countRows = db.exec(`SELECT COUNT(*) FROM memory_entries WHERE ${whereClause}`, bindings);
|
|
145
|
+
const purgeable = Number(countRows[0]?.values?.[0]?.[0] ?? 0);
|
|
146
|
+
if (purgeable === 0)
|
|
147
|
+
return { purged: 0 };
|
|
148
|
+
db.run(`DELETE FROM memory_entries WHERE ${whereClause}`, bindings);
|
|
149
|
+
return { purged: db.getRowsModified?.() ?? 0 };
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
db.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
108
155
|
//# sourceMappingURL=ephemeral-namespace-purge.js.map
|
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.11",
|
|
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.10",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|