mrvn-cli 0.3.2 → 0.3.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/dist/index.d.ts +7 -0
- package/dist/index.js +7154 -6981
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +1176 -9
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +4683 -4508
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -6474,13 +6474,13 @@ var error16 = () => {
|
|
|
6474
6474
|
// no unit
|
|
6475
6475
|
};
|
|
6476
6476
|
const typeEntry = (t) => t ? TypeNames[t] : void 0;
|
|
6477
|
-
const
|
|
6477
|
+
const typeLabel2 = (t) => {
|
|
6478
6478
|
const e = typeEntry(t);
|
|
6479
6479
|
if (e)
|
|
6480
6480
|
return e.label;
|
|
6481
6481
|
return t ?? TypeNames.unknown.label;
|
|
6482
6482
|
};
|
|
6483
|
-
const withDefinite = (t) => `\u05D4${
|
|
6483
|
+
const withDefinite = (t) => `\u05D4${typeLabel2(t)}`;
|
|
6484
6484
|
const verbFor = (t) => {
|
|
6485
6485
|
const e = typeEntry(t);
|
|
6486
6486
|
const gender = e?.gender ?? "m";
|
|
@@ -6530,7 +6530,7 @@ var error16 = () => {
|
|
|
6530
6530
|
switch (issue2.code) {
|
|
6531
6531
|
case "invalid_type": {
|
|
6532
6532
|
const expectedKey = issue2.expected;
|
|
6533
|
-
const expected = TypeDictionary[expectedKey ?? ""] ??
|
|
6533
|
+
const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel2(expectedKey);
|
|
6534
6534
|
const receivedType = parsedType(issue2.input);
|
|
6535
6535
|
const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
|
|
6536
6536
|
if (/^[A-Z]/.test(issue2.expected)) {
|
|
@@ -17934,7 +17934,7 @@ ${fragment}`);
|
|
|
17934
17934
|
}
|
|
17935
17935
|
|
|
17936
17936
|
// src/skills/action-tools.ts
|
|
17937
|
-
import { tool as
|
|
17937
|
+
import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
|
|
17938
17938
|
|
|
17939
17939
|
// src/skills/action-runner.ts
|
|
17940
17940
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -17943,6 +17943,1172 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
17943
17943
|
import {
|
|
17944
17944
|
createSdkMcpServer
|
|
17945
17945
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
17946
|
+
|
|
17947
|
+
// src/agent/tools/web.ts
|
|
17948
|
+
import * as http2 from "http";
|
|
17949
|
+
import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
|
|
17950
|
+
|
|
17951
|
+
// src/web/data.ts
|
|
17952
|
+
function getOverviewData(store) {
|
|
17953
|
+
const types = [];
|
|
17954
|
+
const counts = store.counts();
|
|
17955
|
+
for (const type of store.registeredTypes) {
|
|
17956
|
+
const total = counts[type] ?? 0;
|
|
17957
|
+
const open = store.list({ type, status: "open" }).length;
|
|
17958
|
+
types.push({ type, total, open });
|
|
17959
|
+
}
|
|
17960
|
+
const allDocs = store.list();
|
|
17961
|
+
const sorted = allDocs.sort(
|
|
17962
|
+
(a, b) => (b.frontmatter.updated ?? b.frontmatter.created).localeCompare(
|
|
17963
|
+
a.frontmatter.updated ?? a.frontmatter.created
|
|
17964
|
+
)
|
|
17965
|
+
);
|
|
17966
|
+
return { types, recent: sorted.slice(0, 20) };
|
|
17967
|
+
}
|
|
17968
|
+
function getDocumentListData(store, type, filterStatus, filterOwner) {
|
|
17969
|
+
if (!store.registeredTypes.includes(type)) return void 0;
|
|
17970
|
+
const allOfType = store.list({ type });
|
|
17971
|
+
const statuses = [...new Set(allOfType.map((d) => d.frontmatter.status))].sort();
|
|
17972
|
+
const owners = [
|
|
17973
|
+
...new Set(allOfType.map((d) => d.frontmatter.owner).filter(Boolean))
|
|
17974
|
+
].sort();
|
|
17975
|
+
let docs = allOfType;
|
|
17976
|
+
if (filterStatus) {
|
|
17977
|
+
docs = docs.filter((d) => d.frontmatter.status === filterStatus);
|
|
17978
|
+
}
|
|
17979
|
+
if (filterOwner) {
|
|
17980
|
+
docs = docs.filter((d) => d.frontmatter.owner === filterOwner);
|
|
17981
|
+
}
|
|
17982
|
+
docs.sort((a, b) => a.frontmatter.id.localeCompare(b.frontmatter.id));
|
|
17983
|
+
return { type, docs, statuses, owners, filterStatus, filterOwner };
|
|
17984
|
+
}
|
|
17985
|
+
function getDocumentDetail(store, type, id) {
|
|
17986
|
+
if (!store.registeredTypes.includes(type)) return void 0;
|
|
17987
|
+
return store.get(id);
|
|
17988
|
+
}
|
|
17989
|
+
function getGarData(store, projectName) {
|
|
17990
|
+
const metrics = collectGarMetrics(store);
|
|
17991
|
+
return evaluateGar(projectName, metrics);
|
|
17992
|
+
}
|
|
17993
|
+
function getBoardData(store, type) {
|
|
17994
|
+
const docs = type ? store.list({ type }) : store.list();
|
|
17995
|
+
const types = store.registeredTypes;
|
|
17996
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
17997
|
+
for (const doc of docs) {
|
|
17998
|
+
const status = doc.frontmatter.status;
|
|
17999
|
+
if (!byStatus.has(status)) byStatus.set(status, []);
|
|
18000
|
+
byStatus.get(status).push(doc);
|
|
18001
|
+
}
|
|
18002
|
+
const statusOrder = ["open", "draft", "in-progress", "blocked"];
|
|
18003
|
+
const allStatuses = [...byStatus.keys()];
|
|
18004
|
+
const ordered = [];
|
|
18005
|
+
for (const s of statusOrder) {
|
|
18006
|
+
if (allStatuses.includes(s)) ordered.push(s);
|
|
18007
|
+
}
|
|
18008
|
+
for (const s of allStatuses.sort()) {
|
|
18009
|
+
if (!ordered.includes(s) && s !== "done" && s !== "closed" && s !== "resolved") {
|
|
18010
|
+
ordered.push(s);
|
|
18011
|
+
}
|
|
18012
|
+
}
|
|
18013
|
+
for (const s of ["done", "closed", "resolved"]) {
|
|
18014
|
+
if (allStatuses.includes(s)) ordered.push(s);
|
|
18015
|
+
}
|
|
18016
|
+
const columns = ordered.map((status) => ({
|
|
18017
|
+
status,
|
|
18018
|
+
docs: byStatus.get(status) ?? []
|
|
18019
|
+
}));
|
|
18020
|
+
return { columns, type, types };
|
|
18021
|
+
}
|
|
18022
|
+
|
|
18023
|
+
// src/web/templates/layout.ts
|
|
18024
|
+
function escapeHtml(str) {
|
|
18025
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
18026
|
+
}
|
|
18027
|
+
function statusBadge(status) {
|
|
18028
|
+
const cls = {
|
|
18029
|
+
open: "badge-open",
|
|
18030
|
+
done: "badge-done",
|
|
18031
|
+
closed: "badge-done",
|
|
18032
|
+
resolved: "badge-resolved",
|
|
18033
|
+
"in-progress": "badge-in-progress",
|
|
18034
|
+
"in progress": "badge-in-progress",
|
|
18035
|
+
draft: "badge-draft",
|
|
18036
|
+
blocked: "badge-blocked"
|
|
18037
|
+
}[status.toLowerCase()] ?? "badge-default";
|
|
18038
|
+
return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
|
|
18039
|
+
}
|
|
18040
|
+
function formatDate(iso) {
|
|
18041
|
+
if (!iso) return "";
|
|
18042
|
+
return iso.slice(0, 10);
|
|
18043
|
+
}
|
|
18044
|
+
function typeLabel(type) {
|
|
18045
|
+
return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
18046
|
+
}
|
|
18047
|
+
function renderMarkdown(md) {
|
|
18048
|
+
const lines = md.split("\n");
|
|
18049
|
+
const out = [];
|
|
18050
|
+
let inList = false;
|
|
18051
|
+
let listTag = "ul";
|
|
18052
|
+
for (const raw of lines) {
|
|
18053
|
+
const line = raw;
|
|
18054
|
+
if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
|
|
18055
|
+
out.push(`</${listTag}>`);
|
|
18056
|
+
inList = false;
|
|
18057
|
+
}
|
|
18058
|
+
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
|
|
18059
|
+
if (headingMatch) {
|
|
18060
|
+
const level = headingMatch[1].length;
|
|
18061
|
+
out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
|
|
18062
|
+
continue;
|
|
18063
|
+
}
|
|
18064
|
+
const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
|
|
18065
|
+
if (ulMatch) {
|
|
18066
|
+
if (!inList || listTag !== "ul") {
|
|
18067
|
+
if (inList) out.push(`</${listTag}>`);
|
|
18068
|
+
out.push("<ul>");
|
|
18069
|
+
inList = true;
|
|
18070
|
+
listTag = "ul";
|
|
18071
|
+
}
|
|
18072
|
+
out.push(`<li>${inline(ulMatch[1])}</li>`);
|
|
18073
|
+
continue;
|
|
18074
|
+
}
|
|
18075
|
+
const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
18076
|
+
if (olMatch) {
|
|
18077
|
+
if (!inList || listTag !== "ol") {
|
|
18078
|
+
if (inList) out.push(`</${listTag}>`);
|
|
18079
|
+
out.push("<ol>");
|
|
18080
|
+
inList = true;
|
|
18081
|
+
listTag = "ol";
|
|
18082
|
+
}
|
|
18083
|
+
out.push(`<li>${inline(olMatch[1])}</li>`);
|
|
18084
|
+
continue;
|
|
18085
|
+
}
|
|
18086
|
+
if (line.trim() === "") {
|
|
18087
|
+
if (inList) {
|
|
18088
|
+
out.push(`</${listTag}>`);
|
|
18089
|
+
inList = false;
|
|
18090
|
+
}
|
|
18091
|
+
continue;
|
|
18092
|
+
}
|
|
18093
|
+
out.push(`<p>${inline(line)}</p>`);
|
|
18094
|
+
}
|
|
18095
|
+
if (inList) out.push(`</${listTag}>`);
|
|
18096
|
+
return out.join("\n");
|
|
18097
|
+
}
|
|
18098
|
+
function inline(text) {
|
|
18099
|
+
let s = escapeHtml(text);
|
|
18100
|
+
s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
18101
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
18102
|
+
s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
18103
|
+
s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
18104
|
+
s = s.replace(/_([^_]+)_/g, "<em>$1</em>");
|
|
18105
|
+
return s;
|
|
18106
|
+
}
|
|
18107
|
+
function layout(opts, body) {
|
|
18108
|
+
const topItems = [
|
|
18109
|
+
{ href: "/", label: "Overview" },
|
|
18110
|
+
{ href: "/board", label: "Board" },
|
|
18111
|
+
{ href: "/gar", label: "GAR Report" }
|
|
18112
|
+
];
|
|
18113
|
+
const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
|
|
18114
|
+
const groupsHtml = opts.navGroups.map((group) => {
|
|
18115
|
+
const links = group.types.map((type) => {
|
|
18116
|
+
const href = `/docs/${type}`;
|
|
18117
|
+
return `<a href="${href}" class="${isActive(href)}">${typeLabel(type)}s</a>`;
|
|
18118
|
+
}).join("\n ");
|
|
18119
|
+
return `
|
|
18120
|
+
<div class="nav-group">
|
|
18121
|
+
<div class="nav-group-label">${escapeHtml(group.label)}</div>
|
|
18122
|
+
${links}
|
|
18123
|
+
</div>`;
|
|
18124
|
+
}).join("\n");
|
|
18125
|
+
return `<!DOCTYPE html>
|
|
18126
|
+
<html lang="en">
|
|
18127
|
+
<head>
|
|
18128
|
+
<meta charset="UTF-8">
|
|
18129
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18130
|
+
<title>${escapeHtml(opts.title)} \u2014 Marvin</title>
|
|
18131
|
+
<link rel="stylesheet" href="/styles.css">
|
|
18132
|
+
</head>
|
|
18133
|
+
<body>
|
|
18134
|
+
<div class="shell">
|
|
18135
|
+
<aside class="sidebar">
|
|
18136
|
+
<div class="sidebar-brand">
|
|
18137
|
+
<h1>Marvin</h1>
|
|
18138
|
+
<div class="project-name">${escapeHtml(opts.projectName)}</div>
|
|
18139
|
+
</div>
|
|
18140
|
+
<nav>
|
|
18141
|
+
${topItems.map((n) => `<a href="${n.href}" class="${isActive(n.href)}">${n.label}</a>`).join("\n ")}
|
|
18142
|
+
${groupsHtml}
|
|
18143
|
+
</nav>
|
|
18144
|
+
</aside>
|
|
18145
|
+
<main class="main">
|
|
18146
|
+
${body}
|
|
18147
|
+
</main>
|
|
18148
|
+
</div>
|
|
18149
|
+
</body>
|
|
18150
|
+
</html>`;
|
|
18151
|
+
}
|
|
18152
|
+
|
|
18153
|
+
// src/web/templates/styles.ts
|
|
18154
|
+
function renderStyles() {
|
|
18155
|
+
return `
|
|
18156
|
+
:root {
|
|
18157
|
+
--bg: #0f1117;
|
|
18158
|
+
--bg-card: #1a1d27;
|
|
18159
|
+
--bg-hover: #222632;
|
|
18160
|
+
--border: #2a2e3a;
|
|
18161
|
+
--text: #e1e4ea;
|
|
18162
|
+
--text-dim: #8b8fa4;
|
|
18163
|
+
--accent: #6c8cff;
|
|
18164
|
+
--accent-dim: #4a6ad4;
|
|
18165
|
+
--green: #34d399;
|
|
18166
|
+
--amber: #fbbf24;
|
|
18167
|
+
--red: #f87171;
|
|
18168
|
+
--radius: 8px;
|
|
18169
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
18170
|
+
--mono: "SF Mono", "Fira Code", monospace;
|
|
18171
|
+
}
|
|
18172
|
+
|
|
18173
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
18174
|
+
|
|
18175
|
+
body {
|
|
18176
|
+
font-family: var(--font);
|
|
18177
|
+
background: var(--bg);
|
|
18178
|
+
color: var(--text);
|
|
18179
|
+
line-height: 1.6;
|
|
18180
|
+
min-height: 100vh;
|
|
18181
|
+
}
|
|
18182
|
+
|
|
18183
|
+
a { color: var(--accent); text-decoration: none; }
|
|
18184
|
+
a:hover { text-decoration: underline; }
|
|
18185
|
+
|
|
18186
|
+
/* Layout */
|
|
18187
|
+
.shell {
|
|
18188
|
+
display: flex;
|
|
18189
|
+
min-height: 100vh;
|
|
18190
|
+
}
|
|
18191
|
+
|
|
18192
|
+
.sidebar {
|
|
18193
|
+
width: 220px;
|
|
18194
|
+
background: var(--bg-card);
|
|
18195
|
+
border-right: 1px solid var(--border);
|
|
18196
|
+
padding: 1.5rem 0;
|
|
18197
|
+
position: fixed;
|
|
18198
|
+
top: 0;
|
|
18199
|
+
left: 0;
|
|
18200
|
+
bottom: 0;
|
|
18201
|
+
overflow-y: auto;
|
|
18202
|
+
}
|
|
18203
|
+
|
|
18204
|
+
.sidebar-brand {
|
|
18205
|
+
padding: 0 1.25rem 1.25rem;
|
|
18206
|
+
border-bottom: 1px solid var(--border);
|
|
18207
|
+
margin-bottom: 1rem;
|
|
18208
|
+
}
|
|
18209
|
+
|
|
18210
|
+
.sidebar-brand h1 {
|
|
18211
|
+
font-size: 1.1rem;
|
|
18212
|
+
font-weight: 700;
|
|
18213
|
+
color: var(--accent);
|
|
18214
|
+
letter-spacing: -0.02em;
|
|
18215
|
+
}
|
|
18216
|
+
|
|
18217
|
+
.sidebar-brand .project-name {
|
|
18218
|
+
font-size: 0.75rem;
|
|
18219
|
+
color: var(--text-dim);
|
|
18220
|
+
margin-top: 0.25rem;
|
|
18221
|
+
}
|
|
18222
|
+
|
|
18223
|
+
.sidebar nav a {
|
|
18224
|
+
display: block;
|
|
18225
|
+
padding: 0.5rem 1.25rem;
|
|
18226
|
+
color: var(--text-dim);
|
|
18227
|
+
font-size: 0.875rem;
|
|
18228
|
+
transition: background 0.15s, color 0.15s;
|
|
18229
|
+
}
|
|
18230
|
+
|
|
18231
|
+
.sidebar nav a:hover {
|
|
18232
|
+
background: var(--bg-hover);
|
|
18233
|
+
color: var(--text);
|
|
18234
|
+
text-decoration: none;
|
|
18235
|
+
}
|
|
18236
|
+
|
|
18237
|
+
.sidebar nav a.active {
|
|
18238
|
+
color: var(--accent);
|
|
18239
|
+
background: rgba(108, 140, 255, 0.08);
|
|
18240
|
+
border-right: 2px solid var(--accent);
|
|
18241
|
+
}
|
|
18242
|
+
|
|
18243
|
+
.nav-group {
|
|
18244
|
+
margin-top: 0.75rem;
|
|
18245
|
+
padding-top: 0.75rem;
|
|
18246
|
+
border-top: 1px solid var(--border);
|
|
18247
|
+
}
|
|
18248
|
+
|
|
18249
|
+
.nav-group-label {
|
|
18250
|
+
padding: 0.25rem 1.25rem 0.25rem;
|
|
18251
|
+
font-size: 0.65rem;
|
|
18252
|
+
text-transform: uppercase;
|
|
18253
|
+
letter-spacing: 0.08em;
|
|
18254
|
+
color: var(--text-dim);
|
|
18255
|
+
font-weight: 600;
|
|
18256
|
+
}
|
|
18257
|
+
|
|
18258
|
+
.main {
|
|
18259
|
+
margin-left: 220px;
|
|
18260
|
+
flex: 1;
|
|
18261
|
+
padding: 2rem 2.5rem;
|
|
18262
|
+
max-width: 1200px;
|
|
18263
|
+
}
|
|
18264
|
+
|
|
18265
|
+
/* Page header */
|
|
18266
|
+
.page-header {
|
|
18267
|
+
margin-bottom: 2rem;
|
|
18268
|
+
}
|
|
18269
|
+
|
|
18270
|
+
.page-header h2 {
|
|
18271
|
+
font-size: 1.5rem;
|
|
18272
|
+
font-weight: 600;
|
|
18273
|
+
}
|
|
18274
|
+
|
|
18275
|
+
.page-header .subtitle {
|
|
18276
|
+
color: var(--text-dim);
|
|
18277
|
+
font-size: 0.875rem;
|
|
18278
|
+
margin-top: 0.25rem;
|
|
18279
|
+
}
|
|
18280
|
+
|
|
18281
|
+
/* Breadcrumb */
|
|
18282
|
+
.breadcrumb {
|
|
18283
|
+
font-size: 0.8rem;
|
|
18284
|
+
color: var(--text-dim);
|
|
18285
|
+
margin-bottom: 1rem;
|
|
18286
|
+
}
|
|
18287
|
+
|
|
18288
|
+
.breadcrumb a { color: var(--text-dim); }
|
|
18289
|
+
.breadcrumb a:hover { color: var(--accent); }
|
|
18290
|
+
.breadcrumb .sep { margin: 0 0.4rem; }
|
|
18291
|
+
|
|
18292
|
+
/* Cards grid */
|
|
18293
|
+
.cards {
|
|
18294
|
+
display: grid;
|
|
18295
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
18296
|
+
gap: 1rem;
|
|
18297
|
+
margin-bottom: 2rem;
|
|
18298
|
+
}
|
|
18299
|
+
|
|
18300
|
+
.card {
|
|
18301
|
+
background: var(--bg-card);
|
|
18302
|
+
border: 1px solid var(--border);
|
|
18303
|
+
border-radius: var(--radius);
|
|
18304
|
+
padding: 1.25rem;
|
|
18305
|
+
transition: border-color 0.15s;
|
|
18306
|
+
}
|
|
18307
|
+
|
|
18308
|
+
.card:hover {
|
|
18309
|
+
border-color: var(--accent-dim);
|
|
18310
|
+
}
|
|
18311
|
+
|
|
18312
|
+
.card a { color: inherit; text-decoration: none; display: block; }
|
|
18313
|
+
|
|
18314
|
+
.card .card-label {
|
|
18315
|
+
font-size: 0.75rem;
|
|
18316
|
+
text-transform: uppercase;
|
|
18317
|
+
letter-spacing: 0.05em;
|
|
18318
|
+
color: var(--text-dim);
|
|
18319
|
+
margin-bottom: 0.5rem;
|
|
18320
|
+
}
|
|
18321
|
+
|
|
18322
|
+
.card .card-value {
|
|
18323
|
+
font-size: 1.75rem;
|
|
18324
|
+
font-weight: 700;
|
|
18325
|
+
}
|
|
18326
|
+
|
|
18327
|
+
.card .card-sub {
|
|
18328
|
+
font-size: 0.8rem;
|
|
18329
|
+
color: var(--text-dim);
|
|
18330
|
+
margin-top: 0.25rem;
|
|
18331
|
+
}
|
|
18332
|
+
|
|
18333
|
+
/* Status badge */
|
|
18334
|
+
.badge {
|
|
18335
|
+
display: inline-block;
|
|
18336
|
+
padding: 0.15rem 0.6rem;
|
|
18337
|
+
border-radius: 999px;
|
|
18338
|
+
font-size: 0.7rem;
|
|
18339
|
+
font-weight: 600;
|
|
18340
|
+
text-transform: uppercase;
|
|
18341
|
+
letter-spacing: 0.03em;
|
|
18342
|
+
}
|
|
18343
|
+
|
|
18344
|
+
.badge-open { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
|
|
18345
|
+
.badge-done { background: rgba(52, 211, 153, 0.15); color: var(--green); }
|
|
18346
|
+
.badge-in-progress { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
|
|
18347
|
+
.badge-draft { background: rgba(139, 143, 164, 0.15); color: var(--text-dim); }
|
|
18348
|
+
.badge-closed, .badge-resolved { background: rgba(52, 211, 153, 0.15); color: var(--green); }
|
|
18349
|
+
.badge-blocked { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
18350
|
+
.badge-default { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
|
|
18351
|
+
|
|
18352
|
+
/* Table */
|
|
18353
|
+
.table-wrap {
|
|
18354
|
+
overflow-x: auto;
|
|
18355
|
+
}
|
|
18356
|
+
|
|
18357
|
+
table {
|
|
18358
|
+
width: 100%;
|
|
18359
|
+
border-collapse: collapse;
|
|
18360
|
+
}
|
|
18361
|
+
|
|
18362
|
+
th {
|
|
18363
|
+
text-align: left;
|
|
18364
|
+
padding: 0.6rem 0.75rem;
|
|
18365
|
+
font-size: 0.7rem;
|
|
18366
|
+
text-transform: uppercase;
|
|
18367
|
+
letter-spacing: 0.05em;
|
|
18368
|
+
color: var(--text-dim);
|
|
18369
|
+
border-bottom: 1px solid var(--border);
|
|
18370
|
+
}
|
|
18371
|
+
|
|
18372
|
+
td {
|
|
18373
|
+
padding: 0.6rem 0.75rem;
|
|
18374
|
+
font-size: 0.875rem;
|
|
18375
|
+
border-bottom: 1px solid var(--border);
|
|
18376
|
+
}
|
|
18377
|
+
|
|
18378
|
+
tr:hover td {
|
|
18379
|
+
background: var(--bg-hover);
|
|
18380
|
+
}
|
|
18381
|
+
|
|
18382
|
+
/* GAR */
|
|
18383
|
+
.gar-overall {
|
|
18384
|
+
text-align: center;
|
|
18385
|
+
padding: 2rem;
|
|
18386
|
+
margin-bottom: 2rem;
|
|
18387
|
+
border-radius: var(--radius);
|
|
18388
|
+
border: 1px solid var(--border);
|
|
18389
|
+
background: var(--bg-card);
|
|
18390
|
+
}
|
|
18391
|
+
|
|
18392
|
+
.gar-overall .dot {
|
|
18393
|
+
width: 60px;
|
|
18394
|
+
height: 60px;
|
|
18395
|
+
border-radius: 50%;
|
|
18396
|
+
display: inline-block;
|
|
18397
|
+
margin-bottom: 0.75rem;
|
|
18398
|
+
}
|
|
18399
|
+
|
|
18400
|
+
.gar-overall .label {
|
|
18401
|
+
font-size: 1.1rem;
|
|
18402
|
+
font-weight: 600;
|
|
18403
|
+
text-transform: uppercase;
|
|
18404
|
+
}
|
|
18405
|
+
|
|
18406
|
+
.gar-areas {
|
|
18407
|
+
display: grid;
|
|
18408
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
18409
|
+
gap: 1rem;
|
|
18410
|
+
}
|
|
18411
|
+
|
|
18412
|
+
.gar-area {
|
|
18413
|
+
background: var(--bg-card);
|
|
18414
|
+
border: 1px solid var(--border);
|
|
18415
|
+
border-radius: var(--radius);
|
|
18416
|
+
padding: 1.25rem;
|
|
18417
|
+
}
|
|
18418
|
+
|
|
18419
|
+
.gar-area .area-header {
|
|
18420
|
+
display: flex;
|
|
18421
|
+
align-items: center;
|
|
18422
|
+
gap: 0.6rem;
|
|
18423
|
+
margin-bottom: 0.75rem;
|
|
18424
|
+
}
|
|
18425
|
+
|
|
18426
|
+
.gar-area .area-dot {
|
|
18427
|
+
width: 14px;
|
|
18428
|
+
height: 14px;
|
|
18429
|
+
border-radius: 50%;
|
|
18430
|
+
flex-shrink: 0;
|
|
18431
|
+
}
|
|
18432
|
+
|
|
18433
|
+
.gar-area .area-name {
|
|
18434
|
+
font-weight: 600;
|
|
18435
|
+
font-size: 1rem;
|
|
18436
|
+
}
|
|
18437
|
+
|
|
18438
|
+
.gar-area .area-summary {
|
|
18439
|
+
font-size: 0.85rem;
|
|
18440
|
+
color: var(--text-dim);
|
|
18441
|
+
margin-bottom: 0.75rem;
|
|
18442
|
+
}
|
|
18443
|
+
|
|
18444
|
+
.gar-area ul {
|
|
18445
|
+
list-style: none;
|
|
18446
|
+
font-size: 0.8rem;
|
|
18447
|
+
}
|
|
18448
|
+
|
|
18449
|
+
.gar-area li {
|
|
18450
|
+
padding: 0.2rem 0;
|
|
18451
|
+
color: var(--text-dim);
|
|
18452
|
+
}
|
|
18453
|
+
|
|
18454
|
+
.gar-area li .ref-id {
|
|
18455
|
+
color: var(--accent);
|
|
18456
|
+
font-family: var(--mono);
|
|
18457
|
+
margin-right: 0.4rem;
|
|
18458
|
+
}
|
|
18459
|
+
|
|
18460
|
+
.dot-green { background: var(--green); }
|
|
18461
|
+
.dot-amber { background: var(--amber); }
|
|
18462
|
+
.dot-red { background: var(--red); }
|
|
18463
|
+
|
|
18464
|
+
/* Board / Kanban */
|
|
18465
|
+
.board {
|
|
18466
|
+
display: flex;
|
|
18467
|
+
gap: 1rem;
|
|
18468
|
+
overflow-x: auto;
|
|
18469
|
+
padding-bottom: 1rem;
|
|
18470
|
+
}
|
|
18471
|
+
|
|
18472
|
+
.board-column {
|
|
18473
|
+
min-width: 240px;
|
|
18474
|
+
max-width: 300px;
|
|
18475
|
+
flex: 1;
|
|
18476
|
+
}
|
|
18477
|
+
|
|
18478
|
+
.board-column-header {
|
|
18479
|
+
font-size: 0.75rem;
|
|
18480
|
+
text-transform: uppercase;
|
|
18481
|
+
letter-spacing: 0.05em;
|
|
18482
|
+
color: var(--text-dim);
|
|
18483
|
+
padding: 0.5rem 0.75rem;
|
|
18484
|
+
border-bottom: 2px solid var(--border);
|
|
18485
|
+
margin-bottom: 0.5rem;
|
|
18486
|
+
display: flex;
|
|
18487
|
+
justify-content: space-between;
|
|
18488
|
+
}
|
|
18489
|
+
|
|
18490
|
+
.board-column-header .count {
|
|
18491
|
+
background: var(--bg-hover);
|
|
18492
|
+
padding: 0 0.5rem;
|
|
18493
|
+
border-radius: 999px;
|
|
18494
|
+
font-size: 0.7rem;
|
|
18495
|
+
}
|
|
18496
|
+
|
|
18497
|
+
.board-card {
|
|
18498
|
+
background: var(--bg-card);
|
|
18499
|
+
border: 1px solid var(--border);
|
|
18500
|
+
border-radius: var(--radius);
|
|
18501
|
+
padding: 0.75rem;
|
|
18502
|
+
margin-bottom: 0.5rem;
|
|
18503
|
+
transition: border-color 0.15s;
|
|
18504
|
+
}
|
|
18505
|
+
|
|
18506
|
+
.board-card:hover {
|
|
18507
|
+
border-color: var(--accent-dim);
|
|
18508
|
+
}
|
|
18509
|
+
|
|
18510
|
+
.board-card .bc-id {
|
|
18511
|
+
font-family: var(--mono);
|
|
18512
|
+
font-size: 0.7rem;
|
|
18513
|
+
color: var(--accent);
|
|
18514
|
+
}
|
|
18515
|
+
|
|
18516
|
+
.board-card .bc-title {
|
|
18517
|
+
font-size: 0.85rem;
|
|
18518
|
+
margin: 0.25rem 0;
|
|
18519
|
+
}
|
|
18520
|
+
|
|
18521
|
+
.board-card .bc-owner {
|
|
18522
|
+
font-size: 0.7rem;
|
|
18523
|
+
color: var(--text-dim);
|
|
18524
|
+
}
|
|
18525
|
+
|
|
18526
|
+
/* Detail page */
|
|
18527
|
+
.detail-meta {
|
|
18528
|
+
background: var(--bg-card);
|
|
18529
|
+
border: 1px solid var(--border);
|
|
18530
|
+
border-radius: var(--radius);
|
|
18531
|
+
padding: 1.25rem;
|
|
18532
|
+
margin-bottom: 1.5rem;
|
|
18533
|
+
}
|
|
18534
|
+
|
|
18535
|
+
.detail-meta dl {
|
|
18536
|
+
display: grid;
|
|
18537
|
+
grid-template-columns: 120px 1fr;
|
|
18538
|
+
gap: 0.4rem 1rem;
|
|
18539
|
+
}
|
|
18540
|
+
|
|
18541
|
+
.detail-meta dt {
|
|
18542
|
+
font-size: 0.75rem;
|
|
18543
|
+
text-transform: uppercase;
|
|
18544
|
+
letter-spacing: 0.05em;
|
|
18545
|
+
color: var(--text-dim);
|
|
18546
|
+
}
|
|
18547
|
+
|
|
18548
|
+
.detail-meta dd {
|
|
18549
|
+
font-size: 0.875rem;
|
|
18550
|
+
}
|
|
18551
|
+
|
|
18552
|
+
.detail-content {
|
|
18553
|
+
background: var(--bg-card);
|
|
18554
|
+
border: 1px solid var(--border);
|
|
18555
|
+
border-radius: var(--radius);
|
|
18556
|
+
padding: 1.5rem;
|
|
18557
|
+
line-height: 1.7;
|
|
18558
|
+
}
|
|
18559
|
+
|
|
18560
|
+
.detail-content h1, .detail-content h2, .detail-content h3 {
|
|
18561
|
+
margin: 1.25rem 0 0.5rem;
|
|
18562
|
+
font-weight: 600;
|
|
18563
|
+
}
|
|
18564
|
+
|
|
18565
|
+
.detail-content h1 { font-size: 1.3rem; }
|
|
18566
|
+
.detail-content h2 { font-size: 1.15rem; }
|
|
18567
|
+
.detail-content h3 { font-size: 1rem; }
|
|
18568
|
+
.detail-content p { margin-bottom: 0.75rem; }
|
|
18569
|
+
.detail-content ul, .detail-content ol { margin: 0.5rem 0 0.75rem 1.5rem; }
|
|
18570
|
+
.detail-content li { margin-bottom: 0.25rem; }
|
|
18571
|
+
.detail-content code {
|
|
18572
|
+
background: var(--bg-hover);
|
|
18573
|
+
padding: 0.1rem 0.35rem;
|
|
18574
|
+
border-radius: 3px;
|
|
18575
|
+
font-family: var(--mono);
|
|
18576
|
+
font-size: 0.85em;
|
|
18577
|
+
}
|
|
18578
|
+
|
|
18579
|
+
/* Filters */
|
|
18580
|
+
.filters {
|
|
18581
|
+
display: flex;
|
|
18582
|
+
gap: 0.75rem;
|
|
18583
|
+
margin-bottom: 1.5rem;
|
|
18584
|
+
flex-wrap: wrap;
|
|
18585
|
+
}
|
|
18586
|
+
|
|
18587
|
+
.filters select {
|
|
18588
|
+
background: var(--bg-card);
|
|
18589
|
+
border: 1px solid var(--border);
|
|
18590
|
+
color: var(--text);
|
|
18591
|
+
padding: 0.4rem 0.75rem;
|
|
18592
|
+
border-radius: var(--radius);
|
|
18593
|
+
font-size: 0.8rem;
|
|
18594
|
+
cursor: pointer;
|
|
18595
|
+
}
|
|
18596
|
+
|
|
18597
|
+
.filters select:focus {
|
|
18598
|
+
outline: none;
|
|
18599
|
+
border-color: var(--accent);
|
|
18600
|
+
}
|
|
18601
|
+
|
|
18602
|
+
/* Empty state */
|
|
18603
|
+
.empty {
|
|
18604
|
+
text-align: center;
|
|
18605
|
+
padding: 3rem;
|
|
18606
|
+
color: var(--text-dim);
|
|
18607
|
+
}
|
|
18608
|
+
|
|
18609
|
+
.empty p { font-size: 0.9rem; }
|
|
18610
|
+
|
|
18611
|
+
/* Section heading */
|
|
18612
|
+
.section-title {
|
|
18613
|
+
font-size: 0.9rem;
|
|
18614
|
+
font-weight: 600;
|
|
18615
|
+
margin: 1.5rem 0 0.75rem;
|
|
18616
|
+
}
|
|
18617
|
+
|
|
18618
|
+
/* Priority */
|
|
18619
|
+
.priority-high { color: var(--red); }
|
|
18620
|
+
.priority-medium { color: var(--amber); }
|
|
18621
|
+
.priority-low { color: var(--green); }
|
|
18622
|
+
`;
|
|
18623
|
+
}
|
|
18624
|
+
|
|
18625
|
+
// src/web/templates/pages/overview.ts
|
|
18626
|
+
function overviewPage(data) {
|
|
18627
|
+
const cards = data.types.map(
|
|
18628
|
+
(t) => `
|
|
18629
|
+
<div class="card">
|
|
18630
|
+
<a href="/docs/${t.type}">
|
|
18631
|
+
<div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
|
|
18632
|
+
<div class="card-value">${t.total}</div>
|
|
18633
|
+
${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
|
|
18634
|
+
</a>
|
|
18635
|
+
</div>`
|
|
18636
|
+
).join("\n");
|
|
18637
|
+
const rows = data.recent.map(
|
|
18638
|
+
(doc) => `
|
|
18639
|
+
<tr>
|
|
18640
|
+
<td><a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
|
|
18641
|
+
<td>${escapeHtml(doc.frontmatter.title)}</td>
|
|
18642
|
+
<td>${escapeHtml(typeLabel(doc.frontmatter.type))}</td>
|
|
18643
|
+
<td>${statusBadge(doc.frontmatter.status)}</td>
|
|
18644
|
+
<td>${formatDate(doc.frontmatter.updated ?? doc.frontmatter.created)}</td>
|
|
18645
|
+
</tr>`
|
|
18646
|
+
).join("\n");
|
|
18647
|
+
return `
|
|
18648
|
+
<div class="page-header">
|
|
18649
|
+
<h2>Project Overview</h2>
|
|
18650
|
+
</div>
|
|
18651
|
+
|
|
18652
|
+
<div class="cards">
|
|
18653
|
+
${cards}
|
|
18654
|
+
</div>
|
|
18655
|
+
|
|
18656
|
+
<div class="section-title">Recent Activity</div>
|
|
18657
|
+
${data.recent.length > 0 ? `
|
|
18658
|
+
<div class="table-wrap">
|
|
18659
|
+
<table>
|
|
18660
|
+
<thead>
|
|
18661
|
+
<tr>
|
|
18662
|
+
<th>ID</th>
|
|
18663
|
+
<th>Title</th>
|
|
18664
|
+
<th>Type</th>
|
|
18665
|
+
<th>Status</th>
|
|
18666
|
+
<th>Updated</th>
|
|
18667
|
+
</tr>
|
|
18668
|
+
</thead>
|
|
18669
|
+
<tbody>
|
|
18670
|
+
${rows}
|
|
18671
|
+
</tbody>
|
|
18672
|
+
</table>
|
|
18673
|
+
</div>` : `<div class="empty"><p>No documents yet.</p></div>`}
|
|
18674
|
+
`;
|
|
18675
|
+
}
|
|
18676
|
+
|
|
18677
|
+
// src/web/templates/pages/documents.ts
|
|
18678
|
+
function documentsPage(data) {
|
|
18679
|
+
const label = typeLabel(data.type);
|
|
18680
|
+
const statusOptions = data.statuses.map(
|
|
18681
|
+
(s) => `<option value="${escapeHtml(s)}"${data.filterStatus === s ? " selected" : ""}>${escapeHtml(s)}</option>`
|
|
18682
|
+
).join("");
|
|
18683
|
+
const ownerOptions = data.owners.map(
|
|
18684
|
+
(o) => `<option value="${escapeHtml(o)}"${data.filterOwner === o ? " selected" : ""}>${escapeHtml(o)}</option>`
|
|
18685
|
+
).join("");
|
|
18686
|
+
const rows = data.docs.map(
|
|
18687
|
+
(doc) => `
|
|
18688
|
+
<tr>
|
|
18689
|
+
<td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
|
|
18690
|
+
<td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a></td>
|
|
18691
|
+
<td>${statusBadge(doc.frontmatter.status)}</td>
|
|
18692
|
+
<td>${escapeHtml(doc.frontmatter.owner ?? "\u2014")}</td>
|
|
18693
|
+
<td>${doc.frontmatter.priority ? `<span class="priority-${doc.frontmatter.priority.toLowerCase()}">${escapeHtml(doc.frontmatter.priority)}</span>` : "\u2014"}</td>
|
|
18694
|
+
<td>${formatDate(doc.frontmatter.updated ?? doc.frontmatter.created)}</td>
|
|
18695
|
+
</tr>`
|
|
18696
|
+
).join("\n");
|
|
18697
|
+
return `
|
|
18698
|
+
<div class="page-header">
|
|
18699
|
+
<h2>${escapeHtml(label)}s</h2>
|
|
18700
|
+
<div class="subtitle">${data.docs.length} document${data.docs.length !== 1 ? "s" : ""}</div>
|
|
18701
|
+
</div>
|
|
18702
|
+
|
|
18703
|
+
<div class="filters">
|
|
18704
|
+
<select onchange="filterByStatus(this.value)">
|
|
18705
|
+
<option value="">All statuses</option>
|
|
18706
|
+
${statusOptions}
|
|
18707
|
+
</select>
|
|
18708
|
+
<select onchange="filterByOwner(this.value)">
|
|
18709
|
+
<option value="">All owners</option>
|
|
18710
|
+
${ownerOptions}
|
|
18711
|
+
</select>
|
|
18712
|
+
</div>
|
|
18713
|
+
|
|
18714
|
+
${data.docs.length > 0 ? `
|
|
18715
|
+
<div class="table-wrap">
|
|
18716
|
+
<table>
|
|
18717
|
+
<thead>
|
|
18718
|
+
<tr>
|
|
18719
|
+
<th>ID</th>
|
|
18720
|
+
<th>Title</th>
|
|
18721
|
+
<th>Status</th>
|
|
18722
|
+
<th>Owner</th>
|
|
18723
|
+
<th>Priority</th>
|
|
18724
|
+
<th>Updated</th>
|
|
18725
|
+
</tr>
|
|
18726
|
+
</thead>
|
|
18727
|
+
<tbody>
|
|
18728
|
+
${rows}
|
|
18729
|
+
</tbody>
|
|
18730
|
+
</table>
|
|
18731
|
+
</div>` : `<div class="empty"><p>No ${label.toLowerCase()}s found.</p></div>`}
|
|
18732
|
+
|
|
18733
|
+
<script>
|
|
18734
|
+
function filterByStatus(status) {
|
|
18735
|
+
const url = new URL(window.location);
|
|
18736
|
+
if (status) url.searchParams.set('status', status);
|
|
18737
|
+
else url.searchParams.delete('status');
|
|
18738
|
+
window.location = url;
|
|
18739
|
+
}
|
|
18740
|
+
function filterByOwner(owner) {
|
|
18741
|
+
const url = new URL(window.location);
|
|
18742
|
+
if (owner) url.searchParams.set('owner', owner);
|
|
18743
|
+
else url.searchParams.delete('owner');
|
|
18744
|
+
window.location = url;
|
|
18745
|
+
}
|
|
18746
|
+
</script>
|
|
18747
|
+
`;
|
|
18748
|
+
}
|
|
18749
|
+
|
|
18750
|
+
// src/web/templates/pages/document-detail.ts
|
|
18751
|
+
function documentDetailPage(doc) {
|
|
18752
|
+
const fm = doc.frontmatter;
|
|
18753
|
+
const label = typeLabel(fm.type);
|
|
18754
|
+
const skipKeys = /* @__PURE__ */ new Set(["title", "type"]);
|
|
18755
|
+
const entries = Object.entries(fm).filter(
|
|
18756
|
+
([key]) => !skipKeys.has(key) && fm[key] != null
|
|
18757
|
+
);
|
|
18758
|
+
const dtDd = entries.map(([key, value]) => {
|
|
18759
|
+
let rendered;
|
|
18760
|
+
if (key === "status") {
|
|
18761
|
+
rendered = statusBadge(value);
|
|
18762
|
+
} else if (key === "tags" && Array.isArray(value)) {
|
|
18763
|
+
rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
|
|
18764
|
+
} else if (key === "created" || key === "updated") {
|
|
18765
|
+
rendered = formatDate(value);
|
|
18766
|
+
} else {
|
|
18767
|
+
rendered = escapeHtml(String(value));
|
|
18768
|
+
}
|
|
18769
|
+
return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
|
|
18770
|
+
}).join("\n ");
|
|
18771
|
+
return `
|
|
18772
|
+
<div class="breadcrumb">
|
|
18773
|
+
<a href="/">Overview</a><span class="sep">/</span>
|
|
18774
|
+
<a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
|
|
18775
|
+
${escapeHtml(fm.id)}
|
|
18776
|
+
</div>
|
|
18777
|
+
|
|
18778
|
+
<div class="page-header">
|
|
18779
|
+
<h2>${escapeHtml(fm.title)}</h2>
|
|
18780
|
+
<div class="subtitle">${escapeHtml(fm.id)} · ${escapeHtml(label)}</div>
|
|
18781
|
+
</div>
|
|
18782
|
+
|
|
18783
|
+
<div class="detail-meta">
|
|
18784
|
+
<dl>
|
|
18785
|
+
${dtDd}
|
|
18786
|
+
</dl>
|
|
18787
|
+
</div>
|
|
18788
|
+
|
|
18789
|
+
${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
|
|
18790
|
+
`;
|
|
18791
|
+
}
|
|
18792
|
+
|
|
18793
|
+
// src/web/templates/pages/gar.ts
|
|
18794
|
+
function garPage(report) {
|
|
18795
|
+
const dotClass = `dot-${report.overall}`;
|
|
18796
|
+
const areaCards = report.areas.map(
|
|
18797
|
+
(area) => `
|
|
18798
|
+
<div class="gar-area">
|
|
18799
|
+
<div class="area-header">
|
|
18800
|
+
<div class="area-dot dot-${area.status}"></div>
|
|
18801
|
+
<div class="area-name">${escapeHtml(area.name)}</div>
|
|
18802
|
+
</div>
|
|
18803
|
+
<div class="area-summary">${escapeHtml(area.summary)}</div>
|
|
18804
|
+
${area.items.length > 0 ? `<ul>${area.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.title)}</li>`).join("")}</ul>` : ""}
|
|
18805
|
+
</div>`
|
|
18806
|
+
).join("\n");
|
|
18807
|
+
return `
|
|
18808
|
+
<div class="page-header">
|
|
18809
|
+
<h2>GAR Report</h2>
|
|
18810
|
+
<div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
|
|
18811
|
+
</div>
|
|
18812
|
+
|
|
18813
|
+
<div class="gar-overall">
|
|
18814
|
+
<div class="dot ${dotClass}"></div>
|
|
18815
|
+
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
18816
|
+
</div>
|
|
18817
|
+
|
|
18818
|
+
<div class="gar-areas">
|
|
18819
|
+
${areaCards}
|
|
18820
|
+
</div>
|
|
18821
|
+
`;
|
|
18822
|
+
}
|
|
18823
|
+
|
|
18824
|
+
// src/web/templates/pages/board.ts
|
|
18825
|
+
function boardPage(data) {
|
|
18826
|
+
const typeOptions = data.types.map(
|
|
18827
|
+
(t) => `<option value="${escapeHtml(t)}"${data.type === t ? " selected" : ""}>${escapeHtml(typeLabel(t))}s</option>`
|
|
18828
|
+
).join("");
|
|
18829
|
+
const columns = data.columns.map(
|
|
18830
|
+
(col) => `
|
|
18831
|
+
<div class="board-column">
|
|
18832
|
+
<div class="board-column-header">
|
|
18833
|
+
<span>${escapeHtml(col.status)}</span>
|
|
18834
|
+
<span class="count">${col.docs.length}</span>
|
|
18835
|
+
</div>
|
|
18836
|
+
${col.docs.map(
|
|
18837
|
+
(doc) => `
|
|
18838
|
+
<div class="board-card">
|
|
18839
|
+
<a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">
|
|
18840
|
+
<div class="bc-id">${escapeHtml(doc.frontmatter.id)}</div>
|
|
18841
|
+
<div class="bc-title">${escapeHtml(doc.frontmatter.title)}</div>
|
|
18842
|
+
${doc.frontmatter.owner ? `<div class="bc-owner">${escapeHtml(doc.frontmatter.owner)}</div>` : ""}
|
|
18843
|
+
</a>
|
|
18844
|
+
</div>`
|
|
18845
|
+
).join("\n")}
|
|
18846
|
+
</div>`
|
|
18847
|
+
).join("\n");
|
|
18848
|
+
return `
|
|
18849
|
+
<div class="page-header">
|
|
18850
|
+
<h2>Status Board</h2>
|
|
18851
|
+
</div>
|
|
18852
|
+
|
|
18853
|
+
<div class="filters">
|
|
18854
|
+
<select onchange="filterByType(this.value)">
|
|
18855
|
+
<option value="">All types</option>
|
|
18856
|
+
${typeOptions}
|
|
18857
|
+
</select>
|
|
18858
|
+
</div>
|
|
18859
|
+
|
|
18860
|
+
${data.columns.length > 0 ? `<div class="board">${columns}</div>` : `<div class="empty"><p>No documents to display.</p></div>`}
|
|
18861
|
+
|
|
18862
|
+
<script>
|
|
18863
|
+
function filterByType(type) {
|
|
18864
|
+
if (type) window.location = '/board/' + type;
|
|
18865
|
+
else window.location = '/board';
|
|
18866
|
+
}
|
|
18867
|
+
</script>
|
|
18868
|
+
`;
|
|
18869
|
+
}
|
|
18870
|
+
|
|
18871
|
+
// src/web/router.ts
|
|
18872
|
+
function handleRequest(req, res, store, projectName, navGroups) {
|
|
18873
|
+
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
18874
|
+
const pathname = parsed.pathname;
|
|
18875
|
+
const navTypes = store.registeredTypes;
|
|
18876
|
+
try {
|
|
18877
|
+
if (pathname === "/styles.css") {
|
|
18878
|
+
res.writeHead(200, {
|
|
18879
|
+
"Content-Type": "text/css",
|
|
18880
|
+
"Cache-Control": "public, max-age=300"
|
|
18881
|
+
});
|
|
18882
|
+
res.end(renderStyles());
|
|
18883
|
+
return;
|
|
18884
|
+
}
|
|
18885
|
+
if (pathname === "/") {
|
|
18886
|
+
const data = getOverviewData(store);
|
|
18887
|
+
const body = overviewPage(data);
|
|
18888
|
+
respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
|
|
18889
|
+
return;
|
|
18890
|
+
}
|
|
18891
|
+
if (pathname === "/gar") {
|
|
18892
|
+
const report = getGarData(store, projectName);
|
|
18893
|
+
const body = garPage(report);
|
|
18894
|
+
respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
|
|
18895
|
+
return;
|
|
18896
|
+
}
|
|
18897
|
+
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
18898
|
+
if (boardMatch) {
|
|
18899
|
+
const type = boardMatch[1];
|
|
18900
|
+
if (type && !navTypes.includes(type)) {
|
|
18901
|
+
notFound(res, projectName, navGroups, pathname);
|
|
18902
|
+
return;
|
|
18903
|
+
}
|
|
18904
|
+
const data = getBoardData(store, type);
|
|
18905
|
+
const body = boardPage(data);
|
|
18906
|
+
respond(res, layout({ title: "Board", activePath: "/board", projectName, navGroups }, body));
|
|
18907
|
+
return;
|
|
18908
|
+
}
|
|
18909
|
+
const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
|
|
18910
|
+
if (detailMatch) {
|
|
18911
|
+
const [, type, id] = detailMatch;
|
|
18912
|
+
const doc = getDocumentDetail(store, type, id);
|
|
18913
|
+
if (!doc) {
|
|
18914
|
+
notFound(res, projectName, navGroups, pathname);
|
|
18915
|
+
return;
|
|
18916
|
+
}
|
|
18917
|
+
const body = documentDetailPage(doc);
|
|
18918
|
+
respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups }, body));
|
|
18919
|
+
return;
|
|
18920
|
+
}
|
|
18921
|
+
const listMatch = pathname.match(/^\/docs\/([^/]+)$/);
|
|
18922
|
+
if (listMatch) {
|
|
18923
|
+
const type = listMatch[1];
|
|
18924
|
+
const filterStatus = parsed.searchParams.get("status") ?? void 0;
|
|
18925
|
+
const filterOwner = parsed.searchParams.get("owner") ?? void 0;
|
|
18926
|
+
const data = getDocumentListData(store, type, filterStatus, filterOwner);
|
|
18927
|
+
if (!data) {
|
|
18928
|
+
notFound(res, projectName, navGroups, pathname);
|
|
18929
|
+
return;
|
|
18930
|
+
}
|
|
18931
|
+
const body = documentsPage(data);
|
|
18932
|
+
respond(res, layout({ title: `${type}`, activePath: `/docs/${type}`, projectName, navGroups }, body));
|
|
18933
|
+
return;
|
|
18934
|
+
}
|
|
18935
|
+
notFound(res, projectName, navGroups, pathname);
|
|
18936
|
+
} catch (err) {
|
|
18937
|
+
console.error("[marvin web] Error handling request:", err);
|
|
18938
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
18939
|
+
res.end("<h1>500 \u2014 Internal Server Error</h1>");
|
|
18940
|
+
}
|
|
18941
|
+
}
|
|
18942
|
+
function respond(res, html) {
|
|
18943
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
18944
|
+
res.end(html);
|
|
18945
|
+
}
|
|
18946
|
+
function notFound(res, projectName, navGroups, activePath) {
|
|
18947
|
+
const body = `<div class="empty"><h2>404</h2><p>Page not found.</p><p><a href="/">Go to overview</a></p></div>`;
|
|
18948
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
18949
|
+
res.end(layout({ title: "Not Found", activePath, projectName, navGroups }, body));
|
|
18950
|
+
}
|
|
18951
|
+
|
|
18952
|
+
// src/web/server.ts
|
|
18953
|
+
import * as http from "http";
|
|
18954
|
+
import { exec } from "child_process";
|
|
18955
|
+
function openBrowser(url2) {
|
|
18956
|
+
const platform = process.platform;
|
|
18957
|
+
const cmd = platform === "darwin" ? `open "${url2}"` : platform === "win32" ? `start "${url2}"` : `xdg-open "${url2}"`;
|
|
18958
|
+
exec(cmd, (err) => {
|
|
18959
|
+
if (err) {
|
|
18960
|
+
}
|
|
18961
|
+
});
|
|
18962
|
+
}
|
|
18963
|
+
|
|
18964
|
+
// src/agent/tools/web.ts
|
|
18965
|
+
var runningServer = null;
|
|
18966
|
+
function createWebTools(store, projectName, navGroups) {
|
|
18967
|
+
return [
|
|
18968
|
+
tool20(
|
|
18969
|
+
"start_web_dashboard",
|
|
18970
|
+
"Start the Marvin web dashboard on a local port. Returns the base URL. If already running, returns the existing URL.",
|
|
18971
|
+
{
|
|
18972
|
+
port: external_exports.number().optional().describe("Port to listen on (default: 3000)"),
|
|
18973
|
+
open: external_exports.boolean().optional().describe("Open the dashboard in the default browser (default: true)")
|
|
18974
|
+
},
|
|
18975
|
+
async (args) => {
|
|
18976
|
+
const port = args.port ?? 3e3;
|
|
18977
|
+
if (runningServer) {
|
|
18978
|
+
const url3 = `http://localhost:${runningServer.port}`;
|
|
18979
|
+
return {
|
|
18980
|
+
content: [{ type: "text", text: `Dashboard already running at ${url3}` }]
|
|
18981
|
+
};
|
|
18982
|
+
}
|
|
18983
|
+
const server = http2.createServer((req, res) => {
|
|
18984
|
+
handleRequest(req, res, store, projectName, navGroups);
|
|
18985
|
+
});
|
|
18986
|
+
await new Promise((resolve3, reject) => {
|
|
18987
|
+
server.on("error", reject);
|
|
18988
|
+
server.listen(port, () => resolve3());
|
|
18989
|
+
});
|
|
18990
|
+
runningServer = { server, port };
|
|
18991
|
+
const url2 = `http://localhost:${port}`;
|
|
18992
|
+
if (args.open !== false) {
|
|
18993
|
+
openBrowser(url2);
|
|
18994
|
+
}
|
|
18995
|
+
return {
|
|
18996
|
+
content: [{ type: "text", text: `Dashboard started at ${url2}` }]
|
|
18997
|
+
};
|
|
18998
|
+
}
|
|
18999
|
+
),
|
|
19000
|
+
tool20(
|
|
19001
|
+
"stop_web_dashboard",
|
|
19002
|
+
"Stop the running Marvin web dashboard.",
|
|
19003
|
+
{},
|
|
19004
|
+
async () => {
|
|
19005
|
+
if (!runningServer) {
|
|
19006
|
+
return {
|
|
19007
|
+
content: [{ type: "text", text: "No dashboard is currently running." }],
|
|
19008
|
+
isError: true
|
|
19009
|
+
};
|
|
19010
|
+
}
|
|
19011
|
+
await new Promise((resolve3) => {
|
|
19012
|
+
runningServer.server.close(() => resolve3());
|
|
19013
|
+
});
|
|
19014
|
+
runningServer = null;
|
|
19015
|
+
return {
|
|
19016
|
+
content: [{ type: "text", text: "Dashboard stopped." }]
|
|
19017
|
+
};
|
|
19018
|
+
}
|
|
19019
|
+
),
|
|
19020
|
+
tool20(
|
|
19021
|
+
"get_web_dashboard_urls",
|
|
19022
|
+
"Get all available dashboard page URLs. The dashboard must be running.",
|
|
19023
|
+
{},
|
|
19024
|
+
async () => {
|
|
19025
|
+
if (!runningServer) {
|
|
19026
|
+
return {
|
|
19027
|
+
content: [{ type: "text", text: "Dashboard is not running. Use start_web_dashboard first." }],
|
|
19028
|
+
isError: true
|
|
19029
|
+
};
|
|
19030
|
+
}
|
|
19031
|
+
const base = `http://localhost:${runningServer.port}`;
|
|
19032
|
+
const urls = {
|
|
19033
|
+
overview: base,
|
|
19034
|
+
gar: `${base}/gar`,
|
|
19035
|
+
board: `${base}/board`
|
|
19036
|
+
};
|
|
19037
|
+
for (const type of store.registeredTypes) {
|
|
19038
|
+
urls[type] = `${base}/docs/${type}`;
|
|
19039
|
+
}
|
|
19040
|
+
return {
|
|
19041
|
+
content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
|
|
19042
|
+
};
|
|
19043
|
+
},
|
|
19044
|
+
{ annotations: { readOnly: true } }
|
|
19045
|
+
),
|
|
19046
|
+
tool20(
|
|
19047
|
+
"get_dashboard_overview",
|
|
19048
|
+
"Get the project overview data: document type counts and recent activity. Works without the web server running.",
|
|
19049
|
+
{},
|
|
19050
|
+
async () => {
|
|
19051
|
+
const data = getOverviewData(store);
|
|
19052
|
+
const result = {
|
|
19053
|
+
types: data.types,
|
|
19054
|
+
recent: data.recent.map((d) => ({
|
|
19055
|
+
id: d.frontmatter.id,
|
|
19056
|
+
type: d.frontmatter.type,
|
|
19057
|
+
title: d.frontmatter.title,
|
|
19058
|
+
status: d.frontmatter.status,
|
|
19059
|
+
updated: d.frontmatter.updated ?? d.frontmatter.created
|
|
19060
|
+
}))
|
|
19061
|
+
};
|
|
19062
|
+
return {
|
|
19063
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
19064
|
+
};
|
|
19065
|
+
},
|
|
19066
|
+
{ annotations: { readOnly: true } }
|
|
19067
|
+
),
|
|
19068
|
+
tool20(
|
|
19069
|
+
"get_dashboard_gar",
|
|
19070
|
+
"Get the GAR (Governance, Actions, Risks) report as JSON. Works without the web server running.",
|
|
19071
|
+
{},
|
|
19072
|
+
async () => {
|
|
19073
|
+
const report = getGarData(store, projectName);
|
|
19074
|
+
return {
|
|
19075
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
19076
|
+
};
|
|
19077
|
+
},
|
|
19078
|
+
{ annotations: { readOnly: true } }
|
|
19079
|
+
),
|
|
19080
|
+
tool20(
|
|
19081
|
+
"get_dashboard_board",
|
|
19082
|
+
"Get board data showing documents grouped by status. Optionally filter by document type. Works without the web server running.",
|
|
19083
|
+
{
|
|
19084
|
+
type: external_exports.string().optional().describe("Document type to filter by (e.g. 'decision', 'action')")
|
|
19085
|
+
},
|
|
19086
|
+
async (args) => {
|
|
19087
|
+
const data = getBoardData(store, args.type);
|
|
19088
|
+
const result = {
|
|
19089
|
+
type: data.type ?? "all",
|
|
19090
|
+
types: data.types,
|
|
19091
|
+
columns: data.columns.map((col) => ({
|
|
19092
|
+
status: col.status,
|
|
19093
|
+
count: col.docs.length,
|
|
19094
|
+
docs: col.docs.map((d) => ({
|
|
19095
|
+
id: d.frontmatter.id,
|
|
19096
|
+
type: d.frontmatter.type,
|
|
19097
|
+
title: d.frontmatter.title,
|
|
19098
|
+
owner: d.frontmatter.owner
|
|
19099
|
+
}))
|
|
19100
|
+
}))
|
|
19101
|
+
};
|
|
19102
|
+
return {
|
|
19103
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
19104
|
+
};
|
|
19105
|
+
},
|
|
19106
|
+
{ annotations: { readOnly: true } }
|
|
19107
|
+
)
|
|
19108
|
+
];
|
|
19109
|
+
}
|
|
19110
|
+
|
|
19111
|
+
// src/agent/mcp-server.ts
|
|
17946
19112
|
function createMarvinMcpServer(store, options) {
|
|
17947
19113
|
const tools = [
|
|
17948
19114
|
...createDecisionTools(store),
|
|
@@ -17952,7 +19118,8 @@ function createMarvinMcpServer(store, options) {
|
|
|
17952
19118
|
...options?.manifest ? createSourceTools(options.manifest) : [],
|
|
17953
19119
|
...options?.sessionStore ? createSessionTools(options.sessionStore) : [],
|
|
17954
19120
|
...options?.pluginTools ?? [],
|
|
17955
|
-
...options?.skillTools ?? []
|
|
19121
|
+
...options?.skillTools ?? [],
|
|
19122
|
+
...options?.projectName && options?.navGroups ? createWebTools(store, options.projectName, options.navGroups) : []
|
|
17956
19123
|
];
|
|
17957
19124
|
return createSdkMcpServer({
|
|
17958
19125
|
name: "marvin-governance",
|
|
@@ -18024,7 +19191,7 @@ function createSkillActionTools(skills, context) {
|
|
|
18024
19191
|
if (!skill.actions) continue;
|
|
18025
19192
|
for (const action of skill.actions) {
|
|
18026
19193
|
tools.push(
|
|
18027
|
-
|
|
19194
|
+
tool21(
|
|
18028
19195
|
`${skill.id}__${action.id}`,
|
|
18029
19196
|
action.description,
|
|
18030
19197
|
{
|
|
@@ -18251,10 +19418,10 @@ ${lines.join("\n\n")}`;
|
|
|
18251
19418
|
}
|
|
18252
19419
|
|
|
18253
19420
|
// src/mcp/persona-tools.ts
|
|
18254
|
-
import { tool as
|
|
19421
|
+
import { tool as tool22 } from "@anthropic-ai/claude-agent-sdk";
|
|
18255
19422
|
function createPersonaTools(ctx, marvinDir) {
|
|
18256
19423
|
return [
|
|
18257
|
-
|
|
19424
|
+
tool22(
|
|
18258
19425
|
"set_persona",
|
|
18259
19426
|
"Set the active persona for this session. Returns full guidance for the selected persona including behavioral rules, allowed document types, and scope. Call this before working to ensure persona-appropriate behavior.",
|
|
18260
19427
|
{
|
|
@@ -18284,7 +19451,7 @@ ${summaries}`
|
|
|
18284
19451
|
};
|
|
18285
19452
|
}
|
|
18286
19453
|
),
|
|
18287
|
-
|
|
19454
|
+
tool22(
|
|
18288
19455
|
"get_persona_guidance",
|
|
18289
19456
|
"Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
|
|
18290
19457
|
{
|