memoryintel 1.0.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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +9 -0
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/adapters/claudeCode.js +96 -0
- package/dist/adapters/genericPointer.js +39 -0
- package/dist/cli.js +157 -0
- package/dist/commands/daemonStart.js +8 -0
- package/dist/commands/dashboardToggle.js +17 -0
- package/dist/commands/init.js +111 -0
- package/dist/commands/load.js +82 -0
- package/dist/commands/status.js +24 -0
- package/dist/commands/update.js +108 -0
- package/dist/core/atomicWrite.js +6 -0
- package/dist/core/compressionConfig.js +37 -0
- package/dist/core/discovery.js +14 -0
- package/dist/core/eventLog.js +4 -0
- package/dist/core/gitPorcelain.js +45 -0
- package/dist/core/headingMatch.js +44 -0
- package/dist/core/lock.js +67 -0
- package/dist/core/memoryIndex.js +19 -0
- package/dist/core/pathSafety.js +43 -0
- package/dist/core/sectionWriter.js +91 -0
- package/dist/core/toon.js +118 -0
- package/dist/daemon/daemonHandle.js +52 -0
- package/dist/daemon/globalPaths.js +15 -0
- package/dist/daemon/health.js +14 -0
- package/dist/daemon/lifecycle.js +54 -0
- package/dist/daemon/registry.js +60 -0
- package/dist/daemon/server.js +92 -0
- package/dist/daemon/settings.js +13 -0
- package/dist/daemon/views/layout.js +233 -0
- package/dist/daemon/views/projectPage.js +111 -0
- package/dist/daemon/views/registryPage.js +54 -0
- package/dist/skill.js +46 -0
- package/dist/templates/starterFiles.js +22 -0
- package/hooks/hooks.json +11 -0
- package/package.json +52 -0
- package/skills/memoryintel/SKILL.md +55 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
export function escapeHtml(s) {
|
|
2
|
+
return s
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''');
|
|
8
|
+
}
|
|
9
|
+
// Tiers a recency value (days since some event) into the tool's one signature device: a
|
|
10
|
+
// three-step freshness read (fresh / aging / stale) used consistently for both "when did we
|
|
11
|
+
// last see this project" and "when was this memory file last touched" — the two questions
|
|
12
|
+
// this whole dashboard exists to answer at a glance.
|
|
13
|
+
export function freshnessTier(daysAgo) {
|
|
14
|
+
if (daysAgo === null || daysAgo > 7)
|
|
15
|
+
return 'stale';
|
|
16
|
+
if (daysAgo <= 1)
|
|
17
|
+
return 'fresh';
|
|
18
|
+
return 'aging';
|
|
19
|
+
}
|
|
20
|
+
export function daysSince(iso) {
|
|
21
|
+
return Math.floor((Date.now() - new Date(iso).getTime()) / (24 * 60 * 60 * 1000));
|
|
22
|
+
}
|
|
23
|
+
// A day-only label ("0d ago") is indistinguishable for anything from just now up to almost 24h
|
|
24
|
+
// old - on an actively-worked project nearly every file lands in that bucket, making the
|
|
25
|
+
// dashboard's staleness read useless right when it matters most. Minutes/hours below the 24h
|
|
26
|
+
// mark, days once it's crossed.
|
|
27
|
+
export function formatAge(ms) {
|
|
28
|
+
const totalMinutes = Math.floor(Math.max(0, ms) / 60000);
|
|
29
|
+
if (totalMinutes < 1)
|
|
30
|
+
return 'just now';
|
|
31
|
+
if (totalMinutes < 60)
|
|
32
|
+
return `${totalMinutes}m ago`;
|
|
33
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
34
|
+
if (totalHours < 24)
|
|
35
|
+
return `${totalHours}h ago`;
|
|
36
|
+
const totalDays = Math.floor(totalHours / 24);
|
|
37
|
+
return `${totalDays}d ago`;
|
|
38
|
+
}
|
|
39
|
+
const DISPLAY_FONT = `Georgia, 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', serif`;
|
|
40
|
+
const BODY_FONT = `-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif`;
|
|
41
|
+
const MONO_FONT = `'SF Mono', 'Cascadia Code', 'JetBrains Mono', Consolas, 'Liberation Mono', monospace`;
|
|
42
|
+
const BASE_STYLES = `
|
|
43
|
+
:root {
|
|
44
|
+
color-scheme: light dark;
|
|
45
|
+
--bg: #F6F3EC;
|
|
46
|
+
--surface: #FFFFFF;
|
|
47
|
+
--ink: #211D17;
|
|
48
|
+
--muted: #736B5C;
|
|
49
|
+
--accent: #A8752C;
|
|
50
|
+
--border: rgba(33, 29, 23, 0.14);
|
|
51
|
+
--fresh: #3F7D52;
|
|
52
|
+
--aging: #B8853A;
|
|
53
|
+
--stale: #A8514A;
|
|
54
|
+
}
|
|
55
|
+
@media (prefers-color-scheme: dark) {
|
|
56
|
+
:root {
|
|
57
|
+
--bg: #16130F;
|
|
58
|
+
--surface: #201C16;
|
|
59
|
+
--ink: #EEE8DA;
|
|
60
|
+
--muted: #A79D8C;
|
|
61
|
+
--accent: #E3AE58;
|
|
62
|
+
--border: rgba(238, 232, 218, 0.16);
|
|
63
|
+
--fresh: #6FAE82;
|
|
64
|
+
--aging: #D9A653;
|
|
65
|
+
--stale: #D97C74;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
* { box-sizing: border-box; }
|
|
70
|
+
body {
|
|
71
|
+
font-family: ${BODY_FONT};
|
|
72
|
+
background: var(--bg);
|
|
73
|
+
color: var(--ink);
|
|
74
|
+
max-width: 720px;
|
|
75
|
+
margin: 3rem auto;
|
|
76
|
+
padding: 0 1.25rem 4rem;
|
|
77
|
+
line-height: 1.55;
|
|
78
|
+
}
|
|
79
|
+
h1, h2, h3 { font-family: ${DISPLAY_FONT}; line-height: 1.2; font-weight: 600; margin: 0 0 0.5rem; }
|
|
80
|
+
h1 { font-size: 1.6rem; }
|
|
81
|
+
h2 { font-size: 1.15rem; margin-top: 2rem; color: var(--muted); font-weight: 600; letter-spacing: 0.01em; }
|
|
82
|
+
a { color: var(--accent); text-decoration: none; }
|
|
83
|
+
a:hover, a:focus-visible { text-decoration: underline; }
|
|
84
|
+
a:focus-visible, summary:focus-visible, button:focus-visible {
|
|
85
|
+
outline: 2px solid var(--accent);
|
|
86
|
+
outline-offset: 2px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.eyebrow {
|
|
90
|
+
font-family: ${MONO_FONT};
|
|
91
|
+
font-size: 0.72rem;
|
|
92
|
+
letter-spacing: 0.12em;
|
|
93
|
+
text-transform: uppercase;
|
|
94
|
+
color: var(--muted);
|
|
95
|
+
margin-bottom: 0.35rem;
|
|
96
|
+
}
|
|
97
|
+
.path {
|
|
98
|
+
font-family: ${MONO_FONT};
|
|
99
|
+
font-size: 0.82rem;
|
|
100
|
+
color: var(--muted);
|
|
101
|
+
word-break: break-all;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.card {
|
|
105
|
+
background: var(--surface);
|
|
106
|
+
border: 1px solid var(--border);
|
|
107
|
+
border-left: 3px solid var(--border);
|
|
108
|
+
border-radius: 6px;
|
|
109
|
+
padding: 1.1rem 1.2rem;
|
|
110
|
+
margin-bottom: 1rem;
|
|
111
|
+
animation: rise 0.25s ease-out backwards;
|
|
112
|
+
}
|
|
113
|
+
.card.fresh { border-left-color: var(--fresh); }
|
|
114
|
+
.card.aging { border-left-color: var(--aging); }
|
|
115
|
+
.card.stale { border-left-color: var(--stale); }
|
|
116
|
+
.card:nth-of-type(2) { animation-delay: 0.03s; }
|
|
117
|
+
.card:nth-of-type(3) { animation-delay: 0.06s; }
|
|
118
|
+
.card:nth-of-type(4) { animation-delay: 0.09s; }
|
|
119
|
+
@keyframes rise {
|
|
120
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
121
|
+
to { opacity: 1; transform: translateY(0); }
|
|
122
|
+
}
|
|
123
|
+
@media (prefers-reduced-motion: reduce) {
|
|
124
|
+
.card { animation: none; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.muted { opacity: 0.75; color: var(--muted); font-size: 0.9em; }
|
|
128
|
+
.missing { opacity: 0.6; font-style: italic; }
|
|
129
|
+
|
|
130
|
+
.mental-model {
|
|
131
|
+
font-family: ${DISPLAY_FONT};
|
|
132
|
+
font-size: 1.15rem;
|
|
133
|
+
font-style: italic;
|
|
134
|
+
border-left: 3px solid var(--accent);
|
|
135
|
+
padding-left: 1rem;
|
|
136
|
+
margin: 0.5rem 0 0;
|
|
137
|
+
white-space: pre-wrap;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pre {
|
|
141
|
+
font-family: ${MONO_FONT};
|
|
142
|
+
font-size: 0.85rem;
|
|
143
|
+
white-space: pre-wrap;
|
|
144
|
+
word-break: break-word;
|
|
145
|
+
background: color-mix(in srgb, currentColor 6%, transparent);
|
|
146
|
+
padding: 0.75rem;
|
|
147
|
+
border-radius: 6px;
|
|
148
|
+
margin: 0.5rem 0 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.tag {
|
|
152
|
+
display: inline-block;
|
|
153
|
+
font-family: ${MONO_FONT};
|
|
154
|
+
font-size: 0.72rem;
|
|
155
|
+
border-radius: 999px;
|
|
156
|
+
padding: 0.15em 0.75em;
|
|
157
|
+
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
158
|
+
margin: 0 0.4em 0.4em 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
details {
|
|
162
|
+
border-bottom: 1px solid var(--border);
|
|
163
|
+
padding: 0.5rem 0;
|
|
164
|
+
}
|
|
165
|
+
details:last-child { border-bottom: none; }
|
|
166
|
+
summary {
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
font-family: ${MONO_FONT};
|
|
169
|
+
font-size: 0.88rem;
|
|
170
|
+
}
|
|
171
|
+
summary .stale-label.fresh { color: var(--fresh); }
|
|
172
|
+
summary .stale-label.aging { color: var(--aging); }
|
|
173
|
+
summary .stale-label.stale { color: var(--stale); }
|
|
174
|
+
|
|
175
|
+
.timeline-entry {
|
|
176
|
+
position: relative;
|
|
177
|
+
padding-left: 1rem;
|
|
178
|
+
border-left: 2px solid var(--border);
|
|
179
|
+
padding-bottom: 1rem;
|
|
180
|
+
margin-bottom: 0;
|
|
181
|
+
}
|
|
182
|
+
.timeline-entry:last-child { padding-bottom: 0; }
|
|
183
|
+
.timeline-files { display: block; font-family: var(--mono, monospace); font-size: 0.82em; }
|
|
184
|
+
.timeline-entry::before {
|
|
185
|
+
content: '';
|
|
186
|
+
position: absolute;
|
|
187
|
+
left: -5px;
|
|
188
|
+
top: 0.3rem;
|
|
189
|
+
width: 8px;
|
|
190
|
+
height: 8px;
|
|
191
|
+
border-radius: 50%;
|
|
192
|
+
background: var(--accent);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.empty-state {
|
|
196
|
+
font-family: ${DISPLAY_FONT};
|
|
197
|
+
font-style: italic;
|
|
198
|
+
color: var(--muted);
|
|
199
|
+
padding: 2rem 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.dashboard-controls {
|
|
203
|
+
margin-top: 3rem;
|
|
204
|
+
padding-top: 1.5rem;
|
|
205
|
+
border-top: 1px solid var(--border);
|
|
206
|
+
}
|
|
207
|
+
.btn-stop {
|
|
208
|
+
font-family: ${BODY_FONT};
|
|
209
|
+
font-size: 0.85rem;
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
color: var(--stale);
|
|
212
|
+
background: color-mix(in srgb, var(--stale) 10%, var(--surface));
|
|
213
|
+
border: 1px solid var(--stale);
|
|
214
|
+
border-radius: 6px;
|
|
215
|
+
padding: 0.5rem 1rem;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
}
|
|
218
|
+
.btn-stop:hover { background: color-mix(in srgb, var(--stale) 18%, var(--surface)); }
|
|
219
|
+
`;
|
|
220
|
+
export function pageShell(title, bodyHtml) {
|
|
221
|
+
return `<!doctype html>
|
|
222
|
+
<html lang="en">
|
|
223
|
+
<head>
|
|
224
|
+
<meta charset="utf-8">
|
|
225
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
226
|
+
<title>${escapeHtml(title)}</title>
|
|
227
|
+
<style>${BASE_STYLES}</style>
|
|
228
|
+
</head>
|
|
229
|
+
<body>
|
|
230
|
+
${bodyHtml}
|
|
231
|
+
</body>
|
|
232
|
+
</html>`;
|
|
233
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
import { WRITABLE_FILES } from '../../core/pathSafety.js';
|
|
4
|
+
import { computeFileHealth } from '../health.js';
|
|
5
|
+
import { detectToolsWired } from '../registry.js';
|
|
6
|
+
import { getCeilingLines, countLines } from '../../core/compressionConfig.js';
|
|
7
|
+
import { escapeHtml, pageShell, freshnessTier, formatAge } from './layout.js';
|
|
8
|
+
function renderFileBrowser(memoryRoot) {
|
|
9
|
+
const groups = {};
|
|
10
|
+
for (const file of WRITABLE_FILES) {
|
|
11
|
+
if (file === 'context/currentMentalModel.md')
|
|
12
|
+
continue;
|
|
13
|
+
const [domain] = file.split('/');
|
|
14
|
+
(groups[domain] ??= []).push(file);
|
|
15
|
+
}
|
|
16
|
+
const health = computeFileHealth(memoryRoot);
|
|
17
|
+
const healthByFile = Object.fromEntries(health.map((h) => [h.file, h]));
|
|
18
|
+
const sections = Object.entries(groups).map(([domain, files]) => {
|
|
19
|
+
const items = files.map((file) => {
|
|
20
|
+
const path = join(memoryRoot, file);
|
|
21
|
+
const content = existsSync(path) ? readFileSync(path, 'utf-8').trim() : '';
|
|
22
|
+
const staleness = healthByFile[file]?.staleDays;
|
|
23
|
+
const lastUpdated = healthByFile[file]?.lastUpdated;
|
|
24
|
+
const tier = freshnessTier(staleness ?? null);
|
|
25
|
+
const stalenessLabel = lastUpdated ? formatAge(Date.now() - new Date(lastUpdated).getTime()) : 'never updated';
|
|
26
|
+
const lines = countLines(content);
|
|
27
|
+
const ceiling = getCeilingLines(memoryRoot, file);
|
|
28
|
+
const sizeClass = lines > ceiling ? 'stale' : 'muted';
|
|
29
|
+
const sizeLabel = `${lines}/${ceiling} lines`;
|
|
30
|
+
return `<details><summary>${escapeHtml(file)} <span class="muted stale-label ${tier}">(${stalenessLabel})</span> <span class="${sizeClass}">${escapeHtml(sizeLabel)}</span></summary><pre>${escapeHtml(content || '(empty)')}</pre></details>`;
|
|
31
|
+
}).join('\n');
|
|
32
|
+
return `<h3>${escapeHtml(domain)}</h3>\n${items}`;
|
|
33
|
+
});
|
|
34
|
+
return sections.join('\n');
|
|
35
|
+
}
|
|
36
|
+
function readEvents(memoryRoot) {
|
|
37
|
+
const eventsPath = join(memoryRoot, 'memory-events.jsonl');
|
|
38
|
+
if (!existsSync(eventsPath))
|
|
39
|
+
return [];
|
|
40
|
+
return readFileSync(eventsPath, 'utf-8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
41
|
+
}
|
|
42
|
+
// `session-load` events exist for KPI analysis (how often is this project's memory actually
|
|
43
|
+
// read, how many tokens does a session bootstrap cost), not to narrate "what changed" - the
|
|
44
|
+
// question this timeline exists to answer. A `load` fires every session start, so mixing it in
|
|
45
|
+
// unfiltered would bury real content changes under routine reads. Still fully inspectable via
|
|
46
|
+
// ?type=session-load - this only affects the unfiltered default view.
|
|
47
|
+
const TELEMETRY_ONLY_TYPES = new Set(['session-load']);
|
|
48
|
+
function renderEventTimeline(memoryRoot, typeFilter) {
|
|
49
|
+
const events = readEvents(memoryRoot);
|
|
50
|
+
if (events.length === 0)
|
|
51
|
+
return '<p class="muted">No events yet.</p>';
|
|
52
|
+
const filtered = typeFilter
|
|
53
|
+
? events.filter((e) => e.type === typeFilter)
|
|
54
|
+
: events.filter((e) => !TELEMETRY_ONLY_TYPES.has(e.type));
|
|
55
|
+
if (filtered.length === 0)
|
|
56
|
+
return '<p class="muted">No events match this filter.</p>';
|
|
57
|
+
// update() logs one event per file it writes, not one per `update` call - a single logical
|
|
58
|
+
// checkpoint spanning N files (e.g. currentMentalModel.md + progress.md for the same change)
|
|
59
|
+
// produces N events with the identical, agent-written `summary` text. Without the affected
|
|
60
|
+
// file shown, two such events render as indistinguishable back-to-back lines - reads as a
|
|
61
|
+
// duplicate even though they're two real, distinct writes. See business/roadmap.md "Next".
|
|
62
|
+
return filtered.slice().reverse().map((e) => {
|
|
63
|
+
const files = Array.isArray(e.affectedFiles) ? e.affectedFiles : [];
|
|
64
|
+
const filesLabel = files.length > 0 ? `<span class="muted timeline-files">${escapeHtml(files.join(', '))}</span>` : '';
|
|
65
|
+
return `<div class="timeline-entry"><span class="tag">${escapeHtml(e.type)}</span>${escapeHtml(e.summary)} ${filesLabel}<div class="muted">${escapeHtml(e.timestamp)}</div></div>`;
|
|
66
|
+
}).join('\n');
|
|
67
|
+
}
|
|
68
|
+
// KPI summary for `session-load` events: how often this project's memory is actually read, and
|
|
69
|
+
// roughly how much it costs per read. `totalChars` on each event is what was actually loaded at
|
|
70
|
+
// that moment - avgTokens is a ~4-chars/token estimate (a standard approximation, not a real
|
|
71
|
+
// tokenizer count; load time has no access to one).
|
|
72
|
+
function renderSessionActivity(memoryRoot, projectRoot) {
|
|
73
|
+
const loads = readEvents(memoryRoot).filter((e) => e.type === 'session-load');
|
|
74
|
+
if (loads.length === 0)
|
|
75
|
+
return '<p class="muted">No session loads recorded yet.</p>';
|
|
76
|
+
const totalChars = loads.reduce((sum, e) => sum + (typeof e.totalChars === 'number' ? e.totalChars : 0), 0);
|
|
77
|
+
const avgTokens = Math.round(totalChars / loads.length / 4);
|
|
78
|
+
const last = loads[loads.length - 1];
|
|
79
|
+
const filterHref = `/project?path=${encodeURIComponent(projectRoot)}&type=session-load`;
|
|
80
|
+
return `<p><strong>${loads.length}</strong> session load(s) recorded · avg ~${avgTokens} tokens/load (est.) · last: ${formatAge(Date.now() - new Date(last.timestamp).getTime())}</p>
|
|
81
|
+
<p class="muted"><a href="${escapeHtml(filterHref)}">view raw load events</a></p>`;
|
|
82
|
+
}
|
|
83
|
+
export function renderProjectPage(projectRoot, options = {}) {
|
|
84
|
+
const memoryRoot = join(projectRoot, '.memoryintel');
|
|
85
|
+
const mentalModelPath = join(memoryRoot, 'context', 'currentMentalModel.md');
|
|
86
|
+
const mentalModel = existsSync(mentalModelPath) ? readFileSync(mentalModelPath, 'utf-8').trim() : '(no mental model yet)';
|
|
87
|
+
const tools = detectToolsWired(projectRoot);
|
|
88
|
+
const toolsHtml = tools.map((t) => `<span class="tag">${escapeHtml(t)}</span>`).join('') || '<span class="muted">no tools wired</span>';
|
|
89
|
+
const body = `
|
|
90
|
+
<a href="/">← All projects</a>
|
|
91
|
+
<div class="eyebrow" style="margin-top: 1rem;">${escapeHtml(basename(projectRoot))}</div>
|
|
92
|
+
<h1>${escapeHtml(basename(projectRoot))}</h1>
|
|
93
|
+
<div class="path">${escapeHtml(projectRoot)}</div>
|
|
94
|
+
|
|
95
|
+
<h2>Current understanding</h2>
|
|
96
|
+
<div class="mental-model">${escapeHtml(mentalModel)}</div>
|
|
97
|
+
|
|
98
|
+
<h2>Automation status</h2>
|
|
99
|
+
<p>${toolsHtml}</p>
|
|
100
|
+
|
|
101
|
+
<h2>Session activity</h2>
|
|
102
|
+
${renderSessionActivity(memoryRoot, projectRoot)}
|
|
103
|
+
|
|
104
|
+
<h2>Memory files</h2>
|
|
105
|
+
${renderFileBrowser(memoryRoot)}
|
|
106
|
+
|
|
107
|
+
<h2>Event timeline</h2>
|
|
108
|
+
${renderEventTimeline(memoryRoot, options.typeFilter)}
|
|
109
|
+
`;
|
|
110
|
+
return pageShell(projectRoot, body);
|
|
111
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
import { escapeHtml, pageShell, freshnessTier, daysSince } from './layout.js';
|
|
4
|
+
function mentalModelPreview(projectPath) {
|
|
5
|
+
const path = join(projectPath, '.memoryintel', 'context', 'currentMentalModel.md');
|
|
6
|
+
if (!existsSync(path))
|
|
7
|
+
return '(no mental model yet)';
|
|
8
|
+
return readFileSync(path, 'utf-8').trim().split('\n')[0] ?? '(empty)';
|
|
9
|
+
}
|
|
10
|
+
const DASHBOARD_CONTROLS = `
|
|
11
|
+
<div class="dashboard-controls">
|
|
12
|
+
<form method="POST" action="/stop">
|
|
13
|
+
<button type="submit" class="btn-stop">Stop dashboard</button>
|
|
14
|
+
</form>
|
|
15
|
+
<p class="muted">Stops the daemon and disables the dashboard for every Memory Intel project on this machine. <code>memoryintel dashboard enable</code> turns it back on, or it starts itself again the next time any project needs it.</p>
|
|
16
|
+
</div>
|
|
17
|
+
`;
|
|
18
|
+
export function renderRegistryPage(entries) {
|
|
19
|
+
const projectPaths = Object.keys(entries);
|
|
20
|
+
if (projectPaths.length === 0) {
|
|
21
|
+
return pageShell('Memory Intel', `
|
|
22
|
+
<div class="eyebrow">Memory Intel</div>
|
|
23
|
+
<h1>Project registry</h1>
|
|
24
|
+
<p class="empty-state">No projects registered yet. Run <code>memoryintel init</code> in a project to see it here.</p>
|
|
25
|
+
${DASHBOARD_CONTROLS}
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
const cards = projectPaths.map((path) => {
|
|
29
|
+
const entry = entries[path];
|
|
30
|
+
if (!existsSync(path)) {
|
|
31
|
+
return `<div class="card missing stale">
|
|
32
|
+
<strong>${escapeHtml(basename(path))}</strong>
|
|
33
|
+
<div class="path">${escapeHtml(path)}</div>
|
|
34
|
+
<p>(missing — this project's directory no longer exists)</p>
|
|
35
|
+
</div>`;
|
|
36
|
+
}
|
|
37
|
+
const preview = mentalModelPreview(path);
|
|
38
|
+
const tier = freshnessTier(daysSince(entry.lastSessionAt));
|
|
39
|
+
const tools = entry.toolsWired.map((t) => `<span class="tag">${escapeHtml(t)}</span>`).join('') || '<span class="muted">no tools wired</span>';
|
|
40
|
+
return `<div class="card ${tier}">
|
|
41
|
+
<h3><a href="/project?path=${encodeURIComponent(path)}">${escapeHtml(basename(path))}</a></h3>
|
|
42
|
+
<div class="path">${escapeHtml(path)}</div>
|
|
43
|
+
<p class="mental-model" style="font-size: 1rem; margin-top: 0.6rem;">${escapeHtml(preview)}</p>
|
|
44
|
+
<p class="muted">Last session: ${escapeHtml(entry.lastSessionAt)}</p>
|
|
45
|
+
<p>${tools}</p>
|
|
46
|
+
</div>`;
|
|
47
|
+
});
|
|
48
|
+
return pageShell('Memory Intel', `
|
|
49
|
+
<div class="eyebrow">Memory Intel</div>
|
|
50
|
+
<h1>Project registry</h1>
|
|
51
|
+
${cards.join('\n')}
|
|
52
|
+
${DASHBOARD_CONTROLS}
|
|
53
|
+
`);
|
|
54
|
+
}
|
package/dist/skill.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { USAGE } from './cli.js';
|
|
2
|
+
export function createSkillMarkdown() {
|
|
3
|
+
return `---
|
|
4
|
+
name: memoryintel
|
|
5
|
+
description: Give an AI coding agent persistent, cross-session project memory. Use when the user asks to set up persistent project memory, or when the current project already contains a .memoryintel/ directory.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Memory Intel
|
|
9
|
+
|
|
10
|
+
Memory Intel gives an AI coding agent durable, cross-session understanding of a project —
|
|
11
|
+
architecture, decisions, progress, and a running "mental model" — that survives new chats, new
|
|
12
|
+
agent sessions, and switching tools entirely (Claude Code, Cursor, Codex, Gemini CLI).
|
|
13
|
+
|
|
14
|
+
## First time in this project? (no \`.memoryintel/\` yet)
|
|
15
|
+
|
|
16
|
+
If the user asks to set up persistent project memory, run:
|
|
17
|
+
|
|
18
|
+
npx -y memoryintel init
|
|
19
|
+
|
|
20
|
+
This is a one-time step. It scaffolds \`.memoryintel/\` and installs pointer files for tools without
|
|
21
|
+
native hook support (Cursor, Codex, Gemini CLI, opencode) — safe to re-run later, it never
|
|
22
|
+
overwrites existing content. Claude Code automation doesn't come from this command at all: it comes
|
|
23
|
+
from the memoryintel plugin's own bundled hooks, active globally for every project once the plugin
|
|
24
|
+
itself is installed — \`init\` never touches \`.claude/settings.json\`.
|
|
25
|
+
|
|
26
|
+
## Already initialized? (\`.memoryintel/\` exists)
|
|
27
|
+
|
|
28
|
+
Read \`.memoryintel/instructions.md\` first — it is the authoritative, per-project guide. In short:
|
|
29
|
+
|
|
30
|
+
- At the start of a session: run \`memoryintel load [--domain technical|business|research]\` and
|
|
31
|
+
treat the output as project context.
|
|
32
|
+
- At the end of a session, only if your work changed real project understanding (new architecture,
|
|
33
|
+
decision, feature, integration, or roadmap item — never for formatting/typos): draft an
|
|
34
|
+
update-plan and run \`memoryintel update\`.
|
|
35
|
+
- If the user asks to turn the dashboard on or off: \`memoryintel dashboard enable\` /
|
|
36
|
+
\`memoryintel dashboard disable\`.
|
|
37
|
+
|
|
38
|
+
## Command reference
|
|
39
|
+
|
|
40
|
+
${USAGE}
|
|
41
|
+
## Sandboxed environments
|
|
42
|
+
|
|
43
|
+
If \`npx\`/a global \`memoryintel\` install aren't directly runnable, fall back to invoking the
|
|
44
|
+
package's built CLI directly: \`node "$(npm root -g)/memoryintel/dist/cli.js" <command>\`.
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const STARTER_FILES = [
|
|
2
|
+
{ relPath: 'context/projectBrief.md', headings: ['Overview'] },
|
|
3
|
+
{ relPath: 'context/objectives.md', headings: ['Objectives'] },
|
|
4
|
+
{ relPath: 'context/activeContext.md', headings: ['Current Focus'] },
|
|
5
|
+
{ relPath: 'context/decisions.md', headings: ['Decisions Log'] },
|
|
6
|
+
{ relPath: 'context/progress.md', headings: ['Status'] },
|
|
7
|
+
{ relPath: 'context/learnings.md', headings: ['Learnings'] },
|
|
8
|
+
{ relPath: 'technical/architecture.md', headings: ['Overview', 'Components', 'Data Flow', 'Integrations'] },
|
|
9
|
+
{ relPath: 'technical/techContext.md', headings: ['Stack', 'Conventions', 'Environment'] },
|
|
10
|
+
{ relPath: 'technical/patterns.md', headings: ['Design Patterns', 'Anti-Patterns'] },
|
|
11
|
+
{ relPath: 'technical/integrations.md', headings: ['External Services', 'Internal Dependencies'] },
|
|
12
|
+
{ relPath: 'technical/infrastructure.md', headings: ['Deployment', 'Hosting', 'CI/CD'] },
|
|
13
|
+
{ relPath: 'business/productContext.md', headings: ['Product Overview', 'Users', 'Value Proposition'] },
|
|
14
|
+
{ relPath: 'business/roadmap.md', headings: ['Now', 'Next', 'Later'] },
|
|
15
|
+
{ relPath: 'business/stakeholders.md', headings: ['Team', 'External Stakeholders'] },
|
|
16
|
+
{ relPath: 'business/marketContext.md', headings: ['Market Overview', 'Competitors'] },
|
|
17
|
+
{ relPath: 'research/findings.md', headings: ['Key Findings'] },
|
|
18
|
+
{ relPath: 'research/references.md', headings: ['Sources'] },
|
|
19
|
+
{ relPath: 'research/hypotheses.md', headings: ['Open Hypotheses'] }
|
|
20
|
+
];
|
|
21
|
+
// No headings — always fully overwritten by `update`, never section-addressed.
|
|
22
|
+
export const MENTAL_MODEL_STARTER = '_No sessions yet._\n';
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Load and maintain persistent project memory automatically",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{ "hooks": [{ "type": "command", "command": "npx -y memoryintel load" }] }
|
|
6
|
+
],
|
|
7
|
+
"Stop": [
|
|
8
|
+
{ "hooks": [{ "type": "command", "command": "npx -y memoryintel check-stop" }] }
|
|
9
|
+
]
|
|
10
|
+
}
|
|
11
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memoryintel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Persistent, cross-session project memory for AI coding agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"memoryintel": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/adeeshsharma/memoryintel.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/adeeshsharma/memoryintel#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/adeeshsharma/memoryintel/issues"
|
|
16
|
+
},
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Adeesh Sharma",
|
|
19
|
+
"url": "https://github.com/adeeshsharma"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"ai-agent",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"cli",
|
|
25
|
+
"memory",
|
|
26
|
+
"context",
|
|
27
|
+
"skill",
|
|
28
|
+
"mcp"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"skills",
|
|
34
|
+
".claude-plugin",
|
|
35
|
+
"hooks"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json && node scripts/build-skill.js && node scripts/ensure-executable.mjs",
|
|
39
|
+
"build:skill": "node scripts/build-skill.js",
|
|
40
|
+
"build:skill:check": "node scripts/build-skill.js --check",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"prepublishOnly": "npm run build && npm test"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"typescript": "^5.5.0",
|
|
49
|
+
"vitest": "^2.0.0",
|
|
50
|
+
"@types/node": "^20.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memoryintel
|
|
3
|
+
description: Give an AI coding agent persistent, cross-session project memory. Use when the user asks to set up persistent project memory, or when the current project already contains a .memoryintel/ directory.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory Intel
|
|
7
|
+
|
|
8
|
+
Memory Intel gives an AI coding agent durable, cross-session understanding of a project —
|
|
9
|
+
architecture, decisions, progress, and a running "mental model" — that survives new chats, new
|
|
10
|
+
agent sessions, and switching tools entirely (Claude Code, Cursor, Codex, Gemini CLI).
|
|
11
|
+
|
|
12
|
+
## First time in this project? (no `.memoryintel/` yet)
|
|
13
|
+
|
|
14
|
+
If the user asks to set up persistent project memory, run:
|
|
15
|
+
|
|
16
|
+
npx -y memoryintel init
|
|
17
|
+
|
|
18
|
+
This is a one-time step. It scaffolds `.memoryintel/` and installs pointer files for tools without
|
|
19
|
+
native hook support (Cursor, Codex, Gemini CLI, opencode) — safe to re-run later, it never
|
|
20
|
+
overwrites existing content. Claude Code automation doesn't come from this command at all: it comes
|
|
21
|
+
from the memoryintel plugin's own bundled hooks, active globally for every project once the plugin
|
|
22
|
+
itself is installed — `init` never touches `.claude/settings.json`.
|
|
23
|
+
|
|
24
|
+
## Already initialized? (`.memoryintel/` exists)
|
|
25
|
+
|
|
26
|
+
Read `.memoryintel/instructions.md` first — it is the authoritative, per-project guide. In short:
|
|
27
|
+
|
|
28
|
+
- At the start of a session: run `memoryintel load [--domain technical|business|research]` and
|
|
29
|
+
treat the output as project context.
|
|
30
|
+
- At the end of a session, only if your work changed real project understanding (new architecture,
|
|
31
|
+
decision, feature, integration, or roadmap item — never for formatting/typos): draft an
|
|
32
|
+
update-plan and run `memoryintel update`.
|
|
33
|
+
- If the user asks to turn the dashboard on or off: `memoryintel dashboard enable` /
|
|
34
|
+
`memoryintel dashboard disable`.
|
|
35
|
+
|
|
36
|
+
## Command reference
|
|
37
|
+
|
|
38
|
+
Usage: memoryintel <command> [options]
|
|
39
|
+
|
|
40
|
+
Commands:
|
|
41
|
+
init [path] Initialize .memoryintel/ in the current or given directory
|
|
42
|
+
load [--domain <d>] Print resolved memory context to stdout
|
|
43
|
+
update <plan.toon|-> Apply an update-plan (file path, or - for stdin)
|
|
44
|
+
status Print a human-readable summary of current memory state
|
|
45
|
+
check-stop Stop-hook check: emit a JSON allow/block decision
|
|
46
|
+
dashboard <enable|disable> Turn the shared local dashboard on or off
|
|
47
|
+
daemon start Run the dashboard daemon in the foreground (usually auto-started)
|
|
48
|
+
|
|
49
|
+
An update-plan row may set kind=compress to compact an oversized section; update() only applies
|
|
50
|
+
such a row when its target file is currently git-clean.
|
|
51
|
+
|
|
52
|
+
## Sandboxed environments
|
|
53
|
+
|
|
54
|
+
If `npx`/a global `memoryintel` install aren't directly runnable, fall back to invoking the
|
|
55
|
+
package's built CLI directly: `node "$(npm root -g)/memoryintel/dist/cli.js" <command>`.
|