@monoes/monomindcli 2.7.6 → 2.7.8
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 +9 -0
- package/.claude/agents/core/coordinator.md +62 -0
- package/.claude/agents/core/planner.md +9 -0
- package/.claude/agents/core/reviewer.md +9 -0
- package/.claude/agents/core/tester.md +8 -0
- package/.claude/helpers/handlers/gates-handler.cjs +180 -47
- package/.claude/helpers/handlers/route-handler.cjs +32 -3
- package/.claude/helpers/hook-handler.cjs +55 -3
- package/.claude/helpers/intelligence.cjs +3 -1
- package/.claude/helpers/statusline.cjs +27 -3
- package/.claude/helpers/utils/monograph.cjs +104 -18
- package/.claude/settings.json +1 -1
- package/.claude/skills/monodesign/scripts/live-server.mjs +38 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +6 -1
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/capabilities/index.d.ts +0 -1
- package/dist/src/capabilities/index.d.ts.map +1 -1
- package/dist/src/capabilities/index.js +8 -1
- package/dist/src/capabilities/index.js.map +1 -1
- package/dist/src/commands/agent-lifecycle.d.ts.map +1 -1
- package/dist/src/commands/agent-lifecycle.js +5 -1
- package/dist/src/commands/agent-lifecycle.js.map +1 -1
- package/dist/src/commands/agent-ops.d.ts.map +1 -1
- package/dist/src/commands/agent-ops.js +32 -2
- package/dist/src/commands/agent-ops.js.map +1 -1
- package/dist/src/commands/browse-workflow.d.ts.map +1 -1
- package/dist/src/commands/browse-workflow.js +30 -0
- package/dist/src/commands/browse-workflow.js.map +1 -1
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +8 -1
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/guidance.d.ts.map +1 -1
- package/dist/src/commands/guidance.js +8 -2
- package/dist/src/commands/guidance.js.map +1 -1
- package/dist/src/commands/hooks-extended-commands.d.ts.map +1 -1
- package/dist/src/commands/hooks-extended-commands.js +10 -2
- package/dist/src/commands/hooks-extended-commands.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +18 -4
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/memory-crud.d.ts.map +1 -1
- package/dist/src/commands/memory-crud.js +40 -3
- package/dist/src/commands/memory-crud.js.map +1 -1
- package/dist/src/commands/org-observe.d.ts.map +1 -1
- package/dist/src/commands/org-observe.js +67 -13
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/commands/org.d.ts +11 -0
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +162 -15
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/commands/performance.d.ts.map +1 -1
- package/dist/src/commands/performance.js +31 -5
- package/dist/src/commands/performance.js.map +1 -1
- package/dist/src/commands/security-misc.d.ts.map +1 -1
- package/dist/src/commands/security-misc.js +18 -3
- package/dist/src/commands/security-misc.js.map +1 -1
- package/dist/src/commands/security-scan.d.ts +30 -1
- package/dist/src/commands/security-scan.d.ts.map +1 -1
- package/dist/src/commands/security-scan.js +182 -69
- package/dist/src/commands/security-scan.js.map +1 -1
- package/dist/src/commands/swarm.d.ts.map +1 -1
- package/dist/src/commands/swarm.js +41 -14
- package/dist/src/commands/swarm.js.map +1 -1
- package/dist/src/consensus/audit-writer.d.ts.map +1 -1
- package/dist/src/consensus/audit-writer.js +11 -10
- package/dist/src/consensus/audit-writer.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +138 -22
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/settings-generator.d.ts.map +1 -1
- package/dist/src/init/settings-generator.js +4 -1
- package/dist/src/init/settings-generator.js.map +1 -1
- package/dist/src/knowledge/document-pipeline.d.ts +17 -0
- package/dist/src/knowledge/document-pipeline.d.ts.map +1 -1
- package/dist/src/knowledge/document-pipeline.js +62 -3
- package/dist/src/knowledge/document-pipeline.js.map +1 -1
- package/dist/src/mcp-client.d.ts.map +1 -1
- package/dist/src/mcp-client.js +78 -0
- package/dist/src/mcp-client.js.map +1 -1
- package/dist/src/mcp-tools/embeddings-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/embeddings-tools.js +16 -8
- package/dist/src/mcp-tools/embeddings-tools.js.map +1 -1
- package/dist/src/mcp-tools/knowledge-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/knowledge-tools.js +27 -13
- package/dist/src/mcp-tools/knowledge-tools.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts +5 -1
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +26 -3
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-read.d.ts +9 -0
- package/dist/src/memory/memory-read.d.ts.map +1 -1
- package/dist/src/memory/memory-read.js +13 -2
- package/dist/src/memory/memory-read.js.map +1 -1
- package/dist/src/monovector/diff-classifier.d.ts.map +1 -1
- package/dist/src/monovector/diff-classifier.js +25 -6
- package/dist/src/monovector/diff-classifier.js.map +1 -1
- package/dist/src/orgrt/daemon.d.ts +21 -1
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +82 -11
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/inbox.d.ts.map +1 -1
- package/dist/src/orgrt/inbox.js +53 -20
- package/dist/src/orgrt/inbox.js.map +1 -1
- package/dist/src/parser.d.ts +4 -2
- package/dist/src/parser.d.ts.map +1 -1
- package/dist/src/parser.js +61 -25
- package/dist/src/parser.js.map +1 -1
- package/dist/src/routing/route-layer-factory.d.ts +15 -0
- package/dist/src/routing/route-layer-factory.d.ts.map +1 -1
- package/dist/src/routing/route-layer-factory.js +44 -2
- package/dist/src/routing/route-layer-factory.js.map +1 -1
- package/dist/src/services/config-file-manager.d.ts +10 -2
- package/dist/src/services/config-file-manager.d.ts.map +1 -1
- package/dist/src/services/config-file-manager.js +10 -2
- package/dist/src/services/config-file-manager.js.map +1 -1
- package/dist/src/ui/collector.mjs +43 -3
- package/dist/src/ui/dashboard.html +27 -8
- package/dist/src/ui/server.mjs +144 -14
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/dist/src/capabilities/watcher.d.ts +0 -18
- package/dist/src/capabilities/watcher.d.ts.map +0 -1
- package/dist/src/capabilities/watcher.js +0 -107
- package/dist/src/capabilities/watcher.js.map +0 -1
- package/dist/src/config-adapter.d.ts +0 -16
- package/dist/src/config-adapter.d.ts.map +0 -1
- package/dist/src/config-adapter.js +0 -220
- package/dist/src/config-adapter.js.map +0 -1
|
@@ -119,11 +119,48 @@ function collectSessions(projectDir) {
|
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Canonical monomind data root for a project, mirroring getMonomindDataRoot()
|
|
124
|
+
* in src/mcp-tools/types.ts: inside a git repo the stores live under
|
|
125
|
+
* `<repo>/.git/monomind`, NOT `<repo>/.monomind`.
|
|
126
|
+
*
|
|
127
|
+
* The dashboard read only the `.monomind` path, so in any git repo — i.e. every
|
|
128
|
+
* real project — it rendered swarm data from a stale or absent file while the
|
|
129
|
+
* MCP tools wrote the live state somewhere else.
|
|
130
|
+
*/
|
|
131
|
+
function monomindDataRoot(projectDir) {
|
|
132
|
+
if (process.env.MONOMIND_DATA_DIR) return process.env.MONOMIND_DATA_DIR;
|
|
133
|
+
try {
|
|
134
|
+
const gitEntry = path.join(projectDir, '.git');
|
|
135
|
+
const st = fs.statSync(gitEntry);
|
|
136
|
+
if (st.isDirectory()) return path.join(gitEntry, 'monomind');
|
|
137
|
+
const m = fs.readFileSync(gitEntry, 'utf8').match(/^gitdir:\s*(.+)/m);
|
|
138
|
+
if (m) {
|
|
139
|
+
const worktreeDir = path.resolve(projectDir, m[1].trim());
|
|
140
|
+
return path.join(path.dirname(path.dirname(worktreeDir)), 'monomind');
|
|
141
|
+
}
|
|
142
|
+
} catch { /* not a git repo — fall through */ }
|
|
143
|
+
return path.join(projectDir, '.monomind');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Read the first of several candidate paths that yields data. */
|
|
147
|
+
function readFirstJSON(...candidates) {
|
|
148
|
+
for (const c of candidates) {
|
|
149
|
+
const v = readJSON(c);
|
|
150
|
+
if (v) return v;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
122
155
|
const _appendedSwarmIds = new Set();
|
|
123
156
|
|
|
124
157
|
function collectSwarm(projectDir) {
|
|
125
|
-
|
|
126
|
-
|
|
158
|
+
// Canonical location first, legacy `<cwd>/.monomind` second so a project
|
|
159
|
+
// written by an older CLI still renders.
|
|
160
|
+
const state = readFirstJSON(
|
|
161
|
+
path.join(monomindDataRoot(projectDir), 'swarm', 'swarm-state.json'),
|
|
162
|
+
path.join(projectDir, '.monomind', 'swarm', 'swarm-state.json'),
|
|
163
|
+
) || {};
|
|
127
164
|
const dotSwarmState = readJSON(path.join(projectDir, '.swarm', 'state.json')) || {};
|
|
128
165
|
const merged = { ...dotSwarmState, ...state };
|
|
129
166
|
|
|
@@ -187,7 +224,10 @@ function collectSwarmHistory(projectDir) {
|
|
|
187
224
|
// this format never has. So without this fallback the tab is empty even
|
|
188
225
|
// though swarms exist. Derive entries directly from the map here.
|
|
189
226
|
try {
|
|
190
|
-
const state =
|
|
227
|
+
const state = readFirstJSON(
|
|
228
|
+
path.join(monomindDataRoot(projectDir), 'swarm', 'swarm-state.json'),
|
|
229
|
+
path.join(projectDir, '.monomind', 'swarm', 'swarm-state.json'),
|
|
230
|
+
) || {};
|
|
191
231
|
const swarmsMap = state.swarms || (state.swarmId ? { [state.swarmId]: state } : {});
|
|
192
232
|
for (const [key, s] of Object.entries(swarmsMap)) {
|
|
193
233
|
if (!s || typeof s !== 'object') continue;
|
|
@@ -5207,6 +5207,9 @@ function renderModelMix() {
|
|
|
5207
5207
|
if (!breakdown[model]) breakdown[model] = { calls: 0, cost: 0 };
|
|
5208
5208
|
breakdown[model].calls += d.calls || 0;
|
|
5209
5209
|
breakdown[model].cost += d.cost || 0;
|
|
5210
|
+
// The server flags models missing from its pricing table. Their cost
|
|
5211
|
+
// contribution is 0 because it is UNKNOWN, not because it was free.
|
|
5212
|
+
if (d.unknownPricing) breakdown[model].unknownPricing = true;
|
|
5210
5213
|
}
|
|
5211
5214
|
}
|
|
5212
5215
|
const entries = Object.entries(breakdown).sort((a,b) => b[1].cost - a[1].cost);
|
|
@@ -5220,11 +5223,14 @@ function renderModelMix() {
|
|
|
5220
5223
|
</tr></thead><tbody>
|
|
5221
5224
|
${entries.map(([model, d]) => {
|
|
5222
5225
|
const short = model.replace(/^claude-/,'').replace(/-\d{8}$/,'');
|
|
5223
|
-
const pct = totalCost > 0 ? Math.round(d.cost/totalCost*100) : 0;
|
|
5226
|
+
const pct = (totalCost > 0 && !d.unknownPricing) ? Math.round(d.cost/totalCost*100) : 0;
|
|
5227
|
+
const costCell = d.unknownPricing
|
|
5228
|
+
? `<span title="No pricing data for this model — spend is unknown, not zero" style="color:var(--text-lo)">unknown</span>`
|
|
5229
|
+
: `$${(d.cost || 0).toFixed(2)}`;
|
|
5224
5230
|
return `<tr>
|
|
5225
5231
|
<td style="font-size:11px">${esc(short)}</td>
|
|
5226
|
-
<td class="lb-cost"
|
|
5227
|
-
<td class="lb-dur">${pct}
|
|
5232
|
+
<td class="lb-cost">${costCell}</td>
|
|
5233
|
+
<td class="lb-dur">${d.unknownPricing ? '—' : pct + '%'}</td>
|
|
5228
5234
|
<td class="lb-dur">${d.calls}</td>
|
|
5229
5235
|
</tr>`;
|
|
5230
5236
|
}).join('')}
|
|
@@ -10240,10 +10246,14 @@ function buildModelDonut() {
|
|
|
10240
10246
|
if (!breakdown[model]) breakdown[model] = { calls: 0, cost: 0 };
|
|
10241
10247
|
breakdown[model].calls += d.calls || 0;
|
|
10242
10248
|
breakdown[model].cost += d.cost || 0;
|
|
10249
|
+
if (d.unknownPricing) breakdown[model].unknownPricing = true;
|
|
10243
10250
|
}
|
|
10244
10251
|
}
|
|
10245
10252
|
const entries = Object.entries(breakdown).sort((a, b) => b[1].cost - a[1].cost);
|
|
10246
10253
|
if (!entries.length) { el.innerHTML = '<div class="loading-txt" style="padding:8px">No model breakdown data</div>'; return; }
|
|
10254
|
+
// Unpriced models contribute 0 to every slice; say so rather than implying the
|
|
10255
|
+
// donut covers all spend.
|
|
10256
|
+
const _unpricedModels = entries.filter(([, d]) => d.unknownPricing).map(([m]) => m);
|
|
10247
10257
|
const totalCost = entries.reduce((a, [, d]) => a + d.cost, 0);
|
|
10248
10258
|
|
|
10249
10259
|
// Build SVG conic-gradient-style donut using stroke-dasharray
|
|
@@ -10259,19 +10269,25 @@ function buildModelDonut() {
|
|
|
10259
10269
|
const svg = `<svg class="donut-svg" width="88" height="88" viewBox="0 0 88 88">
|
|
10260
10270
|
<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="var(--surface-hi)" stroke-width="14"/>
|
|
10261
10271
|
${segments}
|
|
10262
|
-
<text x="${CX}" y="${CY}" text-anchor="middle" dy="0.3em" font-size="9" fill="var(--text-xs)" font-family="var(--mono)"
|
|
10272
|
+
<text x="${CX}" y="${CY}" text-anchor="middle" dy="0.3em" font-size="9" fill="var(--text-xs)" font-family="var(--mono)">${_unpricedModels.length ? '≥' : ''}$${totalCost.toFixed(2)}</text>
|
|
10263
10273
|
</svg>`;
|
|
10264
10274
|
|
|
10265
10275
|
const legend = entries.slice(0, 6).map(([model, d], i) => {
|
|
10266
10276
|
const short = model.replace(/^claude-/, '').replace(/-\d{8}$/, '');
|
|
10267
10277
|
const pct = totalCost > 0 ? Math.round(d.cost / totalCost * 100) : 0;
|
|
10278
|
+
const costTxt = d.unknownPricing
|
|
10279
|
+
? `<span title="No pricing data for this model">unknown</span>`
|
|
10280
|
+
: `$${(d.cost || 0).toFixed(2)} <span style="color:var(--text-xs)">${pct}%</span>`;
|
|
10268
10281
|
return `<div class="donut-item"><div class="donut-swatch" style="background:${DONUT_PALETTE[i % DONUT_PALETTE.length]}"></div>
|
|
10269
10282
|
<span class="donut-name" title="${esc(model)}">${esc(short)}</span>
|
|
10270
|
-
<span class="donut-cost"
|
|
10283
|
+
<span class="donut-cost">${costTxt}</span>
|
|
10271
10284
|
</div>`;
|
|
10272
10285
|
}).join('');
|
|
10273
10286
|
|
|
10274
|
-
|
|
10287
|
+
const unpricedNote = _unpricedModels.length
|
|
10288
|
+
? `<div class="loading-txt" style="padding:4px 8px;font-size:10px">Total excludes ${_unpricedModels.length} model(s) with no pricing data: ${esc(_unpricedModels.join(', '))}</div>`
|
|
10289
|
+
: '';
|
|
10290
|
+
el.innerHTML = `<div class="donut-wrap">${svg}<div class="donut-legend">${legend}</div></div>${unpricedNote}`;
|
|
10275
10291
|
}
|
|
10276
10292
|
|
|
10277
10293
|
// ── feature 60: cost anomaly explainer ────────────────────
|
|
@@ -10289,9 +10305,12 @@ function showCostExplainer(sessId, event) {
|
|
|
10289
10305
|
const pct = costsArr.length ? Math.round(costsArr.filter(c=>c<=(sess.totalCost||0)).length/costsArr.length*100) : 0;
|
|
10290
10306
|
const ratio = median > 0 ? ((sess.totalCost||0)/median).toFixed(1) : '—';
|
|
10291
10307
|
const modelRows = Object.entries(sess.modelBreakdown||{}).sort((a,b)=>b[1].cost-a[1].cost).slice(0,4)
|
|
10292
|
-
.map(([m,d])=>`<div class="err-item">${esc(m.replace(/^claude-/,'').replace(/-\d{8}$/,''))}:
|
|
10308
|
+
.map(([m,d])=>`<div class="err-item">${esc(m.replace(/^claude-/,'').replace(/-\d{8}$/,''))}: ${d.unknownPricing ? 'unknown (no pricing data)' : '$'+(d.cost||0).toFixed(4)} · ${d.calls||0} calls</div>`).join('');
|
|
10309
|
+
// costIncomplete => the server could not price at least one model, so the
|
|
10310
|
+
// session total is a floor, not the actual spend.
|
|
10311
|
+
const costPrefix = sess.costIncomplete ? '≥' : '';
|
|
10293
10312
|
drawer.innerHTML = `<div class="err-drawer-head" style="color:oklch(70% 0.18 80)">
|
|
10294
|
-
<span>Cost anomaly — $${(sess.totalCost||0).toFixed(3)} (${ratio}× median, top ${100-pct}%)</span>
|
|
10313
|
+
<span>Cost anomaly — ${costPrefix}$${(sess.totalCost||0).toFixed(3)} (${ratio}× median, top ${100-pct}%)</span>
|
|
10295
10314
|
<button class="err-close" onclick="this.closest('.err-drawer').classList.remove('open');this.closest('.err-drawer').innerHTML=''">✕</button>
|
|
10296
10315
|
</div>
|
|
10297
10316
|
<div class="err-drawer-body">
|
package/dist/src/ui/server.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
|
+
import zlib from 'zlib';
|
|
6
7
|
import { fileURLToPath } from 'url';
|
|
7
8
|
import { createRequire } from 'module';
|
|
8
9
|
import { spawn } from 'child_process';
|
|
@@ -41,6 +42,16 @@ function _sjGetPricing(model) {
|
|
|
41
42
|
for (const k of Object.keys(_SJ_PRICING)) { if (canonical.startsWith(k) || canonical.includes(k)) return _SJ_PRICING[k]; }
|
|
42
43
|
return null;
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* True when this model has a pricing row (directly, by alias, or by prefix match).
|
|
47
|
+
* A model with no row cannot be costed — every caller that sums _sjCalcCost() must
|
|
48
|
+
* use this to mark the total as INCOMPLETE rather than presenting the missing
|
|
49
|
+
* contribution as a genuine $0.00. The table above is a hand-maintained snapshot,
|
|
50
|
+
* so newly released models land here routinely.
|
|
51
|
+
*/
|
|
52
|
+
export function _sjHasPricing(model) {
|
|
53
|
+
return _sjGetPricing(model) !== null;
|
|
54
|
+
}
|
|
44
55
|
function _sjCalcCost(model, usage) {
|
|
45
56
|
const p = _sjGetPricing(model);
|
|
46
57
|
if (!p || !usage) return 0;
|
|
@@ -586,6 +597,70 @@ function _updateRunState(event, rootDir) {
|
|
|
586
597
|
}
|
|
587
598
|
// ── End runstate helpers ─────────────────────────────────────────────────
|
|
588
599
|
|
|
600
|
+
// ── Security: DNS-rebinding defence ───────────────────────────────────────────
|
|
601
|
+
// The dashboard binds 127.0.0.1 and embeds a live auth token in the HTML served
|
|
602
|
+
// by GET / (an open route — the page needs the token before it can attach it to
|
|
603
|
+
// any fetch). Loopback binding is NOT a boundary against a browser: a page on
|
|
604
|
+
// attacker.example can point its own DNS at 127.0.0.1, and the browser will
|
|
605
|
+
// happily connect and treat the response as same-origin with attacker.example,
|
|
606
|
+
// letting the attacker's script read the token straight out of the <meta> tag
|
|
607
|
+
// and then drive every authenticated /api/* route.
|
|
608
|
+
//
|
|
609
|
+
// The distinguishing signal is the Host header: the browser sends the name from
|
|
610
|
+
// the URL bar, so a rebound request carries `Host: attacker.example` while the
|
|
611
|
+
// real dashboard carries a loopback name. Rejecting non-loopback Host values
|
|
612
|
+
// closes the hole without affecting any legitimate client. (CORS does not help
|
|
613
|
+
// here — the rebound page IS same-origin as far as the browser is concerned.)
|
|
614
|
+
const _LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Extract the hostname from a Host header, dropping the port.
|
|
618
|
+
* Returns '' for a missing/blank header. IPv6 literals keep their brackets
|
|
619
|
+
* (`[::1]:4242` → `[::1]`), which is how they appear in a Host header.
|
|
620
|
+
*/
|
|
621
|
+
export function parseHostHeader(hostHeader) {
|
|
622
|
+
const h = String(hostHeader ?? '').trim().toLowerCase();
|
|
623
|
+
if (!h) return '';
|
|
624
|
+
if (h.startsWith('[')) {
|
|
625
|
+
const end = h.indexOf(']');
|
|
626
|
+
return end === -1 ? h : h.slice(0, end + 1);
|
|
627
|
+
}
|
|
628
|
+
const colon = h.lastIndexOf(':');
|
|
629
|
+
return colon === -1 ? h : h.slice(0, colon);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* True when a request's Host header names this machine's loopback interface or
|
|
634
|
+
* an explicitly configured extra host.
|
|
635
|
+
*
|
|
636
|
+
* A request with NO Host header is allowed: HTTP/1.0 and raw local scripts omit
|
|
637
|
+
* it, and it is not an attack vector — browsers ALWAYS send Host, so a rebinding
|
|
638
|
+
* page cannot suppress it.
|
|
639
|
+
*
|
|
640
|
+
* `extraHosts` comes from startServer({ allowedHosts }) or the
|
|
641
|
+
* MONOMIND_DASHBOARD_ALLOWED_HOSTS env var (comma-separated), for the case where
|
|
642
|
+
* someone deliberately fronts the dashboard with another name.
|
|
643
|
+
*/
|
|
644
|
+
export function isAllowedHost(hostHeader, extraHosts = []) {
|
|
645
|
+
const name = parseHostHeader(hostHeader);
|
|
646
|
+
if (!name) return true; // absent Host — non-browser client, see above
|
|
647
|
+
if (_LOOPBACK_HOSTNAMES.has(name)) return true;
|
|
648
|
+
// 127.0.0.0/8 is entirely loopback, not just 127.0.0.1.
|
|
649
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name)) return true;
|
|
650
|
+
for (const extra of extraHosts) {
|
|
651
|
+
if (parseHostHeader(extra) === name) return true;
|
|
652
|
+
}
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Parse the configured extra-host allow-list from an option + env var. */
|
|
657
|
+
export function resolveAllowedHosts(optionValue) {
|
|
658
|
+
const raw = []
|
|
659
|
+
.concat(Array.isArray(optionValue) ? optionValue : (optionValue ? [optionValue] : []))
|
|
660
|
+
.concat(String(process.env.MONOMIND_DASHBOARD_ALLOWED_HOSTS || '').split(','));
|
|
661
|
+
return raw.map((s) => String(s).trim().toLowerCase()).filter(Boolean);
|
|
662
|
+
}
|
|
663
|
+
|
|
589
664
|
// Server state
|
|
590
665
|
let running = false;
|
|
591
666
|
let currentPort = null;
|
|
@@ -707,7 +782,9 @@ function _resolveSlugToPathUncached(slug, projDir) {
|
|
|
707
782
|
return slug.replace(/-/g, '/');
|
|
708
783
|
}
|
|
709
784
|
|
|
710
|
-
export async function startServer({ port = 4242, projectDir, openBrowser = true } = {}) {
|
|
785
|
+
export async function startServer({ port = 4242, projectDir, openBrowser = true, allowedHosts } = {}) {
|
|
786
|
+
// Extra Host names accepted beyond loopback (see isAllowedHost above).
|
|
787
|
+
const _allowedHosts = resolveAllowedHosts(allowedHosts);
|
|
711
788
|
// ── Security: per-process auth credential for mutating (non-GET) requests ─
|
|
712
789
|
// Generated once per server start and written to a well-known location so
|
|
713
790
|
// trusted local callers (CLI, control-start.cjs) can read it and pass it
|
|
@@ -976,7 +1053,10 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
976
1053
|
const _coldPath = _wp.replace('.warm.jsonl', '.cold.jsonl.gz');
|
|
977
1054
|
if (fs.existsSync(_coldPath)) return; // already compacted
|
|
978
1055
|
const _warmData = fs.readFileSync(_wp);
|
|
979
|
-
|
|
1056
|
+
// `zlib` is a static ESM import at the top of this file. It used to be
|
|
1057
|
+
// `require('zlib')`, which is undefined in an ESM module — the
|
|
1058
|
+
// ReferenceError was swallowed by the surrounding catch, so cold-tier
|
|
1059
|
+
// compaction silently never ran. See tests/repo/no-cjs-require-in-esm.
|
|
980
1060
|
zlib.gzip(_warmData, (_err, _gz) => {
|
|
981
1061
|
if (_err) return;
|
|
982
1062
|
try {
|
|
@@ -1246,6 +1326,16 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
1246
1326
|
const server = http.createServer(async (req, res) => {
|
|
1247
1327
|
const url = req.url.split('?')[0];
|
|
1248
1328
|
|
|
1329
|
+
// ── Security: DNS-rebinding defence (see isAllowedHost) ─────────────────
|
|
1330
|
+
// Must run before ANY route — including the open ones, since GET / is what
|
|
1331
|
+
// hands out the auth token. Fails closed with a bare 403 and no CORS header.
|
|
1332
|
+
if (!isAllowedHost(req.headers.host, _allowedHosts)) {
|
|
1333
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1334
|
+
res.end('Forbidden: Host header is not a loopback address. '
|
|
1335
|
+
+ 'Set MONOMIND_DASHBOARD_ALLOWED_HOSTS to permit another host name.\n');
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1249
1339
|
// ── Security: strict CORS allow-list ────────────────────────────────────
|
|
1250
1340
|
// Only reflect Origin when it is this dashboard's own loopback origin.
|
|
1251
1341
|
// Otherwise the header is omitted entirely, so cross-origin reads fail
|
|
@@ -1436,6 +1526,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
1436
1526
|
const id = f.replace('.jsonl', '');
|
|
1437
1527
|
let lastPrompt = '', summaries = [], totalDurationMs = 0, totalMessages = 0, firstTs = null, lastTs = null, totalCost = 0, toolCalls = 0, userMessages = 0, cacheReadTokens = 0, totalInputTokens = 0, errorCount = 0;
|
|
1438
1528
|
const modelBreakdown = {};
|
|
1529
|
+
const unknownPricingModels = new Set();
|
|
1439
1530
|
const filesTouchedSet = new Set();
|
|
1440
1531
|
try {
|
|
1441
1532
|
const lines = _readTailLines(fp);
|
|
@@ -1478,6 +1569,12 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
1478
1569
|
if (!modelBreakdown[mk]) modelBreakdown[mk] = { calls: 0, cost: 0 };
|
|
1479
1570
|
modelBreakdown[mk].calls++;
|
|
1480
1571
|
modelBreakdown[mk].cost += c;
|
|
1572
|
+
// A model absent from the pricing table contributes 0 to the sum. Flag it
|
|
1573
|
+
// instead of letting the UI render an authoritative-looking $0.00.
|
|
1574
|
+
if (!_sjHasPricing(msg.model)) {
|
|
1575
|
+
modelBreakdown[mk].unknownPricing = true;
|
|
1576
|
+
unknownPricingModels.add(mk);
|
|
1577
|
+
}
|
|
1481
1578
|
cacheReadTokens += (msg.usage.cache_read_input_tokens || 0);
|
|
1482
1579
|
totalInputTokens += (msg.usage.input_tokens || 0)
|
|
1483
1580
|
+ (msg.usage.cache_creation_input_tokens || 0)
|
|
@@ -1493,7 +1590,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
1493
1590
|
const filesTouched = [...filesTouchedSet].slice(0, 20);
|
|
1494
1591
|
const compactCount = summaries.length;
|
|
1495
1592
|
const summary = summaries.length ? summaries[summaries.length - 1].text : null;
|
|
1496
|
-
|
|
1593
|
+
// costIncomplete => totalCost is a LOWER BOUND: at least one model had no
|
|
1594
|
+
// pricing row, so its spend is missing from the sum entirely.
|
|
1595
|
+
sessions.push({ id, mtime, firstTs, lastTs, lastPrompt, summaries, summary, compactCount, errorCount, totalDurationMs, totalMessages, totalCost, costIncomplete: unknownPricingModels.size > 0, unknownPricingModels: [...unknownPricingModels], toolCalls, userMessages, cacheReadTokens, totalInputTokens, modelBreakdown, filesTouched, file: fp });
|
|
1497
1596
|
}
|
|
1498
1597
|
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
|
|
1499
1598
|
res.end(JSON.stringify({ sessions }));
|
|
@@ -1740,16 +1839,29 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
1740
1839
|
try { sessionFiles = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl') && !f.startsWith('._')).map(f => path.join(projDir, f)); } catch {}
|
|
1741
1840
|
if (!sessionFiles.length) continue;
|
|
1742
1841
|
let totalCost = 0;
|
|
1842
|
+
const unknownPricingModels = new Set();
|
|
1743
1843
|
for (const fp of sessionFiles) {
|
|
1744
1844
|
try {
|
|
1745
1845
|
const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean);
|
|
1746
1846
|
for (const line of lines) {
|
|
1747
1847
|
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
1748
|
-
if (e.type === 'assistant' && e.message?.usage) {
|
|
1848
|
+
if (e.type === 'assistant' && e.message?.usage) {
|
|
1849
|
+
const _m = e.message.model || '';
|
|
1850
|
+
totalCost += _sjCalcCost(_m, e.message.usage);
|
|
1851
|
+
if (!_sjHasPricing(_m)) unknownPricingModels.add(_m.replace(/@.*$/, '').replace(/-\d{8}$/, ''));
|
|
1852
|
+
}
|
|
1749
1853
|
}
|
|
1750
1854
|
} catch {}
|
|
1751
1855
|
}
|
|
1752
|
-
|
|
1856
|
+
// Include projects whose entire spend is on unpriced models — previously they
|
|
1857
|
+
// were dropped by `totalCost > 0` and simply vanished from the cost list.
|
|
1858
|
+
if (totalCost > 0 || unknownPricingModels.size > 0) {
|
|
1859
|
+
projectCosts.push({
|
|
1860
|
+
path: projPath, cost: totalCost, sessions: sessionFiles.length,
|
|
1861
|
+
costIncomplete: unknownPricingModels.size > 0,
|
|
1862
|
+
unknownPricingModels: [...unknownPricingModels],
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1753
1865
|
}
|
|
1754
1866
|
projectCosts.sort((a, b) => b.cost - a.cost);
|
|
1755
1867
|
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
|
|
@@ -3845,7 +3957,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
|
|
|
3845
3957
|
|
|
3846
3958
|
const toolCounts = {};
|
|
3847
3959
|
const agentSpawns = {}; // subagent_type → count
|
|
3848
|
-
let turns = 0, totalCost = 0;
|
|
3960
|
+
let turns = 0, totalCost = 0, _costIncomplete = false;
|
|
3849
3961
|
|
|
3850
3962
|
try {
|
|
3851
3963
|
const raw = fs.readFileSync(fp, 'utf8').replace(/\r\n/g, '\n');
|
|
@@ -3868,7 +3980,11 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
|
|
|
3868
3980
|
agentSpawns[sub] = (agentSpawns[sub] || 0) + 1;
|
|
3869
3981
|
}
|
|
3870
3982
|
}
|
|
3871
|
-
if (e.message?.usage)
|
|
3983
|
+
if (e.message?.usage) {
|
|
3984
|
+
const _m = e.message.model || '';
|
|
3985
|
+
totalCost += _sjCalcCost(_m, e.message.usage);
|
|
3986
|
+
if (!_sjHasPricing(_m)) _costIncomplete = true;
|
|
3987
|
+
}
|
|
3872
3988
|
}
|
|
3873
3989
|
}
|
|
3874
3990
|
} catch {}
|
|
@@ -3877,7 +3993,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
|
|
|
3877
3993
|
nodes.push({
|
|
3878
3994
|
id: sid, type: 'session', label: sid.slice(0,8),
|
|
3879
3995
|
turns, totalTools, toolCounts,
|
|
3880
|
-
cost: totalCost, mtime: stat.mtimeMs, size: stat.size,
|
|
3996
|
+
cost: totalCost, costIncomplete: _costIncomplete, mtime: stat.mtimeMs, size: stat.size,
|
|
3881
3997
|
agentSpawns
|
|
3882
3998
|
});
|
|
3883
3999
|
|
|
@@ -6260,9 +6376,23 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
|
|
|
6260
6376
|
if (wantKg && scope !== 'global') {
|
|
6261
6377
|
try { kgSearch = (await import('../memory/memory-kg.js')).kgSearch; } catch { /* kg unavailable */ }
|
|
6262
6378
|
}
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6379
|
+
// Superseded-version filtering — the SAME rule `searchKnowledge` (and
|
|
6380
|
+
// therefore `monomind doc search` / `knowledge_search`) applies. This
|
|
6381
|
+
// endpoint queries the bridge directly for warmth, so without this it
|
|
6382
|
+
// would inject old document versions the Second Brain itself hides.
|
|
6383
|
+
let liveProj = new Set(), liveGlob = new Set(), isSuperseded = () => false, overfetch = (n) => n;
|
|
6384
|
+
try {
|
|
6385
|
+
const dp = await import('../knowledge/document-pipeline.js');
|
|
6386
|
+
liveProj = dp.liveContentHashes(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
6387
|
+
liveGlob = dp.liveContentHashes(process.env.MONOMIND_GLOBAL_BRAIN_DIR
|
|
6388
|
+
|| path.join(os.homedir(), '.monomind', 'global-brain'));
|
|
6389
|
+
isSuperseded = dp.isSupersededKey;
|
|
6390
|
+
overfetch = dp.supersededOverfetchLimit;
|
|
6391
|
+
} catch { /* pipeline unavailable — no filtering, same as before */ }
|
|
6392
|
+
const keepLive = (rows, live) => (rows || []).filter(r => !isSuperseded(String(r.key || ''), live)).slice(0, limit);
|
|
6393
|
+
const [projRaw, globRaw, rules, graph, mems] = await Promise.all([
|
|
6394
|
+
scope !== 'global' ? bridge.bridgeSearchEntries({ query, namespace, limit: overfetch(limit, liveProj) }).catch(() => null) : null,
|
|
6395
|
+
scope !== 'project' ? bridge.bridgeSearchEntries({ query, namespace: 'knowledge:global', limit: overfetch(limit, liveGlob), dbPath: '@global' }).catch(() => null) : null,
|
|
6266
6396
|
// Distilled rules ("when X do Y") learned from sessions/runs —
|
|
6267
6397
|
// small namespace, high injection value, threshold keeps it quiet.
|
|
6268
6398
|
scope !== 'global' && wantRules ? bridge.bridgeSearchEntries({ query, namespace: 'rules', limit: 2, threshold: 0.45 }).catch(() => null) : null,
|
|
@@ -6274,8 +6404,8 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
|
|
|
6274
6404
|
scope !== 'global' && wantMemory ? bridge.bridgeSearchEntries({ query, namespace: 'patterns', limit: 2, threshold: 0.4 }).catch(() => null) : null,
|
|
6275
6405
|
]);
|
|
6276
6406
|
const lists = [
|
|
6277
|
-
(
|
|
6278
|
-
(
|
|
6407
|
+
keepLive(projRaw?.results, liveProj).map(r => ({ id: r.id, key: r.key, content: String(r.content || '').slice(0, 2000), score: r.score + 0.05, global: false, tags: r.tags, importance: 0.6 })),
|
|
6408
|
+
keepLive(globRaw?.results, liveGlob).map(r => ({ id: r.id, key: r.key, content: String(r.content || '').slice(0, 2000), score: r.score, global: true, tags: r.tags })),
|
|
6279
6409
|
(rules?.results || []).map(r => ({ id: r.id, key: r.key, content: String(r.content || '').slice(0, 2000), score: r.score + 0.05, global: false, rule: true, tags: r.tags, importance: 0.7 })),
|
|
6280
6410
|
(graph?.triplets || []).map((t, i) => ({
|
|
6281
6411
|
id: 'kg:' + i + ':' + t.source + '|' + t.relation + '|' + t.target,
|
|
@@ -6303,7 +6433,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
|
|
|
6303
6433
|
} catch { /* best effort */ }
|
|
6304
6434
|
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
6305
6435
|
res.end(JSON.stringify({
|
|
6306
|
-
method:
|
|
6436
|
+
method: projRaw?.searchMethod || globRaw?.searchMethod || rules?.searchMethod || mems?.searchMethod
|
|
6307
6437
|
|| (merged.length ? 'mixed' : 'none'),
|
|
6308
6438
|
results: merged,
|
|
6309
6439
|
}));
|