claude-memory-admin 1.0.1 → 1.2.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 +94 -15
- package/package.json +8 -2
- package/public/app.mjs +849 -408
- package/public/graph.mjs +32 -76
- package/public/index.html +34 -29
- package/public/markdown.mjs +74 -0
- package/public/styles.css +2 -439
- package/public/ui.mjs +309 -0
- package/server.mjs +82 -5
- package/src/instructions.mjs +123 -21
- package/src/model.mjs +4 -1
- package/src/mutate.mjs +355 -1
- package/src/parse.mjs +176 -0
- package/src/settings.mjs +120 -19
- package/src/stores.mjs +34 -4
package/public/ui.mjs
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
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
|
+
ok: 'bg-ok-surface text-ok',
|
|
77
|
+
project: 'bg-type-project-surface text-type-project',
|
|
78
|
+
feedback: 'bg-type-feedback-surface text-type-feedback',
|
|
79
|
+
user: 'bg-type-user-surface text-type-user',
|
|
80
|
+
reference: 'bg-type-reference-surface text-type-reference',
|
|
81
|
+
managed: 'bg-scope-managed-surface text-scope-managed',
|
|
82
|
+
scopeUser: 'bg-scope-user-surface text-scope-user',
|
|
83
|
+
scopeProject: 'bg-scope-project-surface text-scope-project',
|
|
84
|
+
scopeLocal: 'bg-scope-local-surface text-scope-local',
|
|
85
|
+
};
|
|
86
|
+
export const badge = (tone = 'neutral') => cx(badgeBase, pick(badgeTone, tone, 'neutral'));
|
|
87
|
+
|
|
88
|
+
const SCOPE_TONE = { managed: 'managed', user: 'scopeUser', project: 'scopeProject', local: 'scopeLocal' };
|
|
89
|
+
export const scopeBadge = (scope) => badge(SCOPE_TONE[scope] ?? 'neutral');
|
|
90
|
+
export const typeBadge = (type) => badge(badgeTone[type] ? type : 'neutral');
|
|
91
|
+
export const tabBadge = (warn) => cx('ml-1.5', badge(warn ? 'warn' : 'neutral'));
|
|
92
|
+
|
|
93
|
+
export const split = 'grid grid-cols-1 items-start gap-6 xl:grid-cols-[minmax(280px,380px)_1fr]';
|
|
94
|
+
export const detailPane = 'xl:sticky xl:top-4 xl:max-h-[calc(100vh-9rem)] xl:overflow-y-auto';
|
|
95
|
+
|
|
96
|
+
export const sectionLabel = 'mt-5 mb-2 font-mono text-2xs font-semibold uppercase tracking-widest text-fg-subtle first:mt-0';
|
|
97
|
+
export const note = 'text-xs text-fg-muted';
|
|
98
|
+
export const noteTight = 'mt-0 mb-2.5 text-xs text-fg-muted';
|
|
99
|
+
|
|
100
|
+
const memoryItemBase = 'mb-1.5 block w-full cursor-pointer rounded-md border bg-surface px-3 py-2.5 text-left transition-colors';
|
|
101
|
+
const memoryItemState = {
|
|
102
|
+
active: 'border-accent bg-accent-surface',
|
|
103
|
+
idle: 'border-line hover:border-accent-line',
|
|
104
|
+
};
|
|
105
|
+
export const memoryItem = (active) => cx(memoryItemBase, active ? memoryItemState.active : memoryItemState.idle);
|
|
106
|
+
export const memoryTop = 'mb-1 flex items-center gap-2';
|
|
107
|
+
export const memoryName = 'flex-1 truncate text-sm font-medium text-fg';
|
|
108
|
+
export const memoryDesc = 'line-clamp-2 text-xs text-fg-muted';
|
|
109
|
+
|
|
110
|
+
export const card = 'rounded-lg border border-line bg-surface p-5 shadow-card';
|
|
111
|
+
export const detailHead = 'mb-1 flex items-start gap-3';
|
|
112
|
+
export const detailTitle = 'flex-1 text-lg font-semibold tracking-tightest text-fg';
|
|
113
|
+
export const detailDesc = 'mt-0 mb-3.5 text-sm text-fg-muted';
|
|
114
|
+
|
|
115
|
+
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';
|
|
116
|
+
export const metaKey = 'text-fg-subtle';
|
|
117
|
+
export const metaValue = 'm-0 break-all text-fg';
|
|
118
|
+
|
|
119
|
+
export const prose = 'prose text-base text-fg';
|
|
120
|
+
export const proseDim = 'prose text-base text-fg opacity-45';
|
|
121
|
+
|
|
122
|
+
const wikilinkBase = 'rounded-sm border border-transparent px-1 font-medium';
|
|
123
|
+
const wikilinkState = {
|
|
124
|
+
live: 'cursor-pointer bg-accent-surface text-accent hover:border-accent-line',
|
|
125
|
+
dead: 'cursor-help bg-danger-surface text-danger line-through',
|
|
126
|
+
};
|
|
127
|
+
export const wikilink = (live) => cx(wikilinkBase, live ? wikilinkState.live : wikilinkState.dead);
|
|
128
|
+
export const indexLink = (live) => (live ? 'cursor-pointer' : 'cursor-help text-danger line-through decoration-danger');
|
|
129
|
+
|
|
130
|
+
export const linkRow = 'mt-1.5 flex flex-wrap gap-1.5';
|
|
131
|
+
export const linkSection = 'mt-5 border-t border-line pt-3.5';
|
|
132
|
+
const chipBase = 'rounded-full border px-2.5 py-0.5 font-mono text-2xs transition-colors';
|
|
133
|
+
const chipState = {
|
|
134
|
+
live: 'cursor-pointer border-line bg-surface-sunken text-fg hover:border-accent',
|
|
135
|
+
dead: 'cursor-default border-danger-line bg-danger-surface text-danger',
|
|
136
|
+
};
|
|
137
|
+
export const chip = (live) => cx(chipBase, live ? chipState.live : chipState.dead);
|
|
138
|
+
|
|
139
|
+
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';
|
|
140
|
+
const buttonTone = {
|
|
141
|
+
neutral: 'border-line bg-surface text-fg hover:border-line-strong hover:bg-surface-sunken',
|
|
142
|
+
primary: 'border-accent bg-accent text-on-accent hover:bg-accent-hover hover:border-accent-hover',
|
|
143
|
+
danger: 'border-danger-line bg-danger-surface text-danger hover:bg-danger hover:text-on-accent',
|
|
144
|
+
};
|
|
145
|
+
const buttonSize = {
|
|
146
|
+
md: cx(controlPad.md, 'text-sm'),
|
|
147
|
+
sm: cx(controlPad.sm, 'text-xs'),
|
|
148
|
+
};
|
|
149
|
+
export const button = ({ tone = 'neutral', size = 'md' } = {}) =>
|
|
150
|
+
cx(buttonBase, pick(buttonTone, tone, 'neutral'), pick(buttonSize, size, 'md'));
|
|
151
|
+
|
|
152
|
+
export const linkButton = 'cursor-pointer border-none bg-transparent p-0 text-xs text-accent underline hover:text-accent-hover';
|
|
153
|
+
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);
|
|
154
|
+
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';
|
|
155
|
+
|
|
156
|
+
export const segmentGroup = 'inline-flex flex-none items-center gap-px rounded-md border border-line bg-surface-sunken p-0.5';
|
|
157
|
+
const segmentBase = cx('cursor-pointer rounded-sm font-mono text-2xs uppercase tracking-widest transition-colors', controlPad.sm);
|
|
158
|
+
const segmentState = { on: 'bg-accent text-on-accent', off: 'text-fg-muted hover:text-fg' };
|
|
159
|
+
export const segment = (active) => cx(segmentBase, active ? segmentState.on : segmentState.off);
|
|
160
|
+
|
|
161
|
+
export const cardHeadRow = 'mb-4 flex items-center justify-between gap-3 border-b border-line pb-3';
|
|
162
|
+
|
|
163
|
+
const indexLineBase = 'grid grid-cols-[3rem_1fr] gap-3 rounded-sm px-1.5 py-px font-mono text-xs';
|
|
164
|
+
const indexLineTone = {
|
|
165
|
+
heading: 'font-bold text-accent',
|
|
166
|
+
index: 'text-fg',
|
|
167
|
+
text: 'text-fg-muted',
|
|
168
|
+
};
|
|
169
|
+
export const indexLine = ({ dangling = false, dropped = false } = {}) => cx(
|
|
170
|
+
indexLineBase,
|
|
171
|
+
dangling ? 'bg-danger-surface' : 'hover:bg-surface-sunken',
|
|
172
|
+
dropped && 'opacity-45',
|
|
173
|
+
);
|
|
174
|
+
export const indexLineNumber = 'text-right text-fg-subtle select-none';
|
|
175
|
+
export const indexLineText = ({ kind = 'text', clickable = false, dropped = false } = {}) => cx(
|
|
176
|
+
'whitespace-pre-wrap break-words',
|
|
177
|
+
pick(indexLineTone, kind, 'text'),
|
|
178
|
+
clickable && 'cursor-pointer hover:underline',
|
|
179
|
+
dropped && 'line-through decoration-line-strong',
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
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';
|
|
183
|
+
export const cutLabel = 'flex-none font-mono text-2xs whitespace-nowrap text-danger';
|
|
184
|
+
|
|
185
|
+
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';
|
|
186
|
+
const issueTone = { warn: 'border-l-warn', bad: 'border-l-danger' };
|
|
187
|
+
export const issue = (bad) => cx(issueBase, bad ? issueTone.bad : issueTone.warn);
|
|
188
|
+
export const issueBody = 'min-w-0 flex-1';
|
|
189
|
+
export const issueTitle = 'text-sm font-medium text-fg';
|
|
190
|
+
export const issueDetail = 'font-mono text-2xs break-words text-fg-muted';
|
|
191
|
+
|
|
192
|
+
export const cutSummary = 'mb-4 rounded-lg border border-danger-line bg-danger-surface px-4 py-3.5';
|
|
193
|
+
export const cutSummaryHead = 'mb-1 text-sm font-semibold text-danger';
|
|
194
|
+
|
|
195
|
+
export const meter = 'mb-5 rounded-lg border border-line bg-surface px-5 py-4';
|
|
196
|
+
export const meterTop = 'mb-2.5 flex items-baseline gap-2.5';
|
|
197
|
+
export const meterValue = 'font-mono text-2xl font-semibold tracking-tightest text-fg';
|
|
198
|
+
export const meterUnit = 'flex-1 text-sm text-fg-muted';
|
|
199
|
+
export const meterBar = 'h-2 overflow-hidden rounded-full border border-line bg-surface-sunken';
|
|
200
|
+
const meterFillTone = { ok: 'bg-ok', near: 'bg-warn', over: 'bg-danger' };
|
|
201
|
+
export const meterFill = (level) => cx('h-full transition-[width]', pick(meterFillTone, level, 'ok'));
|
|
202
|
+
export const meterNote = 'mt-2.5 text-xs text-fg-muted';
|
|
203
|
+
export const meterFacts = 'mt-3 flex flex-wrap gap-4 border-t border-line pt-3 font-mono text-2xs text-fg-muted';
|
|
204
|
+
export const meterFactValue = 'font-semibold text-fg';
|
|
205
|
+
|
|
206
|
+
export const pruneBar = 'mb-2.5 flex flex-wrap items-center gap-2.5';
|
|
207
|
+
export const pruneSpacer = 'flex-1';
|
|
208
|
+
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);
|
|
209
|
+
|
|
210
|
+
const pruneRowBase = 'mb-1 flex cursor-pointer items-center gap-2.5 rounded-md border px-3 py-2 transition-colors';
|
|
211
|
+
const pruneRowState = { on: 'border-danger-line bg-danger-surface', off: 'border-line bg-surface hover:border-accent-line' };
|
|
212
|
+
export const pruneRow = (on) => cx(pruneRowBase, on ? pruneRowState.on : pruneRowState.off);
|
|
213
|
+
export const pruneMain = 'min-w-0 flex-1';
|
|
214
|
+
export const pruneName = 'block text-sm font-medium text-fg';
|
|
215
|
+
export const pruneDesc = 'block truncate text-xs text-fg-muted';
|
|
216
|
+
export const pruneFacts = 'flex flex-none gap-3 font-mono text-2xs text-fg-muted';
|
|
217
|
+
export const pruneFact = (warn) => (warn ? 'text-warn' : 'text-fg-muted');
|
|
218
|
+
|
|
219
|
+
export const dupe = 'mb-2 rounded-lg border border-line bg-surface px-3.5 py-3';
|
|
220
|
+
export const dupeHead = 'mb-2 flex items-baseline gap-2 font-mono text-2xs text-fg-muted';
|
|
221
|
+
export const dupeScore = 'font-semibold text-warn';
|
|
222
|
+
export const dupePair = 'grid grid-cols-1 gap-2.5 md:grid-cols-2';
|
|
223
|
+
export const dupeSide = 'cursor-pointer rounded-md border border-line bg-surface-sunken px-3 py-2.5 transition-colors hover:border-accent';
|
|
224
|
+
export const dupeSideName = 'text-sm font-medium text-fg';
|
|
225
|
+
export const dupeActions = 'mt-2 flex flex-wrap justify-end gap-2';
|
|
226
|
+
export const dupeSideDesc = 'text-xs text-fg-muted';
|
|
227
|
+
|
|
228
|
+
export const contextMain = 'min-w-0';
|
|
229
|
+
export const contextTags = 'mb-1 flex flex-wrap gap-1.5';
|
|
230
|
+
export const contextFile = 'font-mono text-2xs break-words text-fg';
|
|
231
|
+
export const contextSize = 'font-mono text-2xs whitespace-nowrap text-fg-subtle';
|
|
232
|
+
export const contextRowButton = 'flex w-full cursor-pointer items-baseline justify-between gap-3.5 border-0 border-b border-line bg-transparent px-1 py-1.5 text-left transition-colors last:border-b-0 hover:bg-surface-sunken';
|
|
233
|
+
export const contextCaret = 'mr-1.5 inline-block font-mono text-2xs text-fg-subtle';
|
|
234
|
+
export const contextBody = 'mb-1.5 max-h-[28rem] overflow-y-auto rounded-md border border-line bg-surface-sunken px-3.5 py-2.5';
|
|
235
|
+
|
|
236
|
+
export const graphWrap = 'graph relative overflow-hidden rounded-lg border border-line bg-surface';
|
|
237
|
+
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';
|
|
238
|
+
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);
|
|
239
|
+
export const graphLabel = 'flex items-center gap-1.5 font-mono text-2xs text-fg-muted';
|
|
240
|
+
export const graphRange = 'w-20 accent-accent';
|
|
241
|
+
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';
|
|
242
|
+
export const graphLegendItem = 'flex items-center gap-1.5';
|
|
243
|
+
export const graphLegendSwatch = 'inline-block size-2 rounded-full';
|
|
244
|
+
|
|
245
|
+
export const modalBackdrop = 'fixed inset-0 z-100 flex items-center justify-center bg-scrim p-6';
|
|
246
|
+
export const modal = 'flex max-h-[82vh] w-full max-w-2xl flex-col rounded-xl border border-line bg-surface shadow-pop';
|
|
247
|
+
export const modalHead = 'px-6 pt-5';
|
|
248
|
+
export const modalTitle = 'mb-1 text-lg font-semibold tracking-tightest text-fg';
|
|
249
|
+
export const modalBody = 'overflow-y-auto px-6 py-3.5';
|
|
250
|
+
export const modalFoot = 'flex justify-end gap-2 border-t border-line px-6 pt-3.5 pb-4.5';
|
|
251
|
+
|
|
252
|
+
export const willBlock = 'mb-3.5';
|
|
253
|
+
export const willTitle = 'mb-1.5 font-mono text-2xs font-semibold uppercase tracking-widest text-fg-subtle';
|
|
254
|
+
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';
|
|
255
|
+
const willItemTone = { remove: 'border-l-2 border-l-danger', keep: 'border-l-2 border-l-warn' };
|
|
256
|
+
export const willItem = (tone) => cx(willItemBase, willItemTone[tone]);
|
|
257
|
+
|
|
258
|
+
export const cascadeHead = 'mb-1.5 flex items-baseline justify-between gap-2.5';
|
|
259
|
+
export const cascadeList = 'overflow-hidden rounded-md border border-line';
|
|
260
|
+
const cascadeRowBase = 'flex cursor-pointer items-start gap-2.5 border-b border-line px-3 py-2 transition-colors last:border-b-0';
|
|
261
|
+
const cascadeRowState = { on: 'bg-danger-surface', off: 'hover:bg-surface-sunken' };
|
|
262
|
+
export const cascadeRow = (on) => cx(cascadeRowBase, on ? cascadeRowState.on : cascadeRowState.off);
|
|
263
|
+
export const cascadeText = 'min-w-0';
|
|
264
|
+
export const cascadeName = 'block text-sm font-medium text-fg';
|
|
265
|
+
export const cascadeDetail = 'block font-mono text-2xs break-words text-fg-muted';
|
|
266
|
+
|
|
267
|
+
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);
|
|
268
|
+
export const textArea = cx('my-1 mb-1.5 block min-h-20 w-full resize-y rounded-md border border-line bg-surface-sunken font-mono text-xs leading-relaxed text-fg focus:border-accent focus:outline-none', controlPad.lg);
|
|
269
|
+
export const hookEditor = 'mb-1.5 rounded-md border border-accent-line bg-surface-sunken px-3 py-2.5';
|
|
270
|
+
export const hookEditorFoot = 'flex justify-end gap-2';
|
|
271
|
+
export const charCount = 'mb-3 flex items-baseline justify-between gap-3 font-mono text-2xs text-fg-subtle';
|
|
272
|
+
export const charCountOver = 'mb-3 flex items-baseline justify-between gap-3 font-mono text-2xs text-warn';
|
|
273
|
+
|
|
274
|
+
export const settingsKeyHead = 'mb-1.5 flex flex-wrap items-baseline gap-2.5';
|
|
275
|
+
export const settingsKeyName = 'font-mono text-sm font-semibold text-fg';
|
|
276
|
+
export const settingsEffective = 'rounded-sm bg-accent-surface px-1.5 py-px font-mono text-2xs break-all text-accent';
|
|
277
|
+
export const settingsLayerRow = 'flex flex-wrap items-baseline gap-2.5 border-b border-line py-1.5 last:border-b-0';
|
|
278
|
+
export const settingsLayerFile = 'min-w-0 flex-1 font-mono text-2xs break-all text-fg-subtle';
|
|
279
|
+
const settingsLayerValueBase = 'font-mono text-2xs break-all';
|
|
280
|
+
const settingsLayerValueState = { wins: 'text-fg', shadowed: 'text-fg-subtle line-through decoration-line-strong' };
|
|
281
|
+
export const settingsLayerValue = (wins) => cx(settingsLayerValueBase, wins ? settingsLayerValueState.wins : settingsLayerValueState.shadowed);
|
|
282
|
+
|
|
283
|
+
export const toastRoot = 'fixed bottom-5 left-1/2 z-200 flex -translate-x-1/2 flex-col items-center gap-2';
|
|
284
|
+
const toastBase = 'flex items-center gap-3 rounded-full px-4 py-2 text-sm shadow-pop';
|
|
285
|
+
const toastTone = { info: 'bg-fg text-canvas', error: 'bg-danger text-on-accent' };
|
|
286
|
+
export const toast = (error) => cx(toastBase, error ? toastTone.error : toastTone.info);
|
|
287
|
+
export const toastAction = cx('cursor-pointer rounded-full border border-current bg-transparent text-xs', controlPad.sm);
|
|
288
|
+
|
|
289
|
+
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';
|
|
290
|
+
export const searchTitle = 'flex-1 text-lg font-semibold tracking-tightest text-fg';
|
|
291
|
+
export const searchResults = 'px-7 pt-5 pb-16';
|
|
292
|
+
export const resultGroup = 'mb-6';
|
|
293
|
+
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';
|
|
294
|
+
export const resultTop = 'mb-1 flex items-center gap-2';
|
|
295
|
+
export const resultName = 'flex-1 text-sm font-medium text-fg';
|
|
296
|
+
export const resultSnippet = 'block text-xs text-fg-muted';
|
|
297
|
+
export const resultWhere = 'block font-mono text-2xs text-fg-subtle';
|
|
298
|
+
export const resultMark = 'rounded-sm bg-accent-surface px-px text-accent';
|
|
299
|
+
|
|
300
|
+
export const buttonSmall = button({ size: 'sm' });
|
|
301
|
+
export const buttonPrimarySmall = button({ tone: 'primary', size: 'sm' });
|
|
302
|
+
export const buttonDangerSmall = button({ tone: 'danger', size: 'sm' });
|
|
303
|
+
export const promptCaret = 'text-accent';
|
|
304
|
+
export const inlineCode = 'rounded-sm border border-line bg-surface-sunken px-1 font-mono text-xs text-accent';
|
|
305
|
+
export const okLine = 'm-0 text-base text-fg';
|
|
306
|
+
export const graphEmpty = 'p-10 text-center text-fg-muted';
|
|
307
|
+
export const graphNode = (highlighted) => (highlighted ? 'node hi' : 'node');
|
|
308
|
+
export const graphEdge = 'edge';
|
|
309
|
+
export const graphHit = 'hit';
|
package/server.mjs
CHANGED
|
@@ -10,16 +10,23 @@ import { spawn } from 'node:child_process';
|
|
|
10
10
|
|
|
11
11
|
import { projectsRoot, resolveRoot } from './src/projects.mjs';
|
|
12
12
|
import { buildStore } from './src/model.mjs';
|
|
13
|
-
import { resolveInstructions } from './src/instructions.mjs';
|
|
13
|
+
import { resolveGlobalInstructions, resolveInstructions } from './src/instructions.mjs';
|
|
14
|
+
import { settingsReport } from './src/settings.mjs';
|
|
14
15
|
import { listStores } from './src/stores.mjs';
|
|
15
16
|
import { forgetPath, rememberPath } from './src/pathcache.mjs';
|
|
16
17
|
import { searchAll } from './src/search.mjs';
|
|
17
18
|
import {
|
|
19
|
+
addIndexEntry,
|
|
20
|
+
addIndexEntryPreview,
|
|
18
21
|
deleteIndexLine,
|
|
19
22
|
deleteMemories,
|
|
20
23
|
deleteMemory,
|
|
21
24
|
deletePreview,
|
|
22
25
|
deleteProject,
|
|
26
|
+
editIndexHook,
|
|
27
|
+
mergeMemories,
|
|
28
|
+
mergePreview,
|
|
29
|
+
moveIndexEntry,
|
|
23
30
|
projectDeletePreview,
|
|
24
31
|
removeWikilink,
|
|
25
32
|
restoreMemory,
|
|
@@ -119,6 +126,40 @@ function requireStore(id) {
|
|
|
119
126
|
return store;
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
function storeProjectDir(store) {
|
|
130
|
+
// The global store is the user scope itself, which no project owns.
|
|
131
|
+
if (store.kind === 'global') return null;
|
|
132
|
+
if (store.kind !== 'auto') return store.projectPath || null;
|
|
133
|
+
return store.pathExists ? store.path : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The global store holds instructions, not memory, and its directory is ~/.claude
|
|
138
|
+
* itself. Every write endpoint below resolves a target inside `store.dir`, so
|
|
139
|
+
* without this guard a delete aimed at it would land on the user's CLAUDE.md and
|
|
140
|
+
* settings rather than on a memory file. It is read-only, and refused here once
|
|
141
|
+
* rather than in each of the fifteen handlers.
|
|
142
|
+
*/
|
|
143
|
+
export function refuseWritesToGlobal(store, method) {
|
|
144
|
+
if (store.kind === 'global' && method !== 'GET') {
|
|
145
|
+
throw new Error('The global store is read-only: it holds instruction files, not memory.');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A store with nothing to build a memory model from, in the shape the frontend expects. */
|
|
150
|
+
function emptyModel(store) {
|
|
151
|
+
return {
|
|
152
|
+
...store,
|
|
153
|
+
index: null,
|
|
154
|
+
memories: [],
|
|
155
|
+
graph: { nodes: [], edges: [], dangling: [] },
|
|
156
|
+
health: { orphans: [], referencedOnly: [], danglingIndex: [], danglingWikilinks: [], nameMismatches: [], missingFrontmatter: [], longHooks: [], issues: [], issueCount: 0 },
|
|
157
|
+
stats: null,
|
|
158
|
+
duplicates: [],
|
|
159
|
+
trash: [],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
122
163
|
async function handleApi(req, res, url) {
|
|
123
164
|
const segments = url.pathname.split('/').filter(Boolean); // ['api', 'projects', ...]
|
|
124
165
|
|
|
@@ -144,24 +185,30 @@ async function handleApi(req, res, url) {
|
|
|
144
185
|
}
|
|
145
186
|
|
|
146
187
|
const store = requireStore(decodeURIComponent(segments[2]));
|
|
188
|
+
refuseWritesToGlobal(store, req.method);
|
|
147
189
|
const dir = store.dir;
|
|
148
190
|
const action = segments.slice(3).join('/');
|
|
149
191
|
|
|
150
192
|
if (!action && req.method === 'GET') {
|
|
151
|
-
|
|
193
|
+
// buildStore reads every .md in the directory as a memory file, which for
|
|
194
|
+
// ~/.claude would present CLAUDE.md and its neighbours as memories.
|
|
195
|
+
return sendJson(res, 200, store.kind === 'global' ? emptyModel(store) : buildStore(store));
|
|
152
196
|
}
|
|
153
197
|
|
|
154
198
|
// What a session starting in this store's project would load as instructions.
|
|
155
199
|
// Read-only, and scoped to a directory the app already discovered rather than
|
|
156
200
|
// one named in the request.
|
|
157
201
|
if (action === 'instructions' && req.method === 'GET') {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
: store.projectPath;
|
|
202
|
+
if (store.kind === 'global') return sendJson(res, 200, resolveGlobalInstructions());
|
|
203
|
+
const projectDir = storeProjectDir(store);
|
|
161
204
|
if (!projectDir) return sendJson(res, 200, { projectDir: null, files: [], problems: [], excluded: [], totals: null });
|
|
162
205
|
return sendJson(res, 200, resolveInstructions(projectDir));
|
|
163
206
|
}
|
|
164
207
|
|
|
208
|
+
if (action === 'settings' && req.method === 'GET') {
|
|
209
|
+
return sendJson(res, 200, settingsReport({ projectDir: storeProjectDir(store) }));
|
|
210
|
+
}
|
|
211
|
+
|
|
165
212
|
if (action === 'delete-preview' && req.method === 'POST') {
|
|
166
213
|
const body = await readBody(req);
|
|
167
214
|
return sendJson(res, 200, deletePreview(dir, body.file));
|
|
@@ -209,6 +256,36 @@ async function handleApi(req, res, url) {
|
|
|
209
256
|
return sendJson(res, 200, forgetPath(store.slug));
|
|
210
257
|
}
|
|
211
258
|
|
|
259
|
+
if (action === 'index/hook' && req.method === 'POST') {
|
|
260
|
+
const body = await readBody(req);
|
|
261
|
+
return sendJson(res, 200, editIndexHook(dir, body));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (action === 'index/move' && req.method === 'POST') {
|
|
265
|
+
const body = await readBody(req);
|
|
266
|
+
return sendJson(res, 200, moveIndexEntry(dir, body));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (action === 'index/add-preview' && req.method === 'POST') {
|
|
270
|
+
const body = await readBody(req);
|
|
271
|
+
return sendJson(res, 200, addIndexEntryPreview(dir, body));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (action === 'index/add' && req.method === 'POST') {
|
|
275
|
+
const body = await readBody(req);
|
|
276
|
+
return sendJson(res, 200, addIndexEntry(dir, body));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (action === 'merge-preview' && req.method === 'POST') {
|
|
280
|
+
const body = await readBody(req);
|
|
281
|
+
return sendJson(res, 200, mergePreview(dir, body));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (action === 'merge' && req.method === 'POST') {
|
|
285
|
+
const body = await readBody(req);
|
|
286
|
+
return sendJson(res, 200, mergeMemories(dir, body));
|
|
287
|
+
}
|
|
288
|
+
|
|
212
289
|
if (action === 'index-line/delete' && req.method === 'POST') {
|
|
213
290
|
const body = await readBody(req);
|
|
214
291
|
return sendJson(res, 200, deleteIndexLine(dir, body.lineIndex, body.expectedText));
|
package/src/instructions.mjs
CHANGED
|
@@ -65,6 +65,23 @@ export function maskCode(text) {
|
|
|
65
65
|
// so the character before it has to be a boundary.
|
|
66
66
|
const IMPORT = /(^|[\s(<'"])@((?:~\/|\.{0,2}\/)?[A-Za-z0-9._~\-][A-Za-z0-9._~\-/]*)/g;
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Drop the punctuation that ended the sentence rather than the path.
|
|
70
|
+
*
|
|
71
|
+
* A dot is legal in a filename and so is inside the match, which means an import
|
|
72
|
+
* written at the end of a sentence - "conventions live in @extra.md." - swallows
|
|
73
|
+
* the full stop and resolves to a file nobody has. The same goes for an ellipsis.
|
|
74
|
+
* No path meaningfully ends in a dot, so a trailing run of them is prose.
|
|
75
|
+
*
|
|
76
|
+
* Nothing else can leak in: the only other punctuation the match accepts is
|
|
77
|
+
* `-`, `_`, `~` and `/`, none of which ends an English sentence, and a trailing
|
|
78
|
+
* slash is left alone because it says "directory", which is a real thing to have
|
|
79
|
+
* written and a real thing for the import to fail on.
|
|
80
|
+
*/
|
|
81
|
+
function trimTrailingProse(spec) {
|
|
82
|
+
return spec.replace(/\.+$/, '');
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
/** Every `@path` import in a file's text, with the offset it was found at. */
|
|
69
86
|
export function findImports(text) {
|
|
70
87
|
const masked = maskCode(text);
|
|
@@ -74,7 +91,10 @@ export function findImports(text) {
|
|
|
74
91
|
// "@team@example.com" opens with something that looks exactly like an
|
|
75
92
|
// import until the second @ arrives.
|
|
76
93
|
if (masked[end] === '@') continue;
|
|
77
|
-
|
|
94
|
+
const spec = trimTrailingProse(match[2]);
|
|
95
|
+
// "@." and "@..." are punctuation with an @ in front, not a path.
|
|
96
|
+
if (!spec) continue;
|
|
97
|
+
found.push({ spec, index: match.index + match[1].length });
|
|
78
98
|
}
|
|
79
99
|
return found;
|
|
80
100
|
}
|
|
@@ -313,19 +333,14 @@ function globToRegExp(pattern) {
|
|
|
313
333
|
}
|
|
314
334
|
|
|
315
335
|
/**
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
336
|
+
* Managed policy and user scope: what loads whichever project a session starts in.
|
|
337
|
+
*
|
|
338
|
+
* `home` is a parameter rather than a call to os.homedir() so this is a total
|
|
339
|
+
* function of where it is told to look, which is what makes it testable without a
|
|
340
|
+
* real home directory.
|
|
319
341
|
*/
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
const layers = settingsLayers({ projectDir });
|
|
323
|
-
const excludeFound = lookup(layers, 'claudeMdExcludes');
|
|
324
|
-
const excludes = Array.isArray(excludeFound?.value) ? excludeFound.value.map(String) : [];
|
|
325
|
-
const excluded = [];
|
|
326
|
-
|
|
327
|
-
const candidates = [];
|
|
328
|
-
candidates.push(entry(managedClaudeMd(), 'managed', 'claude-md'));
|
|
342
|
+
function userCandidates(home, layers) {
|
|
343
|
+
const candidates = [entry(managedClaudeMd(), 'managed', 'claude-md')];
|
|
329
344
|
|
|
330
345
|
const managedInline = lookup(layers, 'claudeMd');
|
|
331
346
|
const inline = typeof managedInline?.value === 'string' && managedInline.value.trim()
|
|
@@ -338,8 +353,12 @@ export function resolveInstructions(projectDir) {
|
|
|
338
353
|
for (const file of listRuleFiles(path.join(home, '.claude', 'rules'))) {
|
|
339
354
|
candidates.push(ruleEntry(file, 'user'));
|
|
340
355
|
}
|
|
356
|
+
return candidates;
|
|
357
|
+
}
|
|
341
358
|
|
|
342
|
-
|
|
359
|
+
/** Root-to-cwd, CLAUDE.md then CLAUDE.local.md at each level, then the project's own .claude. */
|
|
360
|
+
function projectCandidates(projectDir) {
|
|
361
|
+
const candidates = [];
|
|
343
362
|
for (const dir of ancestors(projectDir)) {
|
|
344
363
|
candidates.push(entry(path.join(dir, 'CLAUDE.md'), 'project', 'claude-md'));
|
|
345
364
|
candidates.push(entry(path.join(dir, 'CLAUDE.local.md'), 'local', 'claude-md'));
|
|
@@ -348,8 +367,24 @@ export function resolveInstructions(projectDir) {
|
|
|
348
367
|
for (const file of listRuleFiles(path.join(projectDir, '.claude', 'rules'))) {
|
|
349
368
|
candidates.push(ruleEntry(file, 'project'));
|
|
350
369
|
}
|
|
370
|
+
return candidates;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function readExcludes(layers) {
|
|
374
|
+
const found = lookup(layers, 'claudeMdExcludes');
|
|
375
|
+
return Array.isArray(found?.value) ? found.value.map(String) : [];
|
|
376
|
+
}
|
|
351
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Turn a candidate list into the resolved set: excludes applied, imports expanded,
|
|
380
|
+
* every file measured and every problem named.
|
|
381
|
+
*
|
|
382
|
+
* Both entry points go through here, so the whole-session view and the user-scope
|
|
383
|
+
* one cannot drift apart in what they count or what they consider broken.
|
|
384
|
+
*/
|
|
385
|
+
function finalize(candidates, { projectDir, excludes }) {
|
|
352
386
|
const matchers = excludes.map(globToRegExp);
|
|
387
|
+
const excluded = [];
|
|
353
388
|
const loaded = [];
|
|
354
389
|
const problems = [];
|
|
355
390
|
|
|
@@ -379,13 +414,6 @@ export function resolveInstructions(projectDir) {
|
|
|
379
414
|
}
|
|
380
415
|
}
|
|
381
416
|
|
|
382
|
-
// AGENTS.md is read by other tools, not by Claude Code, so one sitting next to
|
|
383
|
-
// an unrelated CLAUDE.md is a real and easy-to-miss divergence.
|
|
384
|
-
const agentsFile = path.join(projectDir, 'AGENTS.md');
|
|
385
|
-
if (fs.existsSync(agentsFile) && !loaded.some((item) => item.file === agentsFile)) {
|
|
386
|
-
problems.push({ kind: 'agents-md-not-imported', file: agentsFile, scope: 'project' });
|
|
387
|
-
}
|
|
388
|
-
|
|
389
417
|
for (const item of loaded) {
|
|
390
418
|
item.lines = item.text.split('\n').length;
|
|
391
419
|
item.bytes = Buffer.byteLength(item.text, 'utf8');
|
|
@@ -421,3 +449,77 @@ export function resolveInstructions(projectDir) {
|
|
|
421
449
|
},
|
|
422
450
|
};
|
|
423
451
|
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Markdown files sitting in ~/.claude that nothing in the chain reaches.
|
|
455
|
+
*
|
|
456
|
+
* A file next to CLAUDE.md looks load-bearing, and stops being so the moment the
|
|
457
|
+
* import that pulled it in is deleted or its content is inlined - which leaves
|
|
458
|
+
* nothing behind to say so. This is the same failure the unimported AGENTS.md
|
|
459
|
+
* check exists for, one directory up.
|
|
460
|
+
*
|
|
461
|
+
* Only the top level is read. The directories below it - plans, skills, projects,
|
|
462
|
+
* backups - are full of markdown that was never meant to be an instruction file,
|
|
463
|
+
* and walking into them would bury the one finding that matters.
|
|
464
|
+
*/
|
|
465
|
+
export function unreferencedUserFiles(home, loadedFiles) {
|
|
466
|
+
const dir = path.join(home, '.claude');
|
|
467
|
+
// Imports arrive already absolutised by resolveImport while candidates are
|
|
468
|
+
// joined from `home`, so both sides are resolved before they are compared.
|
|
469
|
+
const loaded = new Set(loadedFiles.map((item) => path.resolve(item.file)));
|
|
470
|
+
|
|
471
|
+
let entries;
|
|
472
|
+
try {
|
|
473
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
474
|
+
} catch {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return entries
|
|
479
|
+
.filter((item) => item.isFile() && item.name.endsWith('.md'))
|
|
480
|
+
.map((item) => path.resolve(dir, item.name))
|
|
481
|
+
.filter((file) => !loaded.has(file))
|
|
482
|
+
.sort()
|
|
483
|
+
.map((file) => ({ kind: 'unreferenced-user-file', file, scope: 'user' }));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Everything that would load for a session started in `projectDir`, in the order
|
|
488
|
+
* Claude Code loads it: broadest scope first, so the most specific instruction is
|
|
489
|
+
* the last thing read.
|
|
490
|
+
*/
|
|
491
|
+
export function resolveInstructions(projectDir) {
|
|
492
|
+
const layers = settingsLayers({ projectDir });
|
|
493
|
+
const candidates = [
|
|
494
|
+
...userCandidates(os.homedir(), layers),
|
|
495
|
+
...projectCandidates(projectDir),
|
|
496
|
+
];
|
|
497
|
+
const resolved = finalize(candidates, { projectDir, excludes: readExcludes(layers) });
|
|
498
|
+
|
|
499
|
+
// AGENTS.md is read by other tools, not by Claude Code, so one sitting next to
|
|
500
|
+
// an unrelated CLAUDE.md is a real and easy-to-miss divergence.
|
|
501
|
+
const agentsFile = path.join(projectDir, 'AGENTS.md');
|
|
502
|
+
if (fs.existsSync(agentsFile) && !resolved.files.some((item) => item.file === agentsFile)) {
|
|
503
|
+
resolved.problems.push({ kind: 'agents-md-not-imported', file: agentsFile, scope: 'project' });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return resolved;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* The user scope on its own: what every session on this machine pays for before a
|
|
511
|
+
* project is even chosen.
|
|
512
|
+
*
|
|
513
|
+
* This is the half of the chain no project owns, so it has nowhere else to be
|
|
514
|
+
* shown. Resolving it needs no project directory, which is what makes it visible
|
|
515
|
+
* on a machine where no project path could be recovered from a transcript.
|
|
516
|
+
*
|
|
517
|
+
* With no project there is no boundary for an import to resolve outside of, so
|
|
518
|
+
* nothing is marked external here - that check belongs to the whole-session view.
|
|
519
|
+
*/
|
|
520
|
+
export function resolveGlobalInstructions({ home = os.homedir(), settingsFile = null } = {}) {
|
|
521
|
+
const layers = settingsLayers(settingsFile ? { settingsFile } : {});
|
|
522
|
+
const resolved = finalize(userCandidates(home, layers), { projectDir: null, excludes: readExcludes(layers) });
|
|
523
|
+
resolved.problems.push(...unreferencedUserFiles(home, resolved.files));
|
|
524
|
+
return resolved;
|
|
525
|
+
}
|
package/src/model.mjs
CHANGED
|
@@ -256,6 +256,8 @@ export function listTrash(dir) {
|
|
|
256
256
|
...entries.map((e) => e.trashedFile),
|
|
257
257
|
parsed.indexTrashedFile,
|
|
258
258
|
parsed.backupFile,
|
|
259
|
+
parsed.indexBackupFile,
|
|
260
|
+
...(parsed.backups || []).map((b) => b.backupFile),
|
|
259
261
|
].filter(Boolean);
|
|
260
262
|
return {
|
|
261
263
|
...parsed,
|
|
@@ -263,7 +265,8 @@ export function listTrash(dir) {
|
|
|
263
265
|
files: entries,
|
|
264
266
|
label: parsed.label || parsed.name || parsed.memoryFile || parsed.id,
|
|
265
267
|
recordFile: name,
|
|
266
|
-
present: backups.length > 0
|
|
268
|
+
present: (backups.length > 0 || parsed.indexCreated === true)
|
|
269
|
+
&& backups.every((f) => fs.existsSync(path.join(trashPath, f))),
|
|
267
270
|
};
|
|
268
271
|
} catch {
|
|
269
272
|
return null;
|