packmind 0.1.0
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/LICENSE +202 -0
- package/README.md +136 -0
- package/dist/adapters/claude-code.js +67 -0
- package/dist/bin/packmind.js +13 -0
- package/dist/cli/backup-cmd.js +41 -0
- package/dist/cli/ctx.js +14 -0
- package/dist/cli/dashboard-cmd.js +34 -0
- package/dist/cli/doctor.js +45 -0
- package/dist/cli/index-cmd.js +15 -0
- package/dist/cli/index.js +68 -0
- package/dist/cli/init.js +83 -0
- package/dist/cli/insights-cmd.js +28 -0
- package/dist/cli/locate.js +15 -0
- package/dist/cli/maintain-cmd.js +39 -0
- package/dist/cli/mcp-cmd.js +5 -0
- package/dist/cli/policy-cmd.js +40 -0
- package/dist/cli/recall-cmd.js +18 -0
- package/dist/cli/registry.js +32 -0
- package/dist/cli/scan.js +18 -0
- package/dist/cli/solutions-cmd.js +24 -0
- package/dist/cli/status.js +28 -0
- package/dist/cli/update.js +73 -0
- package/dist/cost/estimator.js +21 -0
- package/dist/cost/exact.js +35 -0
- package/dist/cost/insights.js +80 -0
- package/dist/cost/ledger.js +47 -0
- package/dist/cost/pricing.js +27 -0
- package/dist/dashboard/server.js +128 -0
- package/dist/guard/path-guard.js +26 -0
- package/dist/guard/policy.js +0 -0
- package/dist/guard/secrets.js +29 -0
- package/dist/hooks/post-read.js +80 -0
- package/dist/hooks/post-write.js +107 -0
- package/dist/hooks/pre-read.js +94 -0
- package/dist/hooks/pre-write.js +101 -0
- package/dist/hooks/prompt-submit.js +37 -0
- package/dist/hooks/runtime.js +471 -0
- package/dist/hooks/session-start.js +72 -0
- package/dist/hooks/stop.js +69 -0
- package/dist/mcp/server.js +112 -0
- package/dist/mcp/tools.js +130 -0
- package/dist/recall/chunker.js +24 -0
- package/dist/recall/embedder.js +51 -0
- package/dist/recall/indexer.js +94 -0
- package/dist/recall/queue.js +24 -0
- package/dist/recall/store.js +48 -0
- package/dist/state/describe.js +63 -0
- package/dist/state/files.js +45 -0
- package/dist/state/formats.js +80 -0
- package/dist/state/maintain.js +25 -0
- package/dist/state/mapper.js +56 -0
- package/dist/state/project.js +33 -0
- package/dist/state/schema.js +47 -0
- package/dist/state/snapshot.js +69 -0
- package/dist/state/walk.js +75 -0
- package/dist/util/fs-atomic.js +106 -0
- package/dist/util/logger.js +17 -0
- package/dist/util/paths.js +20 -0
- package/dist/util/platform.js +10 -0
- package/package.json +72 -0
- package/src/templates/PACKMIND.md +42 -0
- package/src/templates/claude-md-snippet.md +9 -0
- package/src/templates/config.json +48 -0
- package/src/templates/dashboard.html +359 -0
- package/src/templates/gitattributes +2 -0
- package/src/templates/handoff.md +3 -0
- package/src/templates/hooks-package.json +4 -0
- package/src/templates/identity.md +5 -0
- package/src/templates/journal.md +3 -0
- package/src/templates/knowledge.md +12 -0
- package/src/templates/logo-dark.svg +23 -0
- package/src/templates/logo.svg +23 -0
- package/src/templates/map.md +3 -0
- package/src/templates/policy.json +11 -0
- package/src/templates/solutions.json +1 -0
- package/src/templates/usage.json +17 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>PackMind</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #0a0e14; --side: #0d1117; --surface: #131922; --surface2: #1a212c;
|
|
11
|
+
--border: #20262f; --border2: #2a323d;
|
|
12
|
+
--text: #e6edf3; --muted: #8b95a5; --faint: #5b6675;
|
|
13
|
+
--accent: #00e5ff; --accent2: #4dabf5; --good: #3fb950; --warn: #d29922;
|
|
14
|
+
}
|
|
15
|
+
* { box-sizing: border-box; }
|
|
16
|
+
html, body { height: 100%; }
|
|
17
|
+
body { margin: 0; background: var(--bg); color: var(--text);
|
|
18
|
+
font: 14px/1.55 ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
|
19
|
+
a { color: inherit; }
|
|
20
|
+
|
|
21
|
+
.app { display: grid; grid-template-columns: 248px 1fr; min-height: 100vh; }
|
|
22
|
+
|
|
23
|
+
/* Sidebar */
|
|
24
|
+
.side { background: var(--side); border-right: 1px solid var(--border);
|
|
25
|
+
padding: 18px 14px; display: flex; flex-direction: column; gap: 18px; position: sticky; top: 0; height: 100vh; }
|
|
26
|
+
.brand { display: flex; align-items: center; gap: 10px; padding: 2px 6px; }
|
|
27
|
+
.brand .logo { width: 30px; height: 30px; }
|
|
28
|
+
.brand .logo img { width: 100%; height: 100%; display: block; }
|
|
29
|
+
.brand .name { font-size: 16px; font-weight: 650; letter-spacing: .2px; }
|
|
30
|
+
.proj { padding: 0 6px; }
|
|
31
|
+
.proj .pname { color: var(--muted); font-size: 12px; }
|
|
32
|
+
.pill { display: inline-flex; align-items: center; gap: 6px; margin-top: 8px;
|
|
33
|
+
font-size: 12px; color: var(--good); background: rgba(63,185,80,.12);
|
|
34
|
+
border: 1px solid rgba(63,185,80,.25); padding: 3px 10px; border-radius: 999px; }
|
|
35
|
+
.pill .d { width: 7px; height: 7px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px var(--good); }
|
|
36
|
+
nav { display: flex; flex-direction: column; gap: 2px; }
|
|
37
|
+
.nav-item { display: flex; align-items: center; gap: 11px; padding: 9px 11px;
|
|
38
|
+
border-radius: 9px; color: var(--muted); cursor: pointer; user-select: none;
|
|
39
|
+
border: 1px solid transparent; font-size: 14px; }
|
|
40
|
+
.nav-item svg { width: 17px; height: 17px; flex: none; opacity: .85; }
|
|
41
|
+
.nav-item:hover { color: var(--text); background: var(--surface); }
|
|
42
|
+
.nav-item.active { color: var(--text); background: var(--surface);
|
|
43
|
+
border-color: var(--border2); box-shadow: inset 2px 0 0 var(--accent); }
|
|
44
|
+
.side .foot { margin-top: auto; color: var(--faint); font-size: 11px; padding: 0 6px; }
|
|
45
|
+
|
|
46
|
+
/* Main */
|
|
47
|
+
main { min-width: 0; }
|
|
48
|
+
.topbar { display: flex; align-items: center; gap: 12px; padding: 22px 30px 6px; }
|
|
49
|
+
.topbar h1 { font-size: 22px; font-weight: 650; margin: 0; letter-spacing: .2px; }
|
|
50
|
+
.topbar .spacer { flex: 1; }
|
|
51
|
+
.live { display: inline-flex; align-items: center; gap: 7px; color: var(--good); font-size: 12px; }
|
|
52
|
+
.live .d { width: 7px; height: 7px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px var(--good); }
|
|
53
|
+
.content { padding: 16px 30px 40px; max-width: 1180px; }
|
|
54
|
+
|
|
55
|
+
.hero { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 26px 28px; margin-bottom: 18px; }
|
|
56
|
+
.hero h2 { margin: 0 0 8px; font-size: 30px; font-weight: 700; }
|
|
57
|
+
.hero p { margin: 0 0 14px; color: var(--muted); max-width: 760px; }
|
|
58
|
+
|
|
59
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; }
|
|
60
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 18px 20px; }
|
|
61
|
+
.card .label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .7px; }
|
|
62
|
+
.card .value { font-size: 30px; font-weight: 700; margin-top: 8px; line-height: 1.1; }
|
|
63
|
+
.card .value.accent { color: var(--accent); }
|
|
64
|
+
.card .sub { color: var(--faint); font-size: 12px; margin-top: 4px; }
|
|
65
|
+
|
|
66
|
+
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
|
|
67
|
+
.panel + .panel, .cards + .panel, .panel + .cards, .section + .section { margin-top: 18px; }
|
|
68
|
+
.panel .head { padding: 16px 20px; border-bottom: 1px solid var(--border); font-size: 13px;
|
|
69
|
+
text-transform: uppercase; letter-spacing: .6px; color: var(--muted); }
|
|
70
|
+
.panel .body { padding: 18px 20px; }
|
|
71
|
+
|
|
72
|
+
table { width: 100%; border-collapse: collapse; }
|
|
73
|
+
th, td { text-align: left; padding: 11px 20px; border-bottom: 1px solid var(--border); }
|
|
74
|
+
th { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .5px; font-weight: 500; }
|
|
75
|
+
tr:last-child td { border-bottom: none; }
|
|
76
|
+
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
77
|
+
td.num { color: var(--accent2); }
|
|
78
|
+
code { background: var(--surface2); padding: 1px 7px; border-radius: 6px; font-size: 12.5px;
|
|
79
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
80
|
+
|
|
81
|
+
.search { display: flex; gap: 10px; margin-bottom: 16px; }
|
|
82
|
+
.search input { flex: 1; background: var(--surface); border: 1px solid var(--border); color: var(--text);
|
|
83
|
+
padding: 11px 16px; border-radius: 10px; font-size: 14px; outline: none; }
|
|
84
|
+
.search input::placeholder { color: var(--faint); }
|
|
85
|
+
.search input:focus { border-color: var(--accent); }
|
|
86
|
+
.search button { background: var(--accent); color: #00252b; border: none; padding: 0 20px;
|
|
87
|
+
border-radius: 10px; font-weight: 650; cursor: pointer; }
|
|
88
|
+
|
|
89
|
+
.hit { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; }
|
|
90
|
+
.hit .h { display: flex; gap: 10px; align-items: center; font-size: 12px; margin-bottom: 8px; }
|
|
91
|
+
.hit .kind { color: var(--accent); text-transform: uppercase; letter-spacing: .5px; font-weight: 600; }
|
|
92
|
+
.hit .src { color: var(--muted); }
|
|
93
|
+
.hit .score { margin-left: auto; color: var(--accent2); font-variant-numeric: tabular-nums; }
|
|
94
|
+
.hit pre { margin: 0; white-space: pre-wrap; font-size: 12.5px; max-height: 130px; overflow: auto;
|
|
95
|
+
font-family: ui-monospace, Menlo, monospace; color: var(--text); }
|
|
96
|
+
|
|
97
|
+
.flags { display: flex; flex-direction: column; gap: 8px; }
|
|
98
|
+
.flag { font-size: 13.5px; padding: 11px 14px; border-radius: 10px; background: var(--surface2); border-left: 3px solid var(--faint); }
|
|
99
|
+
.flag b { display: block; margin-bottom: 2px; }
|
|
100
|
+
.flag.good { border-left-color: var(--good); }
|
|
101
|
+
.flag.warn { border-left-color: var(--warn); }
|
|
102
|
+
|
|
103
|
+
pre.raw { margin: 0; padding: 20px; white-space: pre-wrap; font-size: 12.5px; line-height: 1.7;
|
|
104
|
+
font-family: ui-monospace, Menlo, monospace; color: var(--muted); max-height: 70vh; overflow: auto; }
|
|
105
|
+
|
|
106
|
+
.actrow { display: flex; gap: 14px; align-items: baseline; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: 13.5px; }
|
|
107
|
+
.actrow:last-child { border-bottom: none; }
|
|
108
|
+
.actrow .t { color: var(--faint); font-variant-numeric: tabular-nums; width: 46px; flex: none; }
|
|
109
|
+
.actrow .a { color: var(--accent2); width: 90px; flex: none; }
|
|
110
|
+
.actrow .f { color: var(--text); }
|
|
111
|
+
.chartbox { padding: 10px 14px 6px; }
|
|
112
|
+
.barsh { display: flex; flex-direction: column; gap: 10px; }
|
|
113
|
+
.barh { display: grid; grid-template-columns: 130px 1fr 90px; align-items: center; gap: 12px; }
|
|
114
|
+
.barh-label { color: var(--muted); font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
115
|
+
font-family: ui-monospace, Menlo, monospace; }
|
|
116
|
+
.barh-track { height: 12px; background: var(--surface2); border-radius: 6px; overflow: hidden; }
|
|
117
|
+
.barh-fill { height: 100%; border-radius: 6px; background: linear-gradient(90deg, var(--accent2), var(--accent)); }
|
|
118
|
+
.barh-val { text-align: right; color: var(--accent2); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
119
|
+
.empty { color: var(--faint); padding: 22px; text-align: center; }
|
|
120
|
+
.muted { color: var(--muted); }
|
|
121
|
+
h3.sub { font-size: 13px; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); margin: 0 0 12px; }
|
|
122
|
+
</style>
|
|
123
|
+
</head>
|
|
124
|
+
<body>
|
|
125
|
+
<div class="app">
|
|
126
|
+
<aside class="side">
|
|
127
|
+
<div class="brand"><span class="logo"><img src="/logo-dark.svg" alt="" /></span><span class="name">PackMind</span></div>
|
|
128
|
+
<div class="proj">
|
|
129
|
+
<div class="pname" id="pname">—</div>
|
|
130
|
+
<span class="pill"><span class="d"></span> Healthy</span>
|
|
131
|
+
</div>
|
|
132
|
+
<nav id="nav"></nav>
|
|
133
|
+
<div class="foot">Loopback · token-protected</div>
|
|
134
|
+
</aside>
|
|
135
|
+
<main>
|
|
136
|
+
<div class="topbar">
|
|
137
|
+
<h1 id="title">Overview</h1>
|
|
138
|
+
<span class="spacer"></span>
|
|
139
|
+
<span class="live"><span class="d"></span> Live</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="content" id="view"><div class="empty">Loading…</div></div>
|
|
142
|
+
</main>
|
|
143
|
+
</div>
|
|
144
|
+
<script>
|
|
145
|
+
const TOKEN = "__TOKEN__";
|
|
146
|
+
const api = (p) => fetch(p + (p.includes("?") ? "&" : "?") + "token=" + TOKEN).then(r => r.json());
|
|
147
|
+
const $ = (id) => document.getElementById(id);
|
|
148
|
+
const esc = (s) => String(s ?? "").replace(/[&<>]/g, c => ({ "&": "&", "<": "<", ">": ">" }[c]));
|
|
149
|
+
const money = (n) => "$" + (Number(n) || 0).toFixed(4);
|
|
150
|
+
|
|
151
|
+
const ICON = {
|
|
152
|
+
overview: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>',
|
|
153
|
+
insights: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 17l5-5 4 4 8-8"/><path d="M16 4h5v5"/></svg>',
|
|
154
|
+
map: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"><path d="M9 4 3 6v14l6-2 6 2 6-2V4l-6 2-6-2z"/><path d="M9 4v14M15 6v14"/></svg>',
|
|
155
|
+
recall: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>',
|
|
156
|
+
solutions: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l7 3v5c0 4.4-3 7.7-7 9-4-1.3-7-4.6-7-9V6l7-3z"/><path d="m9 11 2 2 4-4"/></svg>',
|
|
157
|
+
knowledge: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"><path d="M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2z"/><path d="M4 19a2 2 0 0 1 2-2h13"/></svg>',
|
|
158
|
+
journal: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M8 6h12M8 12h12M8 18h12"/><circle cx="4" cy="6" r="1"/><circle cx="4" cy="12" r="1"/><circle cx="4" cy="18" r="1"/></svg>',
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// ---- inline-SVG charts (zero dependency) --------------------------------
|
|
162
|
+
function areaChart(values, opts) {
|
|
163
|
+
const o = Object.assign({ w: 760, h: 180, pad: 28, fmt: (v) => v }, opts || {});
|
|
164
|
+
if (!values.length) return '<div class="empty">No session data yet.</div>';
|
|
165
|
+
const { w, h, pad } = o;
|
|
166
|
+
const max = Math.max(...values, 1e-9);
|
|
167
|
+
const n = values.length;
|
|
168
|
+
const x = (i) => pad + (n === 1 ? (w - 2 * pad) / 2 : (i * (w - 2 * pad)) / (n - 1));
|
|
169
|
+
const y = (v) => h - pad - (v / max) * (h - 2 * pad);
|
|
170
|
+
const pts = values.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`);
|
|
171
|
+
const line = "M" + pts.join(" L");
|
|
172
|
+
const area = `M${x(0).toFixed(1)},${(h - pad).toFixed(1)} L` + pts.join(" L") + ` L${x(n - 1).toFixed(1)},${(h - pad).toFixed(1)} Z`;
|
|
173
|
+
const grid = [0, 0.5, 1].map(f => {
|
|
174
|
+
const yy = (h - pad - f * (h - 2 * pad)).toFixed(1);
|
|
175
|
+
return `<line x1="${pad}" y1="${yy}" x2="${w - pad}" y2="${yy}" stroke="var(--border2)" stroke-dasharray="3 4"/>` +
|
|
176
|
+
`<text x="${pad - 6}" y="${(+yy + 4)}" text-anchor="end" fill="var(--faint)" font-size="10">${esc(o.fmt(max * f))}</text>`;
|
|
177
|
+
}).join("");
|
|
178
|
+
const dots = values.map((v, i) => `<circle cx="${x(i).toFixed(1)}" cy="${y(v).toFixed(1)}" r="2.6" fill="var(--accent)"><title>${esc(o.fmt(v))}</title></circle>`).join("");
|
|
179
|
+
return `<svg viewBox="0 0 ${w} ${h}" width="100%" preserveAspectRatio="xMidYMid meet" style="display:block">
|
|
180
|
+
<defs><linearGradient id="ag" x1="0" y1="0" x2="0" y2="1">
|
|
181
|
+
<stop offset="0" stop-color="var(--accent)" stop-opacity="0.30"/>
|
|
182
|
+
<stop offset="1" stop-color="var(--accent)" stop-opacity="0"/></linearGradient></defs>
|
|
183
|
+
${grid}<path d="${area}" fill="url(#ag)"/><path d="${line}" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linejoin="round"/>${dots}
|
|
184
|
+
</svg>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function barsH(rows, opts) {
|
|
188
|
+
const o = Object.assign({ fmt: (v) => v }, opts || {});
|
|
189
|
+
if (!rows.length) return '<div class="empty">No data yet.</div>';
|
|
190
|
+
const max = Math.max(...rows.map(r => r.value), 1e-9);
|
|
191
|
+
return `<div class="barsh">` + rows.map(r => `
|
|
192
|
+
<div class="barh">
|
|
193
|
+
<div class="barh-label" title="${esc(r.label)}">${esc(r.label)}</div>
|
|
194
|
+
<div class="barh-track"><div class="barh-fill" style="width:${Math.max(2, (r.value / max) * 100).toFixed(1)}%"></div></div>
|
|
195
|
+
<div class="barh-val">${esc(o.fmt(r.value))}</div>
|
|
196
|
+
</div>`).join("") + `</div>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const SECTIONS = [
|
|
200
|
+
{ id: "overview", title: "Overview", render: renderOverview },
|
|
201
|
+
{ id: "insights", title: "Insights", render: renderInsights },
|
|
202
|
+
{ id: "map", title: "Project Map", render: renderMap },
|
|
203
|
+
{ id: "recall", title: "Recall", render: renderRecall },
|
|
204
|
+
{ id: "solutions", title: "Solutions", render: renderSolutions },
|
|
205
|
+
{ id: "knowledge", title: "Knowledge", render: renderKnowledge },
|
|
206
|
+
{ id: "journal", title: "Journal", render: renderJournal },
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
let active = "overview";
|
|
210
|
+
let timer = null;
|
|
211
|
+
|
|
212
|
+
function buildNav() {
|
|
213
|
+
$("nav").innerHTML = SECTIONS.map(s =>
|
|
214
|
+
`<div class="nav-item" data-id="${s.id}">${ICON[s.id]}<span>${s.title}</span></div>`).join("");
|
|
215
|
+
document.querySelectorAll(".nav-item").forEach(el => el.onclick = () => go(el.dataset.id));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function go(id) {
|
|
219
|
+
active = id;
|
|
220
|
+
const sec = SECTIONS.find(s => s.id === id);
|
|
221
|
+
document.querySelectorAll(".nav-item").forEach(el => el.classList.toggle("active", el.dataset.id === id));
|
|
222
|
+
$("title").textContent = sec.title;
|
|
223
|
+
$("view").innerHTML = '<div class="empty">Loading…</div>';
|
|
224
|
+
sec.render();
|
|
225
|
+
if (timer) clearInterval(timer);
|
|
226
|
+
// Live-refresh the lightweight, frequently-changing sections.
|
|
227
|
+
if (id === "overview" || id === "insights") timer = setInterval(sec.render, 5000);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function renderOverview() {
|
|
231
|
+
const [o, j, ins] = await Promise.all([api("/api/overview"), api("/api/journal"), api("/api/insights")]);
|
|
232
|
+
$("pname").textContent = o.project || "—";
|
|
233
|
+
const t = o.totals || {};
|
|
234
|
+
const acts = (j.text || "").split(/\r?\n/)
|
|
235
|
+
.map(l => l.match(/^\|\s*(\d{2}:\d{2})\s*\|\s*([^|]+?)\s*\|\s*`?([^|`]+?)`?\s*\|/))
|
|
236
|
+
.filter(Boolean).slice(-12).reverse();
|
|
237
|
+
const costs = (o.sessions || []).map(s => (s.inputCost || 0) + (s.outputCost || 0));
|
|
238
|
+
$("view").innerHTML = `
|
|
239
|
+
<div class="hero">
|
|
240
|
+
<h2>${esc(o.project || "PackMind")}</h2>
|
|
241
|
+
<p>A second brain for Claude Code: project memory, real token & cost accounting, semantic recall, and active guardrails.</p>
|
|
242
|
+
</div>
|
|
243
|
+
<div class="cards">
|
|
244
|
+
<div class="card"><div class="label">Tokens saved</div><div class="value accent">~${(ins.estTokensSaved||0).toLocaleString()}</div>
|
|
245
|
+
<div class="sub">≈ ${money(ins.estCostSaved)} · ${t.dedupedReads || 0} re-reads avoided</div></div>
|
|
246
|
+
<div class="card"><div class="label">Total cost</div><div class="value">${money(o.cost)}</div>
|
|
247
|
+
<div class="sub">${(t.inputTokens||0).toLocaleString()} in / ${(t.outputTokens||0).toLocaleString()} out</div></div>
|
|
248
|
+
<div class="card"><div class="label">Files mapped</div><div class="value">${o.files}</div>
|
|
249
|
+
<div class="sub">${o.vectors.toLocaleString()} vectors indexed</div></div>
|
|
250
|
+
<div class="card"><div class="label">Sessions</div><div class="value">${t.sessions || 0}</div></div>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="panel">
|
|
253
|
+
<div class="head">Cost per session</div>
|
|
254
|
+
<div class="body chartbox">${areaChart(costs, { fmt: (v) => "$" + v.toFixed(3) })}</div>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="panel">
|
|
257
|
+
<div class="head">Recent activity</div>
|
|
258
|
+
<div class="body">${acts.length
|
|
259
|
+
? acts.map(m => `<div class="actrow"><span class="t">${esc(m[1])}</span><span class="a">${esc(m[2])}</span><span class="f"><code>${esc(m[3])}</code></span></div>`).join("")
|
|
260
|
+
: '<div class="empty">No activity yet. Start a Claude Code session to see it here.</div>'}</div>
|
|
261
|
+
</div>`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function renderInsights() {
|
|
265
|
+
const [r, map] = await Promise.all([api("/api/insights"), api("/api/map")]);
|
|
266
|
+
const cov = r.mapCoverage === null ? "—" : Math.round(r.mapCoverage * 100) + "%";
|
|
267
|
+
// Aggregate token weight by top-level directory (client-side, from the map).
|
|
268
|
+
const byDir = {};
|
|
269
|
+
for (const e of map) {
|
|
270
|
+
const top = (e.section || "./").split("/")[0] + "/";
|
|
271
|
+
byDir[top] = (byDir[top] || 0) + e.tokens;
|
|
272
|
+
}
|
|
273
|
+
const dirRows = Object.entries(byDir)
|
|
274
|
+
.map(([label, value]) => ({ label, value }))
|
|
275
|
+
.sort((a, b) => b.value - a.value).slice(0, 10);
|
|
276
|
+
$("view").innerHTML = `
|
|
277
|
+
<div class="cards">
|
|
278
|
+
<div class="card"><div class="label">Estimated saved</div><div class="value accent">${money(r.estCostSaved)}</div>
|
|
279
|
+
<div class="sub">~${(r.estTokensSaved||0).toLocaleString()} tokens · ${r.reReadsAvoided} re-reads avoided</div></div>
|
|
280
|
+
<div class="card"><div class="label">Map coverage</div><div class="value">${cov}</div>
|
|
281
|
+
<div class="sub">of reads hit a description</div></div>
|
|
282
|
+
<div class="card"><div class="label">Total cost</div><div class="value">${money(r.totalCost)}</div>
|
|
283
|
+
<div class="sub">${(r.inputTokens||0).toLocaleString()} in / ${(r.outputTokens||0).toLocaleString()} out</div></div>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="panel">
|
|
286
|
+
<div class="head">Tokens by directory</div>
|
|
287
|
+
<div class="body">${barsH(dirRows, { fmt: (v) => "~" + (v >= 1000 ? (v / 1000).toFixed(1) + "k" : v) + " tok" })}</div>
|
|
288
|
+
</div>
|
|
289
|
+
${(r.flags && r.flags.length) ? `<div class="panel"><div class="head">Notes</div><div class="body"><div class="flags">${
|
|
290
|
+
r.flags.map(f => `<div class="flag ${f.level}"><b>${f.level === "good" ? "✓" : "!"} ${esc(f.title)}</b>${esc(f.detail)}</div>`).join("")
|
|
291
|
+
}</div></div></div>` : ""}
|
|
292
|
+
<div class="panel"><div class="head">Heaviest files</div>
|
|
293
|
+
${(r.topFiles && r.topFiles.length) ? `<table><thead><tr><th>File</th><th class="num">Tokens</th><th class="num">Cost</th></tr></thead><tbody>${
|
|
294
|
+
r.topFiles.map(f => `<tr><td><code>${esc(f.file)}</code></td><td class="num">${f.tokens}</td><td class="num">${money(f.cost)}</td></tr>`).join("")
|
|
295
|
+
}</tbody></table>` : '<div class="empty">No data yet.</div>'}</div>`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let mapData = [];
|
|
299
|
+
async function renderMap() {
|
|
300
|
+
mapData = await api("/api/map");
|
|
301
|
+
$("view").innerHTML = `
|
|
302
|
+
<div class="search"><input id="mq" placeholder="Filter files…" /></div>
|
|
303
|
+
<div class="panel" id="maptbl"></div>`;
|
|
304
|
+
const draw = () => {
|
|
305
|
+
const q = ($("mq").value || "").toLowerCase();
|
|
306
|
+
const rows = mapData.filter(r => !q || (r.section + r.file).toLowerCase().includes(q)).slice(0, 200);
|
|
307
|
+
$("maptbl").innerHTML = `<div class="head">${mapData.length} files tracked</div>` + (rows.length
|
|
308
|
+
? `<table><thead><tr><th>File</th><th>Description</th><th class="num">Tokens</th><th class="num">Cost</th></tr></thead><tbody>${
|
|
309
|
+
rows.map(r => `<tr><td><code>${esc(r.section)}${esc(r.file)}</code></td><td class="muted">${esc(r.description)}</td><td class="num">${r.tokens}</td><td class="num">${money(r.cost)}</td></tr>`).join("")
|
|
310
|
+
}</tbody></table>` : '<div class="empty">No match.</div>');
|
|
311
|
+
};
|
|
312
|
+
$("mq").addEventListener("input", draw);
|
|
313
|
+
draw();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function renderRecall() {
|
|
317
|
+
$("view").innerHTML = `
|
|
318
|
+
<div class="search">
|
|
319
|
+
<input id="rq" placeholder="Search project memory by meaning… (e.g. how is auth handled)" />
|
|
320
|
+
<button id="rgo">Search</button>
|
|
321
|
+
</div>
|
|
322
|
+
<div id="rhits"><div class="empty">Type a query and hit Search.</div></div>`;
|
|
323
|
+
const run = async () => {
|
|
324
|
+
const q = $("rq").value.trim();
|
|
325
|
+
if (!q) return;
|
|
326
|
+
$("rhits").innerHTML = '<div class="empty">Searching…</div>';
|
|
327
|
+
const { hits } = await api("/api/recall?q=" + encodeURIComponent(q));
|
|
328
|
+
$("rhits").innerHTML = (hits && hits.length)
|
|
329
|
+
? hits.map(h => `<div class="hit"><div class="h"><span class="kind">${esc(h.kind)}</span><span class="src">${esc(h.source)}</span><span class="score">${(h.score||0).toFixed(2)}</span></div><pre>${esc((h.text||"").slice(0, 600))}</pre></div>`).join("")
|
|
330
|
+
: '<div class="empty">No matches. Run <code>packmind index</code> if you haven\'t yet.</div>';
|
|
331
|
+
};
|
|
332
|
+
$("rgo").onclick = run;
|
|
333
|
+
$("rq").addEventListener("keydown", e => { if (e.key === "Enter") run(); });
|
|
334
|
+
$("rq").focus();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function renderSolutions() {
|
|
338
|
+
const sols = await api("/api/solutions");
|
|
339
|
+
$("view").innerHTML = `<div class="panel">${sols.length
|
|
340
|
+
? `<table><thead><tr><th>Error</th><th>Fix</th><th>Tags</th><th class="num">Seen</th></tr></thead><tbody>${
|
|
341
|
+
sols.map(s => `<tr><td>${esc(s.error)}</td><td class="muted">${esc(s.fix)}</td><td class="muted">${(s.tags||[]).map(esc).join(", ")}</td><td class="num">${s.occurrences||1}×</td></tr>`).join("")
|
|
342
|
+
}</tbody></table>` : '<div class="empty">No recorded solutions yet. Claude logs fixes here with the <code>record_solution</code> tool.</div>'}</div>`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function renderKnowledge() {
|
|
346
|
+
const k = await api("/api/knowledge");
|
|
347
|
+
$("view").innerHTML = `<div class="panel"><pre class="raw">${esc((k.text || "").trim() || "knowledge.md is empty.")}</pre></div>`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function renderJournal() {
|
|
351
|
+
const j = await api("/api/journal");
|
|
352
|
+
$("view").innerHTML = `<div class="panel"><pre class="raw">${esc((j.text || "").trim() || "journal.md is empty.")}</pre></div>`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
buildNav();
|
|
356
|
+
go("overview");
|
|
357
|
+
</script>
|
|
358
|
+
</body>
|
|
359
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="100%" height="100%">
|
|
2
|
+
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
3
|
+
<path d="M60 140 L60 60 C60 30 80 20 110 20 C150 20 170 40 170 70 C170 100 150 120 110 120 L90 120 L90 150" stroke="#5b6676" stroke-width="8"/>
|
|
4
|
+
<path d="M72 145 L72 70 C72 45 88 32 110 32 C142 32 158 50 158 75 C158 100 142 112 110 112 L102 112 L102 155" stroke="#93a0b3" stroke-width="8"/>
|
|
5
|
+
<path d="M84 150 L84 80 C84 60 96 44 110 44 C134 44 146 60 146 80 C146 100 134 104 110 104 L110 160" stroke="#d7deea" stroke-width="8"/>
|
|
6
|
+
</g>
|
|
7
|
+
|
|
8
|
+
<g stroke="#00e5ff" stroke-width="1.5">
|
|
9
|
+
<line x1="85" y1="55" x2="125" y2="75"/>
|
|
10
|
+
<line x1="125" y1="75" x2="160" y2="65"/>
|
|
11
|
+
<line x1="125" y1="75" x2="140" y2="115"/>
|
|
12
|
+
<line x1="125" y1="75" x2="85" y2="105"/>
|
|
13
|
+
</g>
|
|
14
|
+
|
|
15
|
+
<g fill="#4dabf5">
|
|
16
|
+
<circle cx="85" cy="55" r="3.5"/>
|
|
17
|
+
<circle cx="160" cy="65" r="3.5"/>
|
|
18
|
+
<circle cx="140" cy="115" r="3.5"/>
|
|
19
|
+
<circle cx="85" cy="105" r="3.5"/>
|
|
20
|
+
</g>
|
|
21
|
+
|
|
22
|
+
<circle cx="125" cy="75" r="8" fill="#00e5ff" filter="drop-shadow(0px 0px 4px #00e5ff)"/>
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="100%" height="100%">
|
|
2
|
+
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
3
|
+
<path d="M60 140 L60 60 C60 30 80 20 110 20 C150 20 170 40 170 70 C170 100 150 120 110 120 L90 120 L90 150" stroke="#1f2326" stroke-width="8"/>
|
|
4
|
+
<path d="M72 145 L72 70 C72 45 88 32 110 32 C142 32 158 50 158 75 C158 100 142 112 110 112 L102 112 L102 155" stroke="#2c3136" stroke-width="8"/>
|
|
5
|
+
<path d="M84 150 L84 80 C84 60 96 44 110 44 C134 44 146 60 146 80 C146 100 134 104 110 104 L110 160" stroke="#3d444b" stroke-width="8"/>
|
|
6
|
+
</g>
|
|
7
|
+
|
|
8
|
+
<g stroke="#00e5ff" stroke-width="1.5">
|
|
9
|
+
<line x1="85" y1="55" x2="125" y2="75"/>
|
|
10
|
+
<line x1="125" y1="75" x2="160" y2="65"/>
|
|
11
|
+
<line x1="125" y1="75" x2="140" y2="115"/>
|
|
12
|
+
<line x1="125" y1="75" x2="85" y2="105"/>
|
|
13
|
+
</g>
|
|
14
|
+
|
|
15
|
+
<g fill="#4dabf5">
|
|
16
|
+
<circle cx="85" cy="55" r="3.5"/>
|
|
17
|
+
<circle cx="160" cy="65" r="3.5"/>
|
|
18
|
+
<circle cx="140" cy="115" r="3.5"/>
|
|
19
|
+
<circle cx="85" cy="105" r="3.5"/>
|
|
20
|
+
</g>
|
|
21
|
+
|
|
22
|
+
<circle cx="125" cy="75" r="8" fill="#00e5ff" filter="drop-shadow(0px 0px 4px #00e5ff)"/>
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"model": "claude-opus-4-8",
|
|
4
|
+
"createdAt": "",
|
|
5
|
+
"totals": {
|
|
6
|
+
"inputTokens": 0,
|
|
7
|
+
"outputTokens": 0,
|
|
8
|
+
"inputCost": 0,
|
|
9
|
+
"outputCost": 0,
|
|
10
|
+
"reads": 0,
|
|
11
|
+
"writes": 0,
|
|
12
|
+
"sessions": 0,
|
|
13
|
+
"dedupedReads": 0,
|
|
14
|
+
"mapHits": 0
|
|
15
|
+
},
|
|
16
|
+
"sessions": []
|
|
17
|
+
}
|