claude-memory-admin 1.0.0 → 1.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/README.md +111 -9
- package/package.json +8 -2
- package/public/app.mjs +633 -362
- package/public/graph.mjs +32 -76
- package/public/index.html +34 -29
- package/public/markdown.mjs +74 -0
- package/public/styles.css +2 -402
- package/public/ui.mjs +291 -0
- package/server.mjs +61 -20
- package/src/instructions.mjs +423 -0
- package/src/model.mjs +44 -11
- package/src/mutate.mjs +17 -23
- package/src/pathcache.mjs +93 -0
- package/src/projects.mjs +120 -34
- package/src/search.mjs +13 -11
- package/src/settings.mjs +151 -0
- package/src/stats.mjs +105 -12
- package/src/stores.mjs +146 -0
package/public/ui.mjs
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
const cx = (...parts) => parts.filter(Boolean).join(' ');
|
|
2
|
+
|
|
3
|
+
const pick = (map, key, fallback) => map[key] ?? map[fallback];
|
|
4
|
+
|
|
5
|
+
const controlPad = {
|
|
6
|
+
sm: 'min-h-control-h-sm px-control-x-sm py-control-y-sm',
|
|
7
|
+
md: 'min-h-control-h-md px-control-x-md py-control-y-md',
|
|
8
|
+
lg: 'min-h-control-h-lg px-control-x-lg py-control-y-lg',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const shell = 'grid h-screen grid-cols-[280px_1fr] bg-canvas';
|
|
12
|
+
export const shellCollapsed = 'grid h-screen grid-cols-1 bg-canvas';
|
|
13
|
+
|
|
14
|
+
export const sidebar = 'flex flex-col overflow-hidden border-r border-line bg-surface';
|
|
15
|
+
export const sidebarHidden = 'hidden';
|
|
16
|
+
export const sidebarHead = 'border-b border-line p-4';
|
|
17
|
+
export const sidebarTitle = 'font-mono text-sm font-semibold tracking-tightest text-fg';
|
|
18
|
+
export const sidebarRoot = 'mt-1 mb-2.5 font-mono text-2xs break-all text-fg-muted';
|
|
19
|
+
export const sidebarList = 'flex-1 overflow-y-auto p-2';
|
|
20
|
+
export const sidebarGroup = 'px-2 pt-3 pb-1 font-mono text-2xs font-semibold uppercase tracking-widest text-fg-subtle';
|
|
21
|
+
export const sidebarFoot = 'border-t border-line p-4';
|
|
22
|
+
export const sidebarCredit = 'flex flex-col gap-1.5 text-2xs text-fg-subtle no-underline transition-colors hover:text-fg';
|
|
23
|
+
export const sidebarLogo = 'w-24 opacity-60 transition-opacity dark:invert';
|
|
24
|
+
|
|
25
|
+
export const toggleLabel = 'flex cursor-pointer items-center gap-1.5 text-xs text-fg-muted';
|
|
26
|
+
export const checkbox = 'accent-accent';
|
|
27
|
+
|
|
28
|
+
export const searchInput = cx('w-full rounded-md border border-line bg-surface-sunken font-mono text-xs text-fg placeholder:text-fg-subtle focus:border-accent focus:outline-none', controlPad.lg);
|
|
29
|
+
|
|
30
|
+
const storeItemBase = 'mb-0.5 block w-full cursor-pointer rounded-md border border-transparent px-2.5 py-2 text-left transition-colors';
|
|
31
|
+
const storeItemState = {
|
|
32
|
+
active: 'border-accent-line bg-accent-surface',
|
|
33
|
+
idle: 'hover:bg-surface-sunken',
|
|
34
|
+
empty: 'opacity-50 hover:bg-surface-sunken',
|
|
35
|
+
};
|
|
36
|
+
export const storeItem = ({ active = false, empty = false } = {}) =>
|
|
37
|
+
cx(storeItemBase, active ? storeItemState.active : empty ? storeItemState.empty : storeItemState.idle);
|
|
38
|
+
|
|
39
|
+
export const storeRow = 'flex items-baseline gap-2';
|
|
40
|
+
export const storeName = 'flex-1 truncate text-sm font-medium text-fg';
|
|
41
|
+
export const storeCount = 'font-mono text-2xs text-fg-subtle';
|
|
42
|
+
export const storePath = 'block truncate font-mono text-2xs text-fg-subtle';
|
|
43
|
+
|
|
44
|
+
const dotBase = 'inline-block size-1.5 flex-none rounded-full';
|
|
45
|
+
const dotTone = { ok: 'bg-ok', warn: 'bg-warn', none: 'bg-line-strong' };
|
|
46
|
+
export const dot = (tone) => cx(dotBase, pick(dotTone, tone, 'none'));
|
|
47
|
+
|
|
48
|
+
export const offMarker = 'rounded-sm bg-warn-surface px-1 py-px font-mono text-2xs uppercase tracking-widest text-warn';
|
|
49
|
+
export const rootSource = 'ml-1.5 rounded-sm bg-accent-surface px-1 py-px font-mono text-2xs uppercase tracking-widest text-accent';
|
|
50
|
+
|
|
51
|
+
export const main = 'overflow-y-auto';
|
|
52
|
+
export const emptyState = 'px-10 py-[15vh] text-center text-fg-muted';
|
|
53
|
+
export const emptyTitle = 'mb-2 text-xl font-semibold tracking-tightest text-fg';
|
|
54
|
+
|
|
55
|
+
export const projectHead = 'sticky top-0 z-10 border-b border-line bg-canvas px-7 pt-5';
|
|
56
|
+
export const projectHeadRow = 'flex items-center gap-3';
|
|
57
|
+
export const projectTitle = 'flex-1 text-xl font-semibold tracking-tightest text-fg';
|
|
58
|
+
export const projectSub = 'mt-1 mb-3.5 font-mono text-2xs break-all text-fg-muted [&>span]:block';
|
|
59
|
+
export const subNote = 'mt-0.5 text-2xs text-fg-subtle';
|
|
60
|
+
export const subWarn = 'mt-0.5 text-2xs text-warn';
|
|
61
|
+
|
|
62
|
+
export const tabBar = 'flex gap-0.5';
|
|
63
|
+
const tabBase = cx('-mb-px cursor-pointer border-b-2 font-mono text-xs uppercase tracking-widest transition-colors', controlPad.md);
|
|
64
|
+
const tabState = {
|
|
65
|
+
active: 'border-accent font-semibold text-fg',
|
|
66
|
+
idle: 'border-transparent text-fg-muted hover:text-fg',
|
|
67
|
+
};
|
|
68
|
+
export const tab = (active) => cx(tabBase, active ? tabState.active : tabState.idle);
|
|
69
|
+
|
|
70
|
+
export const tabContent = 'px-7 pt-6 pb-16';
|
|
71
|
+
|
|
72
|
+
const badgeBase = 'flex-none rounded-sm px-1.5 py-px font-mono text-2xs lowercase';
|
|
73
|
+
const badgeTone = {
|
|
74
|
+
neutral: 'bg-surface-sunken text-fg-muted',
|
|
75
|
+
warn: 'bg-warn-surface text-warn',
|
|
76
|
+
project: 'bg-type-project-surface text-type-project',
|
|
77
|
+
feedback: 'bg-type-feedback-surface text-type-feedback',
|
|
78
|
+
user: 'bg-type-user-surface text-type-user',
|
|
79
|
+
reference: 'bg-type-reference-surface text-type-reference',
|
|
80
|
+
managed: 'bg-scope-managed-surface text-scope-managed',
|
|
81
|
+
scopeUser: 'bg-scope-user-surface text-scope-user',
|
|
82
|
+
scopeProject: 'bg-scope-project-surface text-scope-project',
|
|
83
|
+
scopeLocal: 'bg-scope-local-surface text-scope-local',
|
|
84
|
+
};
|
|
85
|
+
export const badge = (tone = 'neutral') => cx(badgeBase, pick(badgeTone, tone, 'neutral'));
|
|
86
|
+
|
|
87
|
+
const SCOPE_TONE = { managed: 'managed', user: 'scopeUser', project: 'scopeProject', local: 'scopeLocal' };
|
|
88
|
+
export const scopeBadge = (scope) => badge(SCOPE_TONE[scope] ?? 'neutral');
|
|
89
|
+
export const typeBadge = (type) => badge(badgeTone[type] ? type : 'neutral');
|
|
90
|
+
export const tabBadge = (warn) => cx('ml-1.5', badge(warn ? 'warn' : 'neutral'));
|
|
91
|
+
|
|
92
|
+
export const split = 'grid grid-cols-1 items-start gap-6 xl:grid-cols-[minmax(280px,380px)_1fr]';
|
|
93
|
+
export const detailPane = 'xl:sticky xl:top-4 xl:max-h-[calc(100vh-9rem)] xl:overflow-y-auto';
|
|
94
|
+
|
|
95
|
+
export const sectionLabel = 'mt-5 mb-2 font-mono text-2xs font-semibold uppercase tracking-widest text-fg-subtle first:mt-0';
|
|
96
|
+
export const note = 'text-xs text-fg-muted';
|
|
97
|
+
export const noteTight = 'mt-0 mb-2.5 text-xs text-fg-muted';
|
|
98
|
+
|
|
99
|
+
const memoryItemBase = 'mb-1.5 block w-full cursor-pointer rounded-md border bg-surface px-3 py-2.5 text-left transition-colors';
|
|
100
|
+
const memoryItemState = {
|
|
101
|
+
active: 'border-accent bg-accent-surface',
|
|
102
|
+
idle: 'border-line hover:border-accent-line',
|
|
103
|
+
};
|
|
104
|
+
export const memoryItem = (active) => cx(memoryItemBase, active ? memoryItemState.active : memoryItemState.idle);
|
|
105
|
+
export const memoryTop = 'mb-1 flex items-center gap-2';
|
|
106
|
+
export const memoryName = 'flex-1 truncate text-sm font-medium text-fg';
|
|
107
|
+
export const memoryDesc = 'line-clamp-2 text-xs text-fg-muted';
|
|
108
|
+
|
|
109
|
+
export const card = 'rounded-lg border border-line bg-surface p-5 shadow-card';
|
|
110
|
+
export const detailHead = 'mb-1 flex items-start gap-3';
|
|
111
|
+
export const detailTitle = 'flex-1 text-lg font-semibold tracking-tightest text-fg';
|
|
112
|
+
export const detailDesc = 'mt-0 mb-3.5 text-sm text-fg-muted';
|
|
113
|
+
|
|
114
|
+
export const metaList = 'mb-4 grid grid-cols-[auto_1fr] gap-x-3.5 gap-y-1 border-y border-line py-3 font-mono text-2xs';
|
|
115
|
+
export const metaKey = 'text-fg-subtle';
|
|
116
|
+
export const metaValue = 'm-0 break-all text-fg';
|
|
117
|
+
|
|
118
|
+
export const prose = 'prose text-base text-fg';
|
|
119
|
+
export const proseDim = 'prose text-base text-fg opacity-45';
|
|
120
|
+
|
|
121
|
+
const wikilinkBase = 'rounded-sm border border-transparent px-1 font-medium';
|
|
122
|
+
const wikilinkState = {
|
|
123
|
+
live: 'cursor-pointer bg-accent-surface text-accent hover:border-accent-line',
|
|
124
|
+
dead: 'cursor-help bg-danger-surface text-danger line-through',
|
|
125
|
+
};
|
|
126
|
+
export const wikilink = (live) => cx(wikilinkBase, live ? wikilinkState.live : wikilinkState.dead);
|
|
127
|
+
export const indexLink = (live) => (live ? 'cursor-pointer' : 'cursor-help text-danger line-through decoration-danger');
|
|
128
|
+
|
|
129
|
+
export const linkRow = 'mt-1.5 flex flex-wrap gap-1.5';
|
|
130
|
+
export const linkSection = 'mt-5 border-t border-line pt-3.5';
|
|
131
|
+
const chipBase = 'rounded-full border px-2.5 py-0.5 font-mono text-2xs transition-colors';
|
|
132
|
+
const chipState = {
|
|
133
|
+
live: 'cursor-pointer border-line bg-surface-sunken text-fg hover:border-accent',
|
|
134
|
+
dead: 'cursor-default border-danger-line bg-danger-surface text-danger',
|
|
135
|
+
};
|
|
136
|
+
export const chip = (live) => cx(chipBase, live ? chipState.live : chipState.dead);
|
|
137
|
+
|
|
138
|
+
const buttonBase = 'inline-flex cursor-pointer items-center gap-1.5 rounded-md border font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-45';
|
|
139
|
+
const buttonTone = {
|
|
140
|
+
neutral: 'border-line bg-surface text-fg hover:border-line-strong hover:bg-surface-sunken',
|
|
141
|
+
primary: 'border-accent bg-accent text-on-accent hover:bg-accent-hover hover:border-accent-hover',
|
|
142
|
+
danger: 'border-danger-line bg-danger-surface text-danger hover:bg-danger hover:text-on-accent',
|
|
143
|
+
};
|
|
144
|
+
const buttonSize = {
|
|
145
|
+
md: cx(controlPad.md, 'text-sm'),
|
|
146
|
+
sm: cx(controlPad.sm, 'text-xs'),
|
|
147
|
+
};
|
|
148
|
+
export const button = ({ tone = 'neutral', size = 'md' } = {}) =>
|
|
149
|
+
cx(buttonBase, pick(buttonTone, tone, 'neutral'), pick(buttonSize, size, 'md'));
|
|
150
|
+
|
|
151
|
+
export const linkButton = 'cursor-pointer border-none bg-transparent p-0 text-xs text-accent underline hover:text-accent-hover';
|
|
152
|
+
export const iconButton = cx('float-right ml-2 cursor-pointer rounded-sm border border-line font-mono text-2xs leading-none text-fg-muted transition-colors hover:border-line-strong hover:text-fg', controlPad.sm);
|
|
153
|
+
export const expandHandle = 'fixed top-3.5 left-0 z-20 cursor-pointer rounded-r-md border border-l-0 border-line bg-surface px-1.5 py-2 font-mono text-2xs text-fg-muted transition-colors hover:text-fg';
|
|
154
|
+
|
|
155
|
+
export const segmentGroup = 'inline-flex flex-none items-center gap-px rounded-md border border-line bg-surface-sunken p-0.5';
|
|
156
|
+
const segmentBase = cx('cursor-pointer rounded-sm font-mono text-2xs uppercase tracking-widest transition-colors', controlPad.sm);
|
|
157
|
+
const segmentState = { on: 'bg-accent text-on-accent', off: 'text-fg-muted hover:text-fg' };
|
|
158
|
+
export const segment = (active) => cx(segmentBase, active ? segmentState.on : segmentState.off);
|
|
159
|
+
|
|
160
|
+
export const cardHeadRow = 'mb-4 flex items-center justify-between gap-3 border-b border-line pb-3';
|
|
161
|
+
|
|
162
|
+
const indexLineBase = 'grid grid-cols-[3rem_1fr] gap-3 rounded-sm px-1.5 py-px font-mono text-xs';
|
|
163
|
+
const indexLineTone = {
|
|
164
|
+
heading: 'font-bold text-accent',
|
|
165
|
+
index: 'text-fg',
|
|
166
|
+
text: 'text-fg-muted',
|
|
167
|
+
};
|
|
168
|
+
export const indexLine = ({ dangling = false, dropped = false } = {}) => cx(
|
|
169
|
+
indexLineBase,
|
|
170
|
+
dangling ? 'bg-danger-surface' : 'hover:bg-surface-sunken',
|
|
171
|
+
dropped && 'opacity-45',
|
|
172
|
+
);
|
|
173
|
+
export const indexLineNumber = 'text-right text-fg-subtle select-none';
|
|
174
|
+
export const indexLineText = ({ kind = 'text', clickable = false, dropped = false } = {}) => cx(
|
|
175
|
+
'whitespace-pre-wrap break-words',
|
|
176
|
+
pick(indexLineTone, kind, 'text'),
|
|
177
|
+
clickable && 'cursor-pointer hover:underline',
|
|
178
|
+
dropped && 'line-through decoration-line-strong',
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
export const cutLine = 'my-2.5 flex items-center gap-2.5 before:flex-1 before:border-t before:border-dashed before:border-danger after:flex-1 after:border-t after:border-dashed after:border-danger';
|
|
182
|
+
export const cutLabel = 'flex-none font-mono text-2xs whitespace-nowrap text-danger';
|
|
183
|
+
|
|
184
|
+
const issueBase = 'mb-2 flex items-start gap-3 rounded-md border border-l-2 border-line bg-surface px-3.5 py-2.5';
|
|
185
|
+
const issueTone = { warn: 'border-l-warn', bad: 'border-l-danger' };
|
|
186
|
+
export const issue = (bad) => cx(issueBase, bad ? issueTone.bad : issueTone.warn);
|
|
187
|
+
export const issueBody = 'min-w-0 flex-1';
|
|
188
|
+
export const issueTitle = 'text-sm font-medium text-fg';
|
|
189
|
+
export const issueDetail = 'font-mono text-2xs break-words text-fg-muted';
|
|
190
|
+
|
|
191
|
+
export const cutSummary = 'mb-4 rounded-lg border border-danger-line bg-danger-surface px-4 py-3.5';
|
|
192
|
+
export const cutSummaryHead = 'mb-1 text-sm font-semibold text-danger';
|
|
193
|
+
|
|
194
|
+
export const meter = 'mb-5 rounded-lg border border-line bg-surface px-5 py-4';
|
|
195
|
+
export const meterTop = 'mb-2.5 flex items-baseline gap-2.5';
|
|
196
|
+
export const meterValue = 'font-mono text-2xl font-semibold tracking-tightest text-fg';
|
|
197
|
+
export const meterUnit = 'flex-1 text-sm text-fg-muted';
|
|
198
|
+
export const meterBar = 'h-2 overflow-hidden rounded-full border border-line bg-surface-sunken';
|
|
199
|
+
const meterFillTone = { ok: 'bg-ok', near: 'bg-warn', over: 'bg-danger' };
|
|
200
|
+
export const meterFill = (level) => cx('h-full transition-[width]', pick(meterFillTone, level, 'ok'));
|
|
201
|
+
export const meterNote = 'mt-2.5 text-xs text-fg-muted';
|
|
202
|
+
export const meterFacts = 'mt-3 flex flex-wrap gap-4 border-t border-line pt-3 font-mono text-2xs text-fg-muted';
|
|
203
|
+
export const meterFactValue = 'font-semibold text-fg';
|
|
204
|
+
|
|
205
|
+
export const pruneBar = 'mb-2.5 flex flex-wrap items-center gap-2.5';
|
|
206
|
+
export const pruneSpacer = 'flex-1';
|
|
207
|
+
export const select = cx('cursor-pointer rounded-md border border-line bg-surface font-mono text-xs text-fg hover:border-line-strong focus:outline-none', controlPad.sm);
|
|
208
|
+
|
|
209
|
+
const pruneRowBase = 'mb-1 flex cursor-pointer items-center gap-2.5 rounded-md border px-3 py-2 transition-colors';
|
|
210
|
+
const pruneRowState = { on: 'border-danger-line bg-danger-surface', off: 'border-line bg-surface hover:border-accent-line' };
|
|
211
|
+
export const pruneRow = (on) => cx(pruneRowBase, on ? pruneRowState.on : pruneRowState.off);
|
|
212
|
+
export const pruneMain = 'min-w-0 flex-1';
|
|
213
|
+
export const pruneName = 'block text-sm font-medium text-fg';
|
|
214
|
+
export const pruneDesc = 'block truncate text-xs text-fg-muted';
|
|
215
|
+
export const pruneFacts = 'flex flex-none gap-3 font-mono text-2xs text-fg-muted';
|
|
216
|
+
export const pruneFact = (warn) => (warn ? 'text-warn' : 'text-fg-muted');
|
|
217
|
+
|
|
218
|
+
export const dupe = 'mb-2 rounded-lg border border-line bg-surface px-3.5 py-3';
|
|
219
|
+
export const dupeHead = 'mb-2 flex items-baseline gap-2 font-mono text-2xs text-fg-muted';
|
|
220
|
+
export const dupeScore = 'font-semibold text-warn';
|
|
221
|
+
export const dupePair = 'grid grid-cols-1 gap-2.5 md:grid-cols-2';
|
|
222
|
+
export const dupeSide = 'cursor-pointer rounded-md border border-line bg-surface-sunken px-3 py-2.5 transition-colors hover:border-accent';
|
|
223
|
+
export const dupeSideName = 'text-sm font-medium text-fg';
|
|
224
|
+
export const dupeSideDesc = 'text-xs text-fg-muted';
|
|
225
|
+
|
|
226
|
+
export const contextRow = 'flex items-baseline justify-between gap-3.5 border-b border-line px-1 py-1.5 last:border-b-0';
|
|
227
|
+
export const contextMain = 'min-w-0';
|
|
228
|
+
export const contextTags = 'mb-1 flex flex-wrap gap-1.5';
|
|
229
|
+
export const contextFile = 'font-mono text-2xs break-words text-fg';
|
|
230
|
+
export const contextSize = 'font-mono text-2xs whitespace-nowrap text-fg-subtle';
|
|
231
|
+
|
|
232
|
+
export const graphWrap = 'graph relative overflow-hidden rounded-lg border border-line bg-surface';
|
|
233
|
+
export const graphToolbar = 'absolute top-3 right-3.5 flex items-center gap-1.5 rounded-md border border-line bg-surface px-2 py-1.5';
|
|
234
|
+
export const graphButton = cx('min-w-6 cursor-pointer rounded-sm border border-line bg-transparent font-mono text-xs text-fg-muted transition-colors hover:border-line-strong hover:text-fg', controlPad.sm);
|
|
235
|
+
export const graphLabel = 'flex items-center gap-1.5 font-mono text-2xs text-fg-muted';
|
|
236
|
+
export const graphRange = 'w-20 accent-accent';
|
|
237
|
+
export const graphLegend = 'absolute bottom-3 left-3.5 flex flex-wrap gap-3 rounded-md border border-line bg-surface px-2.5 py-1.5 font-mono text-2xs text-fg-muted';
|
|
238
|
+
export const graphLegendItem = 'flex items-center gap-1.5';
|
|
239
|
+
export const graphLegendSwatch = 'inline-block size-2 rounded-full';
|
|
240
|
+
|
|
241
|
+
export const modalBackdrop = 'fixed inset-0 z-100 flex items-center justify-center bg-scrim p-6';
|
|
242
|
+
export const modal = 'flex max-h-[82vh] w-full max-w-2xl flex-col rounded-xl border border-line bg-surface shadow-pop';
|
|
243
|
+
export const modalHead = 'px-6 pt-5';
|
|
244
|
+
export const modalTitle = 'mb-1 text-lg font-semibold tracking-tightest text-fg';
|
|
245
|
+
export const modalBody = 'overflow-y-auto px-6 py-3.5';
|
|
246
|
+
export const modalFoot = 'flex justify-end gap-2 border-t border-line px-6 pt-3.5 pb-4.5';
|
|
247
|
+
|
|
248
|
+
export const willBlock = 'mb-3.5';
|
|
249
|
+
export const willTitle = 'mb-1.5 font-mono text-2xs font-semibold uppercase tracking-widest text-fg-subtle';
|
|
250
|
+
const willItemBase = 'mb-1 rounded-md border border-line bg-surface-sunken px-2.5 py-1.5 font-mono text-2xs break-words text-fg-muted';
|
|
251
|
+
const willItemTone = { remove: 'border-l-2 border-l-danger', keep: 'border-l-2 border-l-warn' };
|
|
252
|
+
export const willItem = (tone) => cx(willItemBase, willItemTone[tone]);
|
|
253
|
+
|
|
254
|
+
export const cascadeHead = 'mb-1.5 flex items-baseline justify-between gap-2.5';
|
|
255
|
+
export const cascadeList = 'overflow-hidden rounded-md border border-line';
|
|
256
|
+
const cascadeRowBase = 'flex cursor-pointer items-start gap-2.5 border-b border-line px-3 py-2 transition-colors last:border-b-0';
|
|
257
|
+
const cascadeRowState = { on: 'bg-danger-surface', off: 'hover:bg-surface-sunken' };
|
|
258
|
+
export const cascadeRow = (on) => cx(cascadeRowBase, on ? cascadeRowState.on : cascadeRowState.off);
|
|
259
|
+
export const cascadeText = 'min-w-0';
|
|
260
|
+
export const cascadeName = 'block text-sm font-medium text-fg';
|
|
261
|
+
export const cascadeDetail = 'block font-mono text-2xs break-words text-fg-muted';
|
|
262
|
+
|
|
263
|
+
export const pathInput = cx('my-1 mb-3 w-full rounded-md border border-line bg-surface-sunken font-mono text-xs text-fg focus:border-accent focus:outline-none', controlPad.lg);
|
|
264
|
+
|
|
265
|
+
export const toastRoot = 'fixed bottom-5 left-1/2 z-200 flex -translate-x-1/2 flex-col items-center gap-2';
|
|
266
|
+
const toastBase = 'flex items-center gap-3 rounded-full px-4 py-2 text-sm shadow-pop';
|
|
267
|
+
const toastTone = { info: 'bg-fg text-canvas', error: 'bg-danger text-on-accent' };
|
|
268
|
+
export const toast = (error) => cx(toastBase, error ? toastTone.error : toastTone.info);
|
|
269
|
+
export const toastAction = cx('cursor-pointer rounded-full border border-current bg-transparent text-xs', controlPad.sm);
|
|
270
|
+
|
|
271
|
+
export const searchHead = 'sticky top-0 z-10 flex items-center gap-3 border-b border-line bg-canvas px-7 pt-5 pb-3.5';
|
|
272
|
+
export const searchTitle = 'flex-1 text-lg font-semibold tracking-tightest text-fg';
|
|
273
|
+
export const searchResults = 'px-7 pt-5 pb-16';
|
|
274
|
+
export const resultGroup = 'mb-6';
|
|
275
|
+
export const result = 'mb-1.5 block w-full cursor-pointer rounded-md border border-line bg-surface px-3.5 py-2.5 text-left transition-colors hover:border-accent';
|
|
276
|
+
export const resultTop = 'mb-1 flex items-center gap-2';
|
|
277
|
+
export const resultName = 'flex-1 text-sm font-medium text-fg';
|
|
278
|
+
export const resultSnippet = 'block text-xs text-fg-muted';
|
|
279
|
+
export const resultWhere = 'block font-mono text-2xs text-fg-subtle';
|
|
280
|
+
export const resultMark = 'rounded-sm bg-accent-surface px-px text-accent';
|
|
281
|
+
|
|
282
|
+
export const buttonSmall = button({ size: 'sm' });
|
|
283
|
+
export const buttonPrimarySmall = button({ tone: 'primary', size: 'sm' });
|
|
284
|
+
export const buttonDangerSmall = button({ tone: 'danger', size: 'sm' });
|
|
285
|
+
export const promptCaret = 'text-accent';
|
|
286
|
+
export const inlineCode = 'rounded-sm border border-line bg-surface-sunken px-1 font-mono text-xs text-accent';
|
|
287
|
+
export const okLine = 'm-0 text-base text-fg';
|
|
288
|
+
export const graphEmpty = 'p-10 text-center text-fg-muted';
|
|
289
|
+
export const graphNode = (highlighted) => (highlighted ? 'node hi' : 'node');
|
|
290
|
+
export const graphEdge = 'edge';
|
|
291
|
+
export const graphHit = 'hit';
|
package/server.mjs
CHANGED
|
@@ -8,8 +8,11 @@ import path from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { projectsRoot, resolveRoot } from './src/projects.mjs';
|
|
12
|
+
import { buildStore } from './src/model.mjs';
|
|
13
|
+
import { resolveInstructions } from './src/instructions.mjs';
|
|
14
|
+
import { listStores } from './src/stores.mjs';
|
|
15
|
+
import { forgetPath, rememberPath } from './src/pathcache.mjs';
|
|
13
16
|
import { searchAll } from './src/search.mjs';
|
|
14
17
|
import {
|
|
15
18
|
deleteIndexLine,
|
|
@@ -106,71 +109,109 @@ function readBody(req) {
|
|
|
106
109
|
});
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
/**
|
|
113
|
+
* An id is only ever accepted if it names a store we actually discovered, which
|
|
114
|
+
* is what keeps a request from addressing a directory the app never found.
|
|
115
|
+
*/
|
|
116
|
+
function requireStore(id) {
|
|
117
|
+
const store = listStores(ROOT).find((candidate) => candidate.id === id);
|
|
118
|
+
if (!store) throw new Error(`Unknown store: ${id}`);
|
|
119
|
+
return store;
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
async function handleApi(req, res, url) {
|
|
117
123
|
const segments = url.pathname.split('/').filter(Boolean); // ['api', 'projects', ...]
|
|
118
124
|
|
|
119
|
-
if (segments.length === 2 && segments[1] === '
|
|
120
|
-
|
|
125
|
+
if (segments.length === 2 && segments[1] === 'stores' && req.method === 'GET') {
|
|
126
|
+
const origin = resolveRoot();
|
|
127
|
+
return sendJson(res, 200, {
|
|
128
|
+
root: ROOT,
|
|
129
|
+
// Which settings layer chose this store, so a non-default one is visible
|
|
130
|
+
// rather than something the user has to infer from unfamiliar contents.
|
|
131
|
+
rootSource: origin.path === ROOT ? origin.source : 'flag',
|
|
132
|
+
rootFile: origin.path === ROOT ? origin.file : null,
|
|
133
|
+
rootWarning: origin.invalid,
|
|
134
|
+
stores: listStores(ROOT),
|
|
135
|
+
});
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
if (segments.length === 2 && segments[1] === 'search' && req.method === 'GET') {
|
|
124
139
|
return sendJson(res, 200, searchAll(ROOT, url.searchParams.get('q') || ''));
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
if (segments[1] !== '
|
|
142
|
+
if (segments[1] !== 'stores' || segments.length < 3) {
|
|
128
143
|
return sendJson(res, 404, { error: 'Unknown endpoint' });
|
|
129
144
|
}
|
|
130
145
|
|
|
131
|
-
const
|
|
146
|
+
const store = requireStore(decodeURIComponent(segments[2]));
|
|
147
|
+
const dir = store.dir;
|
|
132
148
|
const action = segments.slice(3).join('/');
|
|
133
149
|
|
|
134
150
|
if (!action && req.method === 'GET') {
|
|
135
|
-
return sendJson(res, 200,
|
|
151
|
+
return sendJson(res, 200, buildStore(store));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// What a session starting in this store's project would load as instructions.
|
|
155
|
+
// Read-only, and scoped to a directory the app already discovered rather than
|
|
156
|
+
// one named in the request.
|
|
157
|
+
if (action === 'instructions' && req.method === 'GET') {
|
|
158
|
+
const projectDir = store.kind === 'auto'
|
|
159
|
+
? (store.pathExists ? store.path : null)
|
|
160
|
+
: store.projectPath;
|
|
161
|
+
if (!projectDir) return sendJson(res, 200, { projectDir: null, files: [], problems: [], excluded: [], totals: null });
|
|
162
|
+
return sendJson(res, 200, resolveInstructions(projectDir));
|
|
136
163
|
}
|
|
137
164
|
|
|
138
165
|
if (action === 'delete-preview' && req.method === 'POST') {
|
|
139
166
|
const body = await readBody(req);
|
|
140
|
-
return sendJson(res, 200, deletePreview(
|
|
167
|
+
return sendJson(res, 200, deletePreview(dir, body.file));
|
|
141
168
|
}
|
|
142
169
|
|
|
143
170
|
if (action === 'delete' && req.method === 'POST') {
|
|
144
171
|
const body = await readBody(req);
|
|
145
|
-
return sendJson(res, 200, deleteMemory(
|
|
172
|
+
return sendJson(res, 200, deleteMemory(dir, body.file, body.alsoDelete || []));
|
|
146
173
|
}
|
|
147
174
|
|
|
148
175
|
if (action === 'delete-many' && req.method === 'POST') {
|
|
149
176
|
const body = await readBody(req);
|
|
150
|
-
return sendJson(res, 200, deleteMemories(
|
|
177
|
+
return sendJson(res, 200, deleteMemories(dir, body.files || [], { label: body.label || null }));
|
|
151
178
|
}
|
|
152
179
|
|
|
153
180
|
if (action === 'project/delete-preview' && req.method === 'POST') {
|
|
154
|
-
return sendJson(res, 200, projectDeletePreview(
|
|
181
|
+
return sendJson(res, 200, projectDeletePreview(dir));
|
|
155
182
|
}
|
|
156
183
|
|
|
157
184
|
if (action === 'project/delete' && req.method === 'POST') {
|
|
158
|
-
return sendJson(res, 200, deleteProject(
|
|
185
|
+
return sendJson(res, 200, deleteProject(dir));
|
|
159
186
|
}
|
|
160
187
|
|
|
161
188
|
if (action === 'wikilink/remove' && req.method === 'POST') {
|
|
162
189
|
const body = await readBody(req);
|
|
163
|
-
return sendJson(res, 200, removeWikilink(
|
|
190
|
+
return sendJson(res, 200, removeWikilink(dir, body.file, body.target));
|
|
164
191
|
}
|
|
165
192
|
|
|
166
193
|
if (action === 'restore' && req.method === 'POST') {
|
|
167
194
|
const body = await readBody(req);
|
|
168
|
-
return sendJson(res, 200, restoreMemory(
|
|
195
|
+
return sendJson(res, 200, restoreMemory(dir, body.id));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The only write outside a memory directory, and only ever on request: it
|
|
199
|
+
// records a path the user confirmed so the store keeps its name after Claude
|
|
200
|
+
// Code sweeps the transcripts that proved it.
|
|
201
|
+
if (action === 'path/remember' && req.method === 'POST') {
|
|
202
|
+
if (store.kind !== 'auto') throw new Error('Only project stores have a path to remember');
|
|
203
|
+
const body = await readBody(req);
|
|
204
|
+
return sendJson(res, 200, rememberPath(store.slug, body.path));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (action === 'path/forget' && req.method === 'POST') {
|
|
208
|
+
if (store.kind !== 'auto') throw new Error('Only project stores have a path to remember');
|
|
209
|
+
return sendJson(res, 200, forgetPath(store.slug));
|
|
169
210
|
}
|
|
170
211
|
|
|
171
212
|
if (action === 'index-line/delete' && req.method === 'POST') {
|
|
172
213
|
const body = await readBody(req);
|
|
173
|
-
return sendJson(res, 200, deleteIndexLine(
|
|
214
|
+
return sendJson(res, 200, deleteIndexLine(dir, body.lineIndex, body.expectedText));
|
|
174
215
|
}
|
|
175
216
|
|
|
176
217
|
return sendJson(res, 404, { error: 'Unknown endpoint' });
|