@monoes/monomindcli 2.9.1 → 2.9.3
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/helpers/statusline.cjs +45 -0
- package/.claude/helpers/token-tracker.cjs +13 -1
- package/.claude/settings.json +0 -1
- package/.claude/skills/github-toolkit/SKILL.md +60 -0
- package/.claude/skills/mastermind-createorg/SKILL.md +8 -2
- package/.claude/skills/memory-toolkit/SKILL.md +75 -0
- package/.claude/skills/monodesign/scripts/detector/rules/checks.mjs +24 -1
- package/README.md +1 -1
- package/dist/src/agents/registry-builder.d.ts.map +1 -1
- package/dist/src/agents/registry-builder.js +94 -1
- package/dist/src/agents/registry-builder.js.map +1 -1
- package/dist/src/capabilities/cap-documents.js +7 -7
- package/dist/src/capabilities/cap-documents.js.map +1 -1
- package/dist/src/commands/completions.js +2 -2
- package/dist/src/commands/completions.js.map +1 -1
- package/dist/src/commands/doctor-env-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-env-checks.js +14 -2
- package/dist/src/commands/doctor-env-checks.js.map +1 -1
- package/dist/src/commands/index.d.ts +27 -41
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +113 -207
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/init-wizard.d.ts.map +1 -1
- package/dist/src/commands/init-wizard.js +35 -0
- package/dist/src/commands/init-wizard.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +43 -0
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/neural-optimize.d.ts.map +1 -1
- package/dist/src/commands/neural-optimize.js +12 -8
- package/dist/src/commands/neural-optimize.js.map +1 -1
- package/dist/src/commands/org-observe.js +2 -2
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -9
- package/dist/src/index.js.map +1 -1
- package/dist/src/init/claudemd-generator.js +2 -2
- package/dist/src/init/claudemd-generator.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +21 -4
- 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 +16 -37
- package/dist/src/init/settings-generator.js.map +1 -1
- package/dist/src/init/statusline-generator.d.ts.map +1 -1
- package/dist/src/init/statusline-generator.js +45 -0
- package/dist/src/init/statusline-generator.js.map +1 -1
- package/dist/src/init/types.d.ts +8 -0
- package/dist/src/init/types.d.ts.map +1 -1
- package/dist/src/init/types.js.map +1 -1
- package/dist/src/init/upgrade.d.ts.map +1 -1
- package/dist/src/init/upgrade.js +3 -9
- package/dist/src/init/upgrade.js.map +1 -1
- package/dist/src/knowledge/document-pipeline.d.ts.map +1 -1
- package/dist/src/knowledge/document-pipeline.js +1 -0
- package/dist/src/knowledge/document-pipeline.js.map +1 -1
- package/dist/src/mcp-tools/memory-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/memory-tools.js +7 -15
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/memory/bm25-index.d.ts +17 -0
- package/dist/src/memory/bm25-index.d.ts.map +1 -1
- package/dist/src/memory/bm25-index.js +17 -0
- package/dist/src/memory/bm25-index.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts +4 -0
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +128 -22
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-initializer.d.ts.map +1 -1
- package/dist/src/memory/memory-initializer.js +0 -50
- package/dist/src/memory/memory-initializer.js.map +1 -1
- package/dist/src/memory/memory-kg.d.ts.map +1 -1
- package/dist/src/memory/memory-kg.js +21 -5
- package/dist/src/memory/memory-kg.js.map +1 -1
- package/dist/src/monovector/diff-classifier.d.ts.map +1 -1
- package/dist/src/monovector/diff-classifier.js +11 -20
- package/dist/src/monovector/diff-classifier.js.map +1 -1
- package/dist/src/orgrt/antigravity-runner.d.ts +9 -0
- package/dist/src/orgrt/antigravity-runner.d.ts.map +1 -0
- package/dist/src/orgrt/antigravity-runner.js +250 -0
- package/dist/src/orgrt/antigravity-runner.js.map +1 -0
- package/dist/src/orgrt/codex-runner.d.ts +9 -0
- package/dist/src/orgrt/codex-runner.d.ts.map +1 -0
- package/dist/src/orgrt/codex-runner.js +216 -0
- package/dist/src/orgrt/codex-runner.js.map +1 -0
- package/dist/src/orgrt/cross-org.d.ts.map +1 -1
- package/dist/src/orgrt/cross-org.js +8 -0
- package/dist/src/orgrt/cross-org.js.map +1 -1
- package/dist/src/orgrt/daemon.d.ts +14 -7
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +72 -12
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/fence.d.ts +39 -0
- package/dist/src/orgrt/fence.d.ts.map +1 -0
- package/dist/src/orgrt/fence.js +75 -0
- package/dist/src/orgrt/fence.js.map +1 -0
- package/dist/src/orgrt/kimicode-runner.d.ts.map +1 -1
- package/dist/src/orgrt/kimicode-runner.js +27 -1
- package/dist/src/orgrt/kimicode-runner.js.map +1 -1
- package/dist/src/orgrt/provider.d.ts.map +1 -1
- package/dist/src/orgrt/provider.js +26 -0
- package/dist/src/orgrt/provider.js.map +1 -1
- package/dist/src/orgrt/session.d.ts +11 -1
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +60 -3
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/orgrt/tool-fence.d.ts +7 -2
- package/dist/src/orgrt/tool-fence.d.ts.map +1 -1
- package/dist/src/orgrt/tool-fence.js +18 -2
- package/dist/src/orgrt/tool-fence.js.map +1 -1
- package/dist/src/orgrt/types.d.ts +222 -0
- package/dist/src/orgrt/types.d.ts.map +1 -1
- package/dist/src/orgrt/types.js +34 -3
- package/dist/src/orgrt/types.js.map +1 -1
- package/dist/src/orgrt/vercel-providers.d.ts +29 -0
- package/dist/src/orgrt/vercel-providers.d.ts.map +1 -0
- package/dist/src/orgrt/vercel-providers.js +162 -0
- package/dist/src/orgrt/vercel-providers.js.map +1 -0
- package/dist/src/orgrt/vercel-runner.d.ts +39 -0
- package/dist/src/orgrt/vercel-runner.d.ts.map +1 -0
- package/dist/src/orgrt/vercel-runner.js +121 -0
- package/dist/src/orgrt/vercel-runner.js.map +1 -0
- package/dist/src/orgrt/vercel-session-store.d.ts +12 -0
- package/dist/src/orgrt/vercel-session-store.d.ts.map +1 -0
- package/dist/src/orgrt/vercel-session-store.js +49 -0
- package/dist/src/orgrt/vercel-session-store.js.map +1 -0
- package/dist/src/pricing/model-pricing.d.ts +10 -1
- package/dist/src/pricing/model-pricing.d.ts.map +1 -1
- package/dist/src/pricing/model-pricing.js +18 -1
- package/dist/src/pricing/model-pricing.js.map +1 -1
- package/dist/src/services/crash-reporter.d.ts +6 -5
- package/dist/src/services/crash-reporter.d.ts.map +1 -1
- package/dist/src/services/crash-reporter.js +6 -81
- package/dist/src/services/crash-reporter.js.map +1 -1
- package/dist/src/ui/collector.mjs +16 -82
- package/dist/src/ui/dashboard.html +176 -545
- package/dist/src/ui/data/mastermind-sessions.json +1 -0
- package/dist/src/ui/markdown.js +273 -0
- package/dist/src/ui/server.mjs +99 -189
- package/dist/src/utils/input-guards.d.ts +4 -1
- package/dist/src/utils/input-guards.d.ts.map +1 -1
- package/dist/src/utils/input-guards.js +6 -4
- package/dist/src/utils/input-guards.js.map +1 -1
- package/dist/src/utils/redaction.d.ts +24 -0
- package/dist/src/utils/redaction.d.ts.map +1 -0
- package/dist/src/utils/redaction.js +190 -0
- package/dist/src/utils/redaction.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -5
- package/scripts/check-workspace-deps.mjs +45 -0
- package/scripts/publish.sh +24 -33
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// packages/@monomind/cli/src/ui/markdown.js
|
|
2
|
+
// High-fidelity markdown renderer, extracted out of dashboard.html (#124 —
|
|
3
|
+
// it was one of four hand-copied markdown-to-HTML renderers in that file,
|
|
4
|
+
// each with different escaping coverage; this was the most complete one,
|
|
5
|
+
// so the others were left in place rather than forced onto this one's
|
|
6
|
+
// styling without a way to visually verify every call site).
|
|
7
|
+
//
|
|
8
|
+
// Loaded as a classic <script src="markdown.js"> (see the tag right before
|
|
9
|
+
// the main inline <script> in dashboard.html) — NOT an ES module — so
|
|
10
|
+
// renderDocMarkdown and gdCopyCode stay plain globals, exactly like every
|
|
11
|
+
// other dashboard.html function, and every existing call site keeps working
|
|
12
|
+
// unmodified.
|
|
13
|
+
//
|
|
14
|
+
// Handles: headings (h1-h6) with anchors, bold/italic/strikethrough, inline
|
|
15
|
+
// code and fenced code blocks (with language label + copy button), unordered/
|
|
16
|
+
// ordered/nested/task lists, GFM tables, nested blockquotes, horizontal rules,
|
|
17
|
+
// links (external get rel=noopener + target=_blank), images, line breaks.
|
|
18
|
+
// HTML-escaped at the boundary; nothing the source contains can inject markup.
|
|
19
|
+
function renderDocMarkdown(md) {
|
|
20
|
+
if (!md) return '';
|
|
21
|
+
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
22
|
+
// Escapes a URL for safe insertion into a double-quoted HTML attribute.
|
|
23
|
+
// #124: links/images previously interpolated `href`/`src` RAW — the
|
|
24
|
+
// sanitizer below (sanitizeHtmlBlock) only ever ran on raw HTML blocks,
|
|
25
|
+
// never on markdown-link URLs, so `[x](" onmouseover="alert(1))` could
|
|
26
|
+
// break out of the attribute. `esc()` alone is sufficient here since a
|
|
27
|
+
// quote is the only character that lets a URL escape a double-quoted
|
|
28
|
+
// attribute value.
|
|
29
|
+
const escAttr = (s) => String(s).replace(/"/g, '"');
|
|
30
|
+
// #124-review: escAttr() alone stops attribute-breakout XSS (a `"` can't
|
|
31
|
+
// escape the quoted attribute value) but does nothing about SCHEME-based
|
|
32
|
+
// execution — `[click me](javascript:alert(document.cookie))` rendered as
|
|
33
|
+
// a fully clickable, executable link even after the escAttr() fix, and
|
|
34
|
+
// sanitizeHtmlBlock() below (which DOES filter javascript: hrefs/srcs)
|
|
35
|
+
// only ever runs on raw HTML blocks, never on markdown-syntax links/
|
|
36
|
+
// images. Reject any scheme outside an explicit allowlist — relative/
|
|
37
|
+
// same-origin URLs (no colon before the first `/`) are always allowed.
|
|
38
|
+
const SAFE_URL_SCHEMES = /^(https?|mailto|tel):/i;
|
|
39
|
+
const safeUrl = (u) => {
|
|
40
|
+
const s = String(u);
|
|
41
|
+
const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s);
|
|
42
|
+
if (hasScheme && !SAFE_URL_SCHEMES.test(s)) return '#';
|
|
43
|
+
return s;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 1. Strip a leading YAML frontmatter block (mastermind plan/spec files have one).
|
|
47
|
+
let src = String(md).replace(/^?---[\s\S]*?\n---\s*\n/, '');
|
|
48
|
+
|
|
49
|
+
// 2. Extract fenced code blocks first so their contents are never reinterpreted.
|
|
50
|
+
// #124-review: previously unanchored (`/```.../g`), so it matched a fence
|
|
51
|
+
// even when preceded by a blockquote's "> " marker, extracting it at the
|
|
52
|
+
// OUTER (top-level) call. The bare placeholder left behind then got
|
|
53
|
+
// captured as ordinary blockquote body text, and flushBQ()'s recursive
|
|
54
|
+
// renderDocMarkdown() call resolves placeholders against its OWN fresh,
|
|
55
|
+
// empty codeBlocks array -- silently producing an empty, unlabeled code
|
|
56
|
+
// block instead of the real content. Anchoring the match to true
|
|
57
|
+
// line-start (mirroring the HTML-block regex below, which already does
|
|
58
|
+
// this and was never affected) means a blockquote-nested fence is simply
|
|
59
|
+
// left alone here and gets extracted correctly by the recursive call
|
|
60
|
+
// instead, once the "> " markers have been stripped from each line.
|
|
61
|
+
const codeBlocks = [];
|
|
62
|
+
src = src.replace(/(^|\n)```([\w+-]*)\n?([\s\S]*?)```/g, (_m, lead, lang, code) => {
|
|
63
|
+
const clean = code.replace(/\n$/, '');
|
|
64
|
+
codeBlocks.push({ lang: (lang || '').toLowerCase(), code: clean });
|
|
65
|
+
return (lead || '') + '\u0000CB' + (codeBlocks.length - 1) + '\u0000';
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// 3. Extract inline HTML blocks (multiline <div>…</div> etc.) — pass through
|
|
69
|
+
// verbatim after escaping, so user-authored HTML in a plan still renders
|
|
70
|
+
// structurally without enabling script injection (we'll re-strip <script>
|
|
71
|
+
// and on* handlers at the end).
|
|
72
|
+
const htmlBlocks = [];
|
|
73
|
+
src = src.replace(/(^|\n)(<(\w+)[^>\n]*>[\s\S]*?<\/\3>)(?=\n|$)/g, (_m, lead, html) => {
|
|
74
|
+
htmlBlocks.push(html);
|
|
75
|
+
return (lead || '') + '\u0000HB' + (htmlBlocks.length - 1) + '\u0000';
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// 4. Escape everything that remains.
|
|
79
|
+
src = esc(src).replace(/\u0000CB(\d+)\u0000/g, 'CB$1').replace(/\u0000HB(\d+)\u0000/g, 'HB$1');
|
|
80
|
+
|
|
81
|
+
// 5. Block-level transforms: tables, headings, hr, blockquotes, lists.
|
|
82
|
+
// Operate line-by-line so nesting and paragraph grouping stay coherent.
|
|
83
|
+
const lines = src.split('\n');
|
|
84
|
+
const out = [];
|
|
85
|
+
let i = 0;
|
|
86
|
+
let inUl = false, inOl = false, inTask = false, bqBuf = [], paraBuf = [];
|
|
87
|
+
|
|
88
|
+
const closeLists = () => {
|
|
89
|
+
if (inUl) { out.push('</ul>'); inUl = false; }
|
|
90
|
+
if (inOl) { out.push('</ol>'); inOl = false; }
|
|
91
|
+
if (inTask) { out.push('</ul>'); inTask = false; }
|
|
92
|
+
};
|
|
93
|
+
const flushPara = () => { if (paraBuf.length) { out.push('<p>' + paraBuf.join(' ') + '</p>'); paraBuf = []; } };
|
|
94
|
+
// Reverses esc() — bqBuf holds lines captured AFTER step 4's escaping, so
|
|
95
|
+
// recursing back into renderDocMarkdown() (which escapes its input itself)
|
|
96
|
+
// would double-escape (`<` -> `&lt;`) without this.
|
|
97
|
+
const unescapeOnce = (s) => String(s).replace(/"/g, '"').replace(/>/g, '>').replace(/</g, '<').replace(/&/g, '&');
|
|
98
|
+
const flushBQ = () => {
|
|
99
|
+
if (!bqBuf.length) return;
|
|
100
|
+
// Recursively render the inner block so nested formatting works.
|
|
101
|
+
out.push('<blockquote>' + renderDocMarkdown(unescapeOnce(bqBuf.join('\n'))) + '</blockquote>');
|
|
102
|
+
bqBuf = [];
|
|
103
|
+
};
|
|
104
|
+
const slugify = (s) => String(s).toLowerCase().replace(/<[^>]+>/g, '').replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-');
|
|
105
|
+
|
|
106
|
+
while (i < lines.length) {
|
|
107
|
+
let line = lines[i];
|
|
108
|
+
|
|
109
|
+
// Block placeholders for code/html blocks (they sit on their own line).
|
|
110
|
+
const cbMatch = line.match(/^CB(\d+)$/);
|
|
111
|
+
if (cbMatch) { closeLists(); flushPara(); flushBQ(); out.push(renderCodeBlock(codeBlocks[+cbMatch[1]])); i++; continue; }
|
|
112
|
+
const hbMatch = line.match(/^HB(\d+)$/);
|
|
113
|
+
if (hbMatch) { closeLists(); flushPara(); flushBQ(); out.push(sanitizeHtmlBlock(htmlBlocks[+hbMatch[1]])); i++; continue; }
|
|
114
|
+
|
|
115
|
+
// Blank line: paragraph + list breaks.
|
|
116
|
+
if (line.trim() === '') { closeLists(); flushPara(); flushBQ(); i++; continue; }
|
|
117
|
+
|
|
118
|
+
// Headings (with anchor).
|
|
119
|
+
const h = line.match(/^(#{1,6})\s+(.+?)(?:\s+#+\s*)?$/);
|
|
120
|
+
if (h) { closeLists(); flushPara(); flushBQ(); const lvl = h[1].length; const txt = inlineMd(h[2]); const id = slugify(h[2]); out.push('<h' + lvl + ' id="' + id + '">' + txt + '</h' + lvl + '>'); i++; continue; }
|
|
121
|
+
|
|
122
|
+
// Horizontal rule.
|
|
123
|
+
if (/^(\*\s*){3,}$/.test(line) || /^(-\s*){3,}$/.test(line) || /^(_\s*){3,}$/.test(line)) { closeLists(); flushPara(); flushBQ(); out.push('<hr>'); i++; continue; }
|
|
124
|
+
|
|
125
|
+
// GFM table — a block of consecutive lines where line 0 and line 1 look
|
|
126
|
+
// like | a | b | / |---|---|. Scan forward to collect the full table.
|
|
127
|
+
if (/^\s*\|.+\|\s*$/.test(line) && i + 1 < lines.length && /^\s*\|?[\s:-]+\|[\s:|-]+\|?\s*$/.test(lines[i + 1])) {
|
|
128
|
+
closeLists(); flushPara(); flushBQ();
|
|
129
|
+
const headerCells = splitTableRow(line);
|
|
130
|
+
const aligns = parseTableAligns(lines[i + 1]);
|
|
131
|
+
i += 2;
|
|
132
|
+
const rows = [];
|
|
133
|
+
while (i < lines.length && /^\s*\|.+\|\s*$/.test(lines[i])) { rows.push(splitTableRow(lines[i])); i++; }
|
|
134
|
+
out.push(renderTable(headerCells, aligns, rows));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Blockquote. `line` has already been through esc() (step 4 above), so a
|
|
139
|
+
// source `>` is now the literal text `>` by this point — match that,
|
|
140
|
+
// not a raw `>` (which can never appear here and never matched).
|
|
141
|
+
const bq = line.match(/^>\s?(.*)$/);
|
|
142
|
+
if (bq) { closeLists(); flushPara(); bqBuf.push(bq[1]); i++; continue; }
|
|
143
|
+
if (bqBuf.length) { flushBQ(); }
|
|
144
|
+
|
|
145
|
+
// Task list item.
|
|
146
|
+
const task = line.match(/^(\s*)([-*])\s+\[( |x|X)\]\s+(.+)$/);
|
|
147
|
+
if (task) { flushPara(); if (inUl) { out.push('</ul>'); inUl = false; } if (inOl) { out.push('</ol>'); inOl = false; } if (!inTask) { out.push('<ul class="md-tasklist">'); inTask = true; } const checked = task[3].toLowerCase() === 'x'; out.push('<li><input type="checkbox" disabled' + (checked ? ' checked' : '') + '> ' + inlineMd(task[4]) + '</li>'); i++; continue; }
|
|
148
|
+
|
|
149
|
+
// Unordered list item.
|
|
150
|
+
const ul = line.match(/^(\s*)([-*+])\s+(.+)$/);
|
|
151
|
+
if (ul && !task) {
|
|
152
|
+
flushPara();
|
|
153
|
+
if (inOl) { out.push('</ol>'); inOl = false; }
|
|
154
|
+
if (inTask) { out.push('</ul>'); inTask = false; }
|
|
155
|
+
if (!inUl) { out.push('<ul>'); inUl = true; }
|
|
156
|
+
const indent = ul[1].length;
|
|
157
|
+
if (indent === 0) out.push('<li>' + inlineMd(ul[3]) + '</li>');
|
|
158
|
+
else {
|
|
159
|
+
// Nested list — close current top item and open a nested <ul>.
|
|
160
|
+
out.push('<li>' + inlineMd(ul[3]) + '</li>');
|
|
161
|
+
}
|
|
162
|
+
i++; continue;
|
|
163
|
+
}
|
|
164
|
+
// Ordered list item.
|
|
165
|
+
const ol = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
|
|
166
|
+
if (ol) {
|
|
167
|
+
flushPara();
|
|
168
|
+
if (inUl) { out.push('</ul>'); inUl = false; }
|
|
169
|
+
if (inTask) { out.push('</ul>'); inTask = false; }
|
|
170
|
+
if (!inOl) { out.push('<ol>'); inOl = true; }
|
|
171
|
+
out.push('<li>' + inlineMd(ol[3]) + '</li>');
|
|
172
|
+
i++; continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Otherwise: paragraph text. Buffer until a blank line / structural break.
|
|
176
|
+
closeLists();
|
|
177
|
+
flushBQ();
|
|
178
|
+
paraBuf.push(inlineMd(line));
|
|
179
|
+
i++;
|
|
180
|
+
}
|
|
181
|
+
closeLists(); flushPara(); flushBQ();
|
|
182
|
+
|
|
183
|
+
let html = out.join('\n');
|
|
184
|
+
// 6. Restore code/html block placeholders INSIDE paragraphs (when a CB/HB
|
|
185
|
+
// token got buffered as paragraph text instead of sitting alone).
|
|
186
|
+
html = html.replace(/CB(\d+)/g, (_m, idx) => renderCodeBlock(codeBlocks[+idx]))
|
|
187
|
+
.replace(/HB(\d+)/g, (_m, idx) => sanitizeHtmlBlock(htmlBlocks[+idx]));
|
|
188
|
+
return html;
|
|
189
|
+
|
|
190
|
+
// ── helpers ──
|
|
191
|
+
function inlineMd(s) {
|
|
192
|
+
// Inline code first (so its contents aren't reformatted).
|
|
193
|
+
const codes = [];
|
|
194
|
+
s = s.replace(/`([^`\n]+)`/g, (_m, c) => { codes.push(c); return '\u0001C' + (codes.length - 1) + '\u0001'; });
|
|
195
|
+
// Images: 
|
|
196
|
+
s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g,
|
|
197
|
+
(_m, alt, src, title) => '<img src="' + escAttr(safeUrl(src)) + '" alt="' + esc(alt) + '"' + (title ? ' title="' + esc(title) + '"' : '') + ' loading="lazy" style="max-width:100%;height:auto;border-radius:6px">');
|
|
198
|
+
// Links: [text](href "title")
|
|
199
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g,
|
|
200
|
+
(_m, txt, href, title) => {
|
|
201
|
+
const external = /^(https?:|mailto:|tel:)/i.test(href);
|
|
202
|
+
const safe = external ? ' target="_blank" rel="noopener noreferrer"' : '';
|
|
203
|
+
return '<a href="' + escAttr(safeUrl(href)) + '"' + (title ? ' title="' + esc(title) + '"' : '') + safe + '>' + txt + '</a>';
|
|
204
|
+
});
|
|
205
|
+
// Bold.
|
|
206
|
+
s = s.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
|
|
207
|
+
s = s.replace(/__([^_\n]+)__/g, '<strong>$1</strong>');
|
|
208
|
+
// Italic.
|
|
209
|
+
s = s.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
|
|
210
|
+
s = s.replace(/(^|[^_])_([^_\n]+)_/g, '$1<em>$2</em>');
|
|
211
|
+
// Strikethrough.
|
|
212
|
+
s = s.replace(/~~([^~\n]+)~~/g, '<del>$1</del>');
|
|
213
|
+
// Restore inline code.
|
|
214
|
+
s = s.replace(/\u0001C(\d+)\u0001/g, (_m, idx) => '<code>' + esc(codes[+idx]) + '</code>');
|
|
215
|
+
return s;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function renderCodeBlock(block) {
|
|
219
|
+
const lang = block?.lang || '';
|
|
220
|
+
const code = block?.code ?? '';
|
|
221
|
+
const labelHtml = lang ? '<span class="gd-code-lang">' + esc(lang) + '</span>' : '';
|
|
222
|
+
return '<div class="gd-code-block">' +
|
|
223
|
+
'<div class="gd-code-bar"><button class="gd-code-copy" onclick="gdCopyCode(this)">copy</button>' + labelHtml + '</div>' +
|
|
224
|
+
'<pre><code' + (lang ? ' class="language-' + esc(lang) + '"' : '') + '>' + esc(code) + '</code></pre>' +
|
|
225
|
+
'</div>';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function sanitizeHtmlBlock(html) {
|
|
229
|
+
// Strip <script> entirely, drop event-handler attributes (onclick=, onload=, on*=),
|
|
230
|
+
// and javascript: URLs. Keep everything else as-is so user HTML renders.
|
|
231
|
+
return String(html)
|
|
232
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
233
|
+
.replace(/\son\w+\s*=\s*"[^"]*"/gi, '')
|
|
234
|
+
.replace(/\son\w+\s*=\s*'[^']*'/gi, '')
|
|
235
|
+
.replace(/\son\w+\s*=\s*[^\s>]+/gi, '')
|
|
236
|
+
.replace(/(href|src)\s*=\s*"javascript:[^"]*"/gi, '$1="#"');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function splitTableRow(line) {
|
|
240
|
+
return line.trim().replace(/^\||\|$/g, '').split('|').map(c => c.trim());
|
|
241
|
+
}
|
|
242
|
+
function parseTableAligns(line) {
|
|
243
|
+
return splitTableRow(line).map(cell => {
|
|
244
|
+
if (/^:\s*-+:$/.test(cell)) return 'center';
|
|
245
|
+
if (/^-*:$/.test(cell)) return 'left';
|
|
246
|
+
if (/^-+:$/.test(cell)) return 'right';
|
|
247
|
+
return null;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function renderTable(headerCells, aligns, rows) {
|
|
251
|
+
const alignAttr = (i) => aligns[i] ? ' style="text-align:' + aligns[i] + '"' : '';
|
|
252
|
+
let html = '<div class="gd-table-wrap"><table><thead><tr>';
|
|
253
|
+
headerCells.forEach((c, i) => { html += '<th' + alignAttr(i) + '>' + inlineMd(c) + '</th>'; });
|
|
254
|
+
html += '</tr></thead><tbody>';
|
|
255
|
+
for (const r of rows) {
|
|
256
|
+
html += '<tr>';
|
|
257
|
+
r.forEach((c, i) => { html += '<td' + alignAttr(i) + '>' + inlineMd(c) + '</td>'; });
|
|
258
|
+
html += '</tr>';
|
|
259
|
+
}
|
|
260
|
+
html += '</tbody></table></div>';
|
|
261
|
+
return html;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function gdCopyCode(btn) {
|
|
266
|
+
const codeEl = btn.closest('.gd-code-block')?.querySelector('code');
|
|
267
|
+
if (!codeEl) return;
|
|
268
|
+
const text = codeEl.textContent;
|
|
269
|
+
navigator.clipboard.writeText(text).then(
|
|
270
|
+
() => { const old = btn.textContent; btn.textContent = 'copied'; setTimeout(() => { btn.textContent = old; }, 1200); },
|
|
271
|
+
() => { btn.textContent = 'failed'; },
|
|
272
|
+
);
|
|
273
|
+
}
|
package/dist/src/ui/server.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import zlib from 'zlib';
|
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
|
-
import { collectAll, getWatchPaths, collectProject, collectSessions,
|
|
10
|
+
import { collectAll, getWatchPaths, collectProject, collectSessions, collectAgents, collectTokens, collectHooks, collectKnowledge, collectMetrics, collectMemory, collectMemoryFiles, collectSystem } from './collector.mjs';
|
|
11
11
|
import { addSseClient, removeSseClient, broadcast, getSseClientCount, closeSseClients, addMmClient, removeMmClient, broadcastMm, getMmClientCount } from './sse-manager.mjs';
|
|
12
12
|
import { handleMonographRoutes } from './routes-monograph.mjs';
|
|
13
13
|
import { handleOrgRoutes } from './routes-org.mjs';
|
|
@@ -20,10 +20,16 @@ const buildDocsState = new Map();
|
|
|
20
20
|
|
|
21
21
|
// Pricing per token (mirrors token-tracker.cjs FALLBACK_PRICING)
|
|
22
22
|
const _SJ_PRICING = {
|
|
23
|
+
'claude-fable-5': { in: 10e-6, out: 50e-6, cw: 12.5e-6, cr: 1e-6 },
|
|
24
|
+
'claude-mythos-5': { in: 10e-6, out: 50e-6, cw: 12.5e-6, cr: 1e-6 },
|
|
25
|
+
'claude-opus-5': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
|
|
23
26
|
'claude-opus-4-8': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
|
|
27
|
+
'claude-opus-4-7': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
|
|
24
28
|
'claude-opus-4-6': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
|
|
25
29
|
'claude-opus-4-5': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
|
|
30
|
+
'claude-opus-4-1': { in: 15e-6, out: 75e-6, cw: 18.75e-6, cr: 1.5e-6 },
|
|
26
31
|
'claude-opus-4': { in: 15e-6, out: 75e-6, cw: 18.75e-6, cr: 1.5e-6 },
|
|
32
|
+
'claude-sonnet-5': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
|
|
27
33
|
'claude-sonnet-4-6': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
|
|
28
34
|
'claude-sonnet-4-5': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
|
|
29
35
|
'claude-sonnet-4': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
|
|
@@ -32,6 +38,7 @@ const _SJ_PRICING = {
|
|
|
32
38
|
'claude-haiku-4-5': { in: 1e-6, out: 5e-6, cw: 1.25e-6, cr: 0.1e-6 },
|
|
33
39
|
'claude-haiku-4': { in: 0.8e-6, out: 4e-6, cw: 1e-6, cr: 0.08e-6 },
|
|
34
40
|
'claude-3-5-haiku': { in: 0.8e-6, out: 4e-6, cw: 1e-6, cr: 0.08e-6 },
|
|
41
|
+
'gpt-5': { in: 2.5e-6, out: 10e-6, cw: 2.5e-6, cr: 1.25e-6 },
|
|
35
42
|
'gpt-4o': { in: 2.5e-6, out: 10e-6, cw: 2.5e-6, cr: 1.25e-6 },
|
|
36
43
|
'gpt-4o-mini': { in: 0.15e-6, out: 0.6e-6, cw: 0.15e-6, cr: 0.075e-6 },
|
|
37
44
|
'gemini-2.5-pro': { in: 1.25e-6, out: 10e-6, cw: 1.25e-6, cr: 0.315e-6 },
|
|
@@ -179,7 +186,6 @@ function buildSectionData(name, dir) {
|
|
|
179
186
|
const d = path.resolve(dir);
|
|
180
187
|
switch (name) {
|
|
181
188
|
case 'sessions': return { sessions: collectSessions(d) };
|
|
182
|
-
case 'swarm': return { swarm: collectSwarm(d), swarmHistory: collectSwarmHistory(d), agents: collectAgents(d) };
|
|
183
189
|
case 'agents': return { agents: collectAgents(d) };
|
|
184
190
|
case 'tokens': return { tokens: collectTokens(d) };
|
|
185
191
|
case 'hooks': return { hooks: collectHooks(d) };
|
|
@@ -1336,7 +1342,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true,
|
|
|
1336
1342
|
// (all of /api/*, GET or not) requires _checkAuth. These serve the HTML/JS
|
|
1337
1343
|
// that embeds the token in a <meta name="mm-token"> tag on page load, so
|
|
1338
1344
|
// they must be reachable before any fetch() call could attach the token.
|
|
1339
|
-
const _OPEN_ROUTES = new Set(['/', '/v2', '/mastermind', '/orgs', '/orgs-files.js', '/favicon.ico']);
|
|
1345
|
+
const _OPEN_ROUTES = new Set(['/', '/v2', '/mastermind', '/orgs', '/orgs-files.js', '/markdown.js', '/favicon.ico']);
|
|
1340
1346
|
const _isOpenRoute = (url, method) =>
|
|
1341
1347
|
(method === 'GET' || method === 'HEAD' || method === 'OPTIONS')
|
|
1342
1348
|
&& (_OPEN_ROUTES.has(url) || /^\/data\/avatars\/[A-Za-z0-9._-]+\.svg$/.test(url));
|
|
@@ -1400,6 +1406,25 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true,
|
|
|
1400
1406
|
return;
|
|
1401
1407
|
}
|
|
1402
1408
|
|
|
1409
|
+
// ------------------------------------------------ GET /markdown.js
|
|
1410
|
+
// Markdown renderer, split out of dashboard.html (#124) — same pattern
|
|
1411
|
+
// as GET /orgs-files.js in routes-org.mjs (a sibling asset served via
|
|
1412
|
+
// its own hardcoded route, not a generic static-file handler). Must be
|
|
1413
|
+
// in _OPEN_ROUTES above: dashboard.html's own <script src="markdown.js">
|
|
1414
|
+
// request happens before the page has the auth token to attach.
|
|
1415
|
+
if (req.method === 'GET' && url === '/markdown.js') {
|
|
1416
|
+
try {
|
|
1417
|
+
const jsPath = path.join(__dirname, 'markdown.js');
|
|
1418
|
+
const js = fs.readFileSync(jsPath, 'utf8');
|
|
1419
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
|
|
1420
|
+
res.end(js);
|
|
1421
|
+
} catch (err) {
|
|
1422
|
+
res.writeHead(404);
|
|
1423
|
+
res.end(`markdown.js not found: ${err.message}`);
|
|
1424
|
+
}
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1403
1428
|
// ------------------------------------------------- GET /data/avatars/*.svg (agent avatars)
|
|
1404
1429
|
if (req.method === 'GET' && /^\/data\/avatars\/[A-Za-z0-9._-]+\.svg$/.test(url)) {
|
|
1405
1430
|
try {
|
|
@@ -3336,160 +3361,40 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true,
|
|
|
3336
3361
|
return;
|
|
3337
3362
|
}
|
|
3338
3363
|
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
if (req.method === 'GET' && url === '/api/graph') {
|
|
3364
|
+
// -------------------------------------------------------- GET /api/docs
|
|
3365
|
+
if (req.method === 'GET' && url === '/api/docs') {
|
|
3342
3366
|
try {
|
|
3343
3367
|
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
3344
3368
|
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
3345
|
-
const
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
const
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
.
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
if (name === 'Bash') return 'bash';
|
|
3367
|
-
if (['Agent','Task'].includes(name)) return 'agent';
|
|
3368
|
-
if (name.startsWith('mcp__monomind__memory') || name.startsWith('mcp__monomind__agentdb')) return 'memory';
|
|
3369
|
-
if (['WebFetch','WebSearch'].includes(name)) return 'web';
|
|
3370
|
-
if (name === 'Skill') return 'skill';
|
|
3371
|
-
return 'other';
|
|
3372
|
-
};
|
|
3373
|
-
|
|
3374
|
-
const nodes = [];
|
|
3375
|
-
const edges = [];
|
|
3376
|
-
const agentTypeNodes = {}; // subagent_type → node id
|
|
3377
|
-
|
|
3378
|
-
for (const fname of sessionFiles) {
|
|
3379
|
-
const sid = fname.replace('.jsonl','');
|
|
3380
|
-
const fp = path.join(sessionsDir, fname);
|
|
3381
|
-
let stat = null;
|
|
3382
|
-
try { stat = fs.statSync(fp); } catch { continue; }
|
|
3383
|
-
|
|
3384
|
-
// Skip files over size cap to avoid memory spikes on large sessions
|
|
3385
|
-
// But still do a lightweight scan for agent spawns (tool_use blocks named Agent/Task)
|
|
3386
|
-
if (stat.size > JSONL_SIZE_CAP) {
|
|
3387
|
-
const truncSpawns = {};
|
|
3388
|
-
try {
|
|
3389
|
-
const raw = fs.readFileSync(fp, 'utf8');
|
|
3390
|
-
for (const line of raw.split('\n')) {
|
|
3391
|
-
if (!line.includes('"tool_use"') || (!line.includes('"Agent"') && !line.includes('"Task"'))) continue;
|
|
3392
|
-
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
3393
|
-
if (e.type !== 'assistant') continue;
|
|
3394
|
-
for (const block of (e.message?.content || [])) {
|
|
3395
|
-
if (!block || block.type !== 'tool_use') continue;
|
|
3396
|
-
if (block.name !== 'Agent' && block.name !== 'Task') continue;
|
|
3397
|
-
const sub = block.input?.subagent_type || block.input?.description || '?';
|
|
3398
|
-
truncSpawns[sub] = (truncSpawns[sub] || 0) + 1;
|
|
3399
|
-
}
|
|
3400
|
-
}
|
|
3401
|
-
} catch {}
|
|
3402
|
-
nodes.push({ id: sid, type: 'session', label: sid.slice(0,8), turns: 0, totalTools: 0,
|
|
3403
|
-
toolCounts: {}, cost: 0, mtime: stat.mtimeMs, size: stat.size, agentSpawns: truncSpawns, truncated: true });
|
|
3404
|
-
for (const [subType, count] of Object.entries(truncSpawns)) {
|
|
3405
|
-
const nodeId = 'agent::' + subType;
|
|
3406
|
-
if (!agentTypeNodes[subType]) {
|
|
3407
|
-
agentTypeNodes[subType] = true;
|
|
3408
|
-
nodes.push({ id: nodeId, type: 'agenttype', label: subType, totalSpawns: 0 });
|
|
3409
|
-
}
|
|
3410
|
-
const aNode = nodes.find(n => n.id === nodeId);
|
|
3411
|
-
if (aNode) aNode.totalSpawns = (aNode.totalSpawns || 0) + count;
|
|
3412
|
-
edges.push({ source: sid, target: nodeId, weight: count, label: String(count) });
|
|
3369
|
+
const root = path.resolve(dir);
|
|
3370
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage']);
|
|
3371
|
+
const DOC_EXT = new Set(['.md', '.mdx']);
|
|
3372
|
+
const files = [];
|
|
3373
|
+
|
|
3374
|
+
// Dirent.isDirectory() is false for a symlinked directory (it reflects
|
|
3375
|
+
// the entry's own type, not the resolved target), so this never
|
|
3376
|
+
// follows a symlink into a loop or outside root — no extra guard needed.
|
|
3377
|
+
const walk = (abs, rel) => {
|
|
3378
|
+
let entries;
|
|
3379
|
+
try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch { return; }
|
|
3380
|
+
for (const entry of entries) {
|
|
3381
|
+
if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue;
|
|
3382
|
+
const childAbs = path.join(abs, entry.name);
|
|
3383
|
+
const childRel = rel ? rel + '/' + entry.name : entry.name;
|
|
3384
|
+
if (entry.isDirectory()) {
|
|
3385
|
+
walk(childAbs, childRel);
|
|
3386
|
+
} else if (entry.isFile() && DOC_EXT.has(path.extname(entry.name))) {
|
|
3387
|
+
let stat;
|
|
3388
|
+
try { stat = fs.statSync(childAbs); } catch { continue; }
|
|
3389
|
+
files.push({ path: childRel, size: stat.size, mtime: stat.mtimeMs });
|
|
3413
3390
|
}
|
|
3414
|
-
continue;
|
|
3415
3391
|
}
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
let turns = 0, totalCost = 0, _costIncomplete = false;
|
|
3420
|
-
|
|
3421
|
-
try {
|
|
3422
|
-
const raw = fs.readFileSync(fp, 'utf8').replace(/\r\n/g, '\n');
|
|
3423
|
-
const lines = raw.split('\n').filter(Boolean);
|
|
3424
|
-
for (const line of lines) {
|
|
3425
|
-
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
3426
|
-
if (e.type === 'user') {
|
|
3427
|
-
// Only count actual human turns, not tool-result responses
|
|
3428
|
-
const ct = e.message?.content;
|
|
3429
|
-
const isToolResult = Array.isArray(ct) && ct.length > 0 && ct.every(b => b && b.type === 'tool_result');
|
|
3430
|
-
if (!isToolResult) turns++;
|
|
3431
|
-
}
|
|
3432
|
-
if (e.type === 'assistant') {
|
|
3433
|
-
for (const block of (e.message?.content || [])) {
|
|
3434
|
-
if (!block || block.type !== 'tool_use') continue;
|
|
3435
|
-
const cat = TOOL_CAT(block.name);
|
|
3436
|
-
toolCounts[cat] = (toolCounts[cat] || 0) + 1;
|
|
3437
|
-
if (cat === 'agent') {
|
|
3438
|
-
const sub = block.input?.subagent_type || block.input?.description || '?';
|
|
3439
|
-
agentSpawns[sub] = (agentSpawns[sub] || 0) + 1;
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
if (e.message?.usage) {
|
|
3443
|
-
const _m = e.message.model || '';
|
|
3444
|
-
totalCost += _sjCalcCost(_m, e.message.usage);
|
|
3445
|
-
if (!_sjHasPricing(_m)) _costIncomplete = true;
|
|
3446
|
-
}
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
} catch {}
|
|
3450
|
-
|
|
3451
|
-
const totalTools = Object.values(toolCounts).reduce((a,b)=>a+b,0);
|
|
3452
|
-
nodes.push({
|
|
3453
|
-
id: sid, type: 'session', label: sid.slice(0,8),
|
|
3454
|
-
turns, totalTools, toolCounts,
|
|
3455
|
-
cost: totalCost, costIncomplete: _costIncomplete, mtime: stat.mtimeMs, size: stat.size,
|
|
3456
|
-
agentSpawns
|
|
3457
|
-
});
|
|
3458
|
-
|
|
3459
|
-
// Create/link agent type nodes
|
|
3460
|
-
for (const [subType, count] of Object.entries(agentSpawns)) {
|
|
3461
|
-
const nodeId = 'agent::' + subType;
|
|
3462
|
-
if (!agentTypeNodes[subType]) {
|
|
3463
|
-
agentTypeNodes[subType] = true;
|
|
3464
|
-
nodes.push({ id: nodeId, type: 'agenttype', label: subType, totalSpawns: 0 });
|
|
3465
|
-
}
|
|
3466
|
-
const aNode = nodes.find(n => n.id === nodeId);
|
|
3467
|
-
if (aNode) aNode.totalSpawns = (aNode.totalSpawns || 0) + count;
|
|
3468
|
-
edges.push({ source: sid, target: nodeId, weight: count, label: String(count) });
|
|
3469
|
-
}
|
|
3470
|
-
}
|
|
3392
|
+
};
|
|
3393
|
+
walk(root, '');
|
|
3394
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
3471
3395
|
|
|
3472
3396
|
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
|
|
3473
|
-
res.end(JSON.stringify({
|
|
3474
|
-
} catch (err) {
|
|
3475
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3476
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
3477
|
-
}
|
|
3478
|
-
return;
|
|
3479
|
-
}
|
|
3480
|
-
|
|
3481
|
-
// ------------------------------------------------- GET /api/swarm-history
|
|
3482
|
-
if (req.method === 'GET' && url === '/api/swarm-history') {
|
|
3483
|
-
try {
|
|
3484
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
3485
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
3486
|
-
const entries = collectSwarmHistory(path.resolve(dir));
|
|
3487
|
-
res.writeHead(200, {
|
|
3488
|
-
'Content-Type': 'application/json',
|
|
3489
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3490
|
-
'Cache-Control': 'no-cache',
|
|
3491
|
-
});
|
|
3492
|
-
res.end(JSON.stringify({ entries }));
|
|
3397
|
+
res.end(JSON.stringify({ files, total: files.length }));
|
|
3493
3398
|
} catch (err) {
|
|
3494
3399
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3495
3400
|
res.end(JSON.stringify({ error: err.message }));
|
|
@@ -3497,50 +3402,55 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true,
|
|
|
3497
3402
|
return;
|
|
3498
3403
|
}
|
|
3499
3404
|
|
|
3500
|
-
//
|
|
3501
|
-
|
|
3405
|
+
// -------------------------------------------------------- GET /api/doc-read
|
|
3406
|
+
// Returns the raw content of one file discovered by /api/docs. Content
|
|
3407
|
+
// exposure is a bigger deal than the metadata /api/docs lists, so
|
|
3408
|
+
// containment is checked against known project roots (same allowlist
|
|
3409
|
+
// /api/global-doc/read uses) rather than trusting the client-supplied
|
|
3410
|
+
// `dir` alone — otherwise `?dir=/` would make any .md file on disk
|
|
3411
|
+
// "contained".
|
|
3412
|
+
if (req.method === 'GET' && url.startsWith('/api/doc-read')) {
|
|
3502
3413
|
try {
|
|
3503
3414
|
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
3504
3415
|
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
const events = collectSwarmEvents(path.resolve(dir), { swarmId, agentId, last });
|
|
3514
|
-
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
|
|
3515
|
-
res.end(JSON.stringify({ events, count: events.length }));
|
|
3516
|
-
} catch (err) {
|
|
3517
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3518
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
3519
|
-
}
|
|
3520
|
-
return;
|
|
3521
|
-
}
|
|
3416
|
+
const rel = qs.get('path');
|
|
3417
|
+
if (!rel || typeof rel !== 'string') {
|
|
3418
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
3419
|
+
res.end(JSON.stringify({ error: 'path query param required' }));
|
|
3420
|
+
return;
|
|
3421
|
+
}
|
|
3422
|
+
let resolved = path.resolve(dir, rel);
|
|
3423
|
+
try { resolved = fs.realpathSync(resolved); } catch {}
|
|
3522
3424
|
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
return;
|
|
3535
|
-
}
|
|
3425
|
+
const allowedRoots = [path.resolve(projectDir || process.cwd())];
|
|
3426
|
+
const projectsBase = path.join(os.homedir(), '.claude', 'projects');
|
|
3427
|
+
try {
|
|
3428
|
+
for (const slug of fs.readdirSync(projectsBase)) {
|
|
3429
|
+
const resolvedProj = resolveSlugToPath(slug, path.join(projectsBase, slug));
|
|
3430
|
+
if (resolvedProj) allowedRoots.push(resolvedProj);
|
|
3431
|
+
}
|
|
3432
|
+
} catch { /* projects tree absent — fine */ }
|
|
3433
|
+
const globalBrain = process.env.MONOMIND_GLOBAL_BRAIN_DIR
|
|
3434
|
+
|| path.join(os.homedir(), '.monomind', 'global-brain');
|
|
3435
|
+
if (fs.existsSync(globalBrain)) allowedRoots.push(globalBrain);
|
|
3536
3436
|
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3437
|
+
const isAllowed = allowedRoots.some(root => {
|
|
3438
|
+
const relCheck = path.relative(root, resolved);
|
|
3439
|
+
return relCheck && !relCheck.startsWith('..') && !path.isAbsolute(relCheck);
|
|
3440
|
+
});
|
|
3441
|
+
if (!isAllowed || !fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
|
|
3442
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
3443
|
+
res.end(JSON.stringify({ error: 'path is outside the allowed project roots' }));
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
if (!/\.(md|mdx)$/i.test(resolved)) {
|
|
3447
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
3448
|
+
res.end(JSON.stringify({ error: 'only markdown (.md/.mdx) files are readable' }));
|
|
3449
|
+
return;
|
|
3450
|
+
}
|
|
3451
|
+
const body = fs.readFileSync(resolved, 'utf8');
|
|
3452
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
|
|
3453
|
+
res.end(JSON.stringify({ body }));
|
|
3544
3454
|
} catch (err) {
|
|
3545
3455
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3546
3456
|
res.end(JSON.stringify({ error: err.message }));
|