claude-memory-admin 1.5.2 → 1.7.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 +96 -129
- package/package.json +1 -1
- package/public/app.mjs +13 -34
- package/public/dialog.mjs +7 -0
- package/public/dialogs/bulk-delete.mjs +2 -1
- package/public/dialogs/trash.mjs +53 -0
- package/public/index.html +1 -0
- package/public/parts.mjs +10 -3
- package/public/state.mjs +31 -4
- package/public/store.mjs +13 -7
- package/public/styles.css +1 -1
- package/public/ui.mjs +9 -17
- package/public/views/cleanup.mjs +63 -0
- package/public/views/context.mjs +2 -1
- package/public/views/cost-issue.mjs +70 -0
- package/public/views/environment.mjs +14 -0
- package/public/views/header.mjs +105 -46
- package/public/views/issue.mjs +12 -20
- package/public/views/memories.mjs +5 -32
- package/public/views/memory-list.mjs +121 -0
- package/public/views/memory.mjs +32 -0
- package/public/views/path-check.mjs +2 -1
- package/public/views/rtk.mjs +148 -0
- package/public/views/search.mjs +4 -5
- package/public/views/segments.mjs +37 -0
- package/public/views/sessions.mjs +2 -2
- package/public/views/settings.mjs +2 -1
- package/public/views/stores.mjs +3 -3
- package/public/views/tools.mjs +76 -0
- package/public/views/worklist.mjs +33 -0
- package/server.mjs +9 -0
- package/src/rtk.mjs +179 -0
- package/public/views/health.mjs +0 -21
- package/public/views/prune.mjs +0 -191
- package/public/views/trash.mjs +0 -37
package/README.md
CHANGED
|
@@ -21,23 +21,23 @@ itself, and worth saying out loud.
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
-
###
|
|
24
|
+
### Cleanup: what MEMORY.md costs you and what is broken, in one list
|
|
25
25
|
|
|
26
|
-

|
|
27
27
|
|
|
28
|
-
###
|
|
28
|
+
### Memory: the list, the index and the graph
|
|
29
29
|
|
|
30
|
-

|
|
31
31
|
|
|
32
32
|
### Graph: see how memories link to each other
|
|
33
33
|
|
|
34
|
-

|
|
34
|
+

|
|
35
35
|
|
|
36
|
-
###
|
|
36
|
+
### Environment: what every session loads before a project is even chosen
|
|
37
37
|
|
|
38
|
-

|
|
39
39
|
|
|
40
|
-
There is a light theme and a dark one, toggled with `t`; the
|
|
40
|
+
There is a light theme and a dark one, toggled with `t`; the Memory shot above is
|
|
41
41
|
the light one and the rest are dark.
|
|
42
42
|
|
|
43
43
|
---
|
|
@@ -77,6 +77,17 @@ project is to the cliff, and makes it quick to get back under it.
|
|
|
77
77
|
|
|
78
78
|
## What it does
|
|
79
79
|
|
|
80
|
+
Everything lives under three tabs, each answering one question:
|
|
81
|
+
|
|
82
|
+
| Tab | The question | Holds |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| **Memory** | what is in here? | the memory list, `MEMORY.md`, the graph |
|
|
85
|
+
| **Cleanup** | what should I fix? | the load meter and one worst-first list of fixable things |
|
|
86
|
+
| **Environment** | what else does Claude load? | instructions, settings, sessions, tools - all read-only |
|
|
87
|
+
|
|
88
|
+
Undo is a button in the project header rather than a fourth tab, because it is a
|
|
89
|
+
safety net and not a place you browse.
|
|
90
|
+
|
|
80
91
|
- **Projects by real path.** The directories on disk are slugified cwds
|
|
81
92
|
(`-Users-me-repos-Blog`) and the slugification is lossy. The true path is
|
|
82
93
|
recovered from the `cwd` field in the session transcripts stored next to each
|
|
@@ -104,16 +115,16 @@ project is to the cliff, and makes it quick to get back under it.
|
|
|
104
115
|
with snippets and match highlighting. Press `/` to jump to it.
|
|
105
116
|
- **Read each memory** with its frontmatter as structured metadata and
|
|
106
117
|
`[[wikilinks]]` as clickable links. Dead links are struck through in red.
|
|
107
|
-
- **
|
|
108
|
-
- **Graph** the wikilinks between memories. Hovering dims everything
|
|
109
|
-
a neighbour, which is the only practical way to read a dense cluster.
|
|
118
|
+
- **Cleanup** (see below).
|
|
119
|
+
- **Graph** the wikilinks between memories, under Memory. Hovering dims everything
|
|
120
|
+
that is not a neighbour, which is the only practical way to read a dense cluster.
|
|
110
121
|
- **Where each memory came from.** Claude Code stamps `originSessionId` into a
|
|
111
122
|
memory's frontmatter, and the transcript it names sits next to the store until
|
|
112
123
|
the sweep takes it. The memory reads *written in "Release process notes", on
|
|
113
124
|
`main`* while that transcript is there, and says so plainly once it is gone:
|
|
114
125
|
a swept id is struck through in red, like a dead wikilink, because why the
|
|
115
126
|
memory exists can no longer be traced from anything on disk.
|
|
116
|
-
- **Sessions
|
|
127
|
+
- **Sessions** (under Environment): the transcripts beside a store, with the retention window drawn
|
|
117
128
|
the way MEMORY.md's cutoff is - each session a tick, the sweep line where it
|
|
118
129
|
falls. Under it, a tile per day shades how much work that day held, and
|
|
119
130
|
clicking one narrows the list to it; the grid spans the retention window
|
|
@@ -123,8 +134,8 @@ project is to the cliff, and makes it quick to get back under it.
|
|
|
123
134
|
slug and then the opening prompt; a session that names itself nowhere in the
|
|
124
135
|
part read is shown by id rather than given an invented name. Only the head of
|
|
125
136
|
each file is ever read, so a 200MB store of transcripts costs a quarter of a
|
|
126
|
-
second, and nothing
|
|
127
|
-
- **
|
|
137
|
+
second, and nothing here deletes one.
|
|
138
|
+
- **What Cleanup checks**: orphans, dangling pointers, broken wikilinks, files linked only
|
|
128
139
|
mid-sentence, `name` fields that disagree with the filename, two files claiming
|
|
129
140
|
one `name`, a file bulleted twice, a blank `description`, a `type` outside the
|
|
130
141
|
four documented ones, a memory that is frontmatter and little else, a hook that
|
|
@@ -133,19 +144,20 @@ project is to the cliff, and makes it quick to get back under it.
|
|
|
133
144
|
been swept, a project whose last proof of its own path is about to be, and
|
|
134
145
|
sessions that produced no memory at all while auto memory was on. An orphan
|
|
135
146
|
can be given the `MEMORY.md` bullet it is missing without leaving the page.
|
|
136
|
-
- **Every count is visible before you click.** Each
|
|
137
|
-
amber for something to tidy and red for something actively
|
|
138
|
-
sidebar gives each store a dot of its worst severity - memory,
|
|
139
|
-
settings together. Which project is in trouble is the first
|
|
140
|
-
you, not something you find by opening
|
|
147
|
+
- **Every count is visible before you click.** Each of the three tabs carries a
|
|
148
|
+
badge, coloured amber for something to tidy and red for something actively
|
|
149
|
+
broken, and the sidebar gives each store a dot of its worst severity - memory,
|
|
150
|
+
instructions and settings together. Which project is in trouble is the first
|
|
151
|
+
thing the app tells you, not something you find by opening every tab in turn.
|
|
141
152
|
- **Dates you can trust.** Claude Code stamps `modified` into frontmatter, but
|
|
142
153
|
only on files that already have some, and never adds frontmatter to a file
|
|
143
154
|
without it. Anything falling back to the file's mtime is labelled, because
|
|
144
155
|
mtime is reset by any copy or restore.
|
|
145
|
-
- **
|
|
146
|
-
*instructions*, which is the other half of the startup budget
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
- **Instructions** (under Environment): what a session starting in this project
|
|
157
|
+
would load as *instructions*, which is the other half of the startup budget
|
|
158
|
+
(see below).
|
|
159
|
+
- **Settings** (under Environment): every layer Claude Code would read, side by
|
|
160
|
+
side, with the value that wins and the ones it shadows (see below).
|
|
149
161
|
- **Delete with cascade**, always reversible.
|
|
150
162
|
|
|
151
163
|
Everything above works the same on both kinds of store. The load meter, graph,
|
|
@@ -155,17 +167,20 @@ memory directory is a `MEMORY.md` index plus topic files under the same 200-line
|
|
|
155
167
|
|
|
156
168
|
## Keeping MEMORY.md small
|
|
157
169
|
|
|
158
|
-
The **
|
|
159
|
-
session and, past the limit, silently stops loading.
|
|
170
|
+
The **Cleanup** tab exists because a bloated index costs tokens on every single
|
|
171
|
+
session and, past the limit, silently stops loading. It is one list: the load
|
|
172
|
+
meter, then everything worth doing something about, worst first, each row
|
|
173
|
+
carrying its own fix. A finding and its fix are never on different tabs.
|
|
160
174
|
|
|
161
175
|
- **Load meter**. How much of the 200-line / 25KB budget the index uses, and
|
|
162
176
|
which of the two is binding. Frontmatter and HTML comments are excluded,
|
|
163
177
|
because Claude Code strips those before loading.
|
|
164
|
-
- **The cutoff, drawn where it falls**. Past the limit, the MEMORY.md
|
|
165
|
-
line across the file and dims everything below it, and
|
|
166
|
-
that stopped being loaded
|
|
178
|
+
- **The cutoff, drawn where it falls**. Past the limit, the MEMORY.md segment rules
|
|
179
|
+
a line across the file and dims everything below it, and Cleanup lists the
|
|
180
|
+
memories that stopped being loaded at the top of the worklist, each with
|
|
181
|
+
**Move up**. Because the stripping shifts every line, the cutoff
|
|
167
182
|
is mapped back to real line numbers rather than counted in the loaded text. The
|
|
168
|
-
|
|
183
|
+
segment reads the index either **rendered** — headings, lists and clickable entries,
|
|
169
184
|
with pointers to files that do not exist struck through — or as **source**, with
|
|
170
185
|
line numbers. Either way the cutoff is drawn in the same place. The choice is
|
|
171
186
|
remembered.
|
|
@@ -178,16 +193,18 @@ session and, past the limit, silently stops loading.
|
|
|
178
193
|
- **Move above the cutoff**. Past the limit, an entry is on disk but invisible.
|
|
179
194
|
**Move up** relocates its bullet, and the indented lines under it, to the end
|
|
180
195
|
of any section that starts above the cutoff. It moves one entry rather than
|
|
181
|
-
making room, so whatever is now last drops below the line instead - the
|
|
196
|
+
making room, so whatever is now last drops below the line instead - the app
|
|
182
197
|
says so before you do it.
|
|
183
198
|
- **Possible overlap**. Pairs of memories ranked by shared *rare* vocabulary, to
|
|
184
199
|
surface the same lesson saved three times from three sessions. It is a hint,
|
|
185
|
-
not a verdict. **Merge** folds one into the other: the body
|
|
186
|
-
heading you name, every `[[wikilink]]` that pointed at the source
|
|
187
|
-
rather than broken, a link the survivor had to the source becomes
|
|
188
|
-
instead of a self-link, and the two index bullets collapse to one.
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
not a verdict, so it sorts last. **Merge** folds one into the other: the body
|
|
201
|
+
moves under a heading you name, every `[[wikilink]]` that pointed at the source
|
|
202
|
+
is repointed rather than broken, a link the survivor had to the source becomes
|
|
203
|
+
plain text instead of a self-link, and the two index bullets collapse to one.
|
|
204
|
+
|
|
205
|
+
Bulk pruning is not here, because it is browsing rather than fixing: the **Memory**
|
|
206
|
+
list sorts by age, size or inbound links, and **Select** turns on the checkboxes so
|
|
207
|
+
several can go as one restore point. One list of memories, not two.
|
|
191
208
|
|
|
192
209
|
Anthropic's own guidance for the index: one line per entry, detail in the topic
|
|
193
210
|
files, merge or drop stale entries.
|
|
@@ -195,13 +212,13 @@ files, merge or drop stale entries.
|
|
|
195
212
|
## The other half of the budget
|
|
196
213
|
|
|
197
214
|
`MEMORY.md` is not the only thing loaded at the start of every session. The
|
|
198
|
-
**
|
|
215
|
+
**Environment** tab's **Instructions** segment resolves what else is, in load order: managed policy, your
|
|
199
216
|
`~/.claude/CLAUDE.md` and `~/.claude/rules/`, every `CLAUDE.md` and
|
|
200
217
|
`CLAUDE.local.md` from the filesystem root down to the project, `.claude/CLAUDE.md`,
|
|
201
218
|
and `.claude/rules/`, with `@path` imports expanded and `claudeMdExcludes` applied.
|
|
202
219
|
|
|
203
220
|
Unlike `MEMORY.md`, none of it is truncated: `CLAUDE.md` files load in full
|
|
204
|
-
however long they are. So
|
|
221
|
+
however long they are. So it reports cost rather than a cliff, and separates
|
|
205
222
|
what every session pays for from the path-scoped rules that only load on a match.
|
|
206
223
|
|
|
207
224
|
It also finds the failures that leave a file silently doing nothing:
|
|
@@ -227,7 +244,7 @@ Backticks are respected, so a `` `@README` `` in your prose is not reported as a
|
|
|
227
244
|
import, and neither is an email address.
|
|
228
245
|
|
|
229
246
|
Every file listed opens in place, so you can read what actually loads without
|
|
230
|
-
leaving the
|
|
247
|
+
leaving the page. Nothing here is editable: the app never rewrites a `CLAUDE.md`.
|
|
231
248
|
|
|
232
249
|
### The user scope on its own
|
|
233
250
|
|
|
@@ -241,7 +258,7 @@ It holds instructions rather than memory, so it is read-only and has no MEMORY.m
|
|
|
241
258
|
graph or trash. Search does not reach into it.
|
|
242
259
|
|
|
243
260
|
This is re-derived from the documented resolution rules rather than reported by
|
|
244
|
-
Claude Code, and the
|
|
261
|
+
Claude Code, and the app says so. Run `/context` in a session for the ground
|
|
245
262
|
truth, or the `InstructionsLoaded` hook to log exactly what loaded and why.
|
|
246
263
|
|
|
247
264
|
## The one check that reads your code
|
|
@@ -252,7 +269,7 @@ it takes the paths a memory names in a code span and asks whether anything in th
|
|
|
252
269
|
repository still matches them. A memory whose `Foo.cs` was renamed two months ago
|
|
253
270
|
still reads as authoritative, and nothing else on this page can tell.
|
|
254
271
|
|
|
255
|
-
Turn it on from the
|
|
272
|
+
Turn it on from the bottom of the Cleanup tab, per store; the choice is remembered in the browser
|
|
256
273
|
and nothing is written to disk. It walks the project once, skipping `.git`,
|
|
257
274
|
`node_modules`, build output and the like, and matches by suffix - a memory that
|
|
258
275
|
says `Infrastructure/Reporting/Foo.cs` for a file that really lives under
|
|
@@ -261,13 +278,14 @@ alone reported seven real paths in ten as missing. Only a token with a real sour
|
|
|
261
278
|
extension is treated as a claim about a file, because memories are also full of
|
|
262
279
|
HTTP routes and type names that no file was ever going to match.
|
|
263
280
|
|
|
264
|
-
It is a pointer, not a verdict, and the
|
|
281
|
+
It is a pointer, not a verdict, and the app says so: a file that moved reads the
|
|
265
282
|
same as one the memory never got right.
|
|
266
283
|
|
|
267
284
|
## Which settings are actually in force
|
|
268
285
|
|
|
269
286
|
Five files can set the keys this tool cares about, and the one everybody edits,
|
|
270
|
-
`~/.claude/settings.json`, is the weakest of them. The **
|
|
287
|
+
`~/.claude/settings.json`, is the weakest of them. The **Environment** tab's
|
|
288
|
+
**Settings** segment reads all
|
|
271
289
|
five and shows, per key, the value that wins and the ones it shadows, struck
|
|
272
290
|
through, each labelled with the file it came from:
|
|
273
291
|
|
|
@@ -282,9 +300,37 @@ It also names the failures that are otherwise silent:
|
|
|
282
300
|
- A file that parses but is not an object, or cannot be read at all.
|
|
283
301
|
- An `autoMemoryDirectory` that is neither absolute nor `~/`-prefixed.
|
|
284
302
|
- A value Claude Code accepts the key of but not the number, like a
|
|
285
|
-
`cleanupPeriodDays` below 1:
|
|
286
|
-
|
|
287
|
-
|
|
303
|
+
`cleanupPeriodDays` below 1: it shows what is written *and* what applies.
|
|
304
|
+
|
|
305
|
+
Every segment of Environment is read-only. It reports what is configured; it never
|
|
306
|
+
writes a setting.
|
|
307
|
+
|
|
308
|
+
## Tools: what a session saved, next to what it cost
|
|
309
|
+
|
|
310
|
+
Every other meter here counts what a session *costs*. [rtk](https://github.com/rtk-ai/rtk)
|
|
311
|
+
counts the other half: it proxies commands like `grep`, `git` and `cargo test`,
|
|
312
|
+
strips their output down before it reaches the model, and keeps a ledger of what
|
|
313
|
+
it removed.
|
|
314
|
+
|
|
315
|
+
If `rtk` is on the PATH the server was started with, Environment grows a fourth
|
|
316
|
+
segment, **Tools**, showing three things:
|
|
317
|
+
|
|
318
|
+
- **This project** - what share of everything rtk read here never reached the
|
|
319
|
+
model. rtk scopes its ledger by working directory, so this is exactly the
|
|
320
|
+
commands run from this project's path.
|
|
321
|
+
- **Every project** - the same figure for the whole machine, with the last
|
|
322
|
+
thirty days called out separately, because a lifetime average hides a habit
|
|
323
|
+
that changed last week.
|
|
324
|
+
- **Left on the table** - commands that ran raw when rtk has a filter for them,
|
|
325
|
+
worst first, with rtk's own estimate of what each would have saved. The
|
|
326
|
+
estimates count what the filter would have stripped, not what the model would
|
|
327
|
+
have ignored.
|
|
328
|
+
|
|
329
|
+
The segment is the one place this app runs a program that is not itself, so it
|
|
330
|
+
stays off until you press **Read rtk stats**, and the answer is remembered. rtk
|
|
331
|
+
is only ever handed a working directory the app resolved on its own, never one
|
|
332
|
+
named in a request, and only its read-only `gain` and `discover` subcommands are
|
|
333
|
+
called. If rtk is not installed, the segment does not appear at all.
|
|
288
334
|
|
|
289
335
|
## Everything it writes is reversible
|
|
290
336
|
|
|
@@ -297,12 +343,14 @@ operation, and they are trashed and restored as a single step.
|
|
|
297
343
|
one restore point. Session transcripts (`*.jsonl`) and the project folder are
|
|
298
344
|
never touched, only the contents of `memory/`.
|
|
299
345
|
|
|
300
|
-
**Remove link** in the
|
|
346
|
+
**Remove link** in the Cleanup tab clears a `[[wikilink]]` whose target no longer
|
|
301
347
|
exists. The markup goes and the words stay, so `see [[gone]] for details` becomes
|
|
302
348
|
`see gone for details`.
|
|
303
349
|
|
|
304
350
|
Everything lands in `memory/.trash/` with a restore record and comes back from the
|
|
305
|
-
|
|
351
|
+
**Undo** control in the project header, one undo per operation however many files
|
|
352
|
+
it touched. Undo is not a place you browse, so it is a button with a count rather
|
|
353
|
+
than a tab of its own.
|
|
306
354
|
|
|
307
355
|
The app never writes a memory file of its own. It edits `MEMORY.md` a line at a
|
|
308
356
|
time - a hook rewritten, a bullet added or moved - and each of those keeps every
|
|
@@ -378,87 +426,6 @@ No bundler and no build step to run it: the backend is `node:http` plus `node:fs
|
|
|
378
426
|
and the frontend is plain ES modules the browser loads directly. Two runtime
|
|
379
427
|
dependencies, `marked` and `dompurify`, both only for rendering memory bodies safely.
|
|
380
428
|
|
|
381
|
-
Styling is the one thing that is compiled. `styles/app.css` is the Tailwind v4
|
|
382
|
-
source and `public/styles.css` is its committed output, so an installed copy is
|
|
383
|
-
ready to serve and never builds anything. After editing the source, run
|
|
384
|
-
`npm run build:css` and commit the result — CI rebuilds it and fails on drift.
|
|
385
|
-
|
|
386
|
-
| Path | Purpose |
|
|
387
|
-
| --- | --- |
|
|
388
|
-
| `bin/claude-memory-admin.mjs` | CLI entry point and argument parsing |
|
|
389
|
-
| `server.mjs` | HTTP server: static files + JSON API |
|
|
390
|
-
| `src/projects.mjs` | Project discovery, slug → real path resolution |
|
|
391
|
-
| `src/settings.mjs` | Layered reads of Claude Code's settings files, and the report behind the Settings tab |
|
|
392
|
-
| `src/pathcache.mjs` | The opt-in record of confirmed project paths |
|
|
393
|
-
| `src/sessions.mjs` | Session transcripts: bounded head reads, retention, provenance |
|
|
394
|
-
| `src/stores.mjs` | Store discovery: the user scope, auto memory and the three agent scopes |
|
|
395
|
-
| `src/instructions.mjs` | CLAUDE.md chain, `@` imports and rules resolution |
|
|
396
|
-
| `src/parse.mjs` | `MEMORY.md` and frontmatter parsers, wikilinks |
|
|
397
|
-
| `src/checks.mjs` | The consistency checks, as pure functions over parsed data |
|
|
398
|
-
| `src/pathcheck.mjs` | The opt-in check of paths a memory names, against the repo |
|
|
399
|
-
| `src/model.mjs` | Joins index, files, graph and health into one model |
|
|
400
|
-
| `src/stats.mjs` | Load-limit accounting and overlap detection |
|
|
401
|
-
| `src/search.mjs` | Full-text search across every store |
|
|
402
|
-
| `src/mutate.mjs` | Delete / edit / merge / restore, the only code that writes |
|
|
403
|
-
| `styles/app.css` | Tailwind source, compiled to `public/styles.css` |
|
|
404
|
-
| `public/` | Frontend |
|
|
405
|
-
|
|
406
|
-
Tests run against committed fixtures under `test/fixtures/`: a projects store
|
|
407
|
-
encoding the awkward shapes real memory directories contain, including one index
|
|
408
|
-
deliberately past the load limit, an `agents/` tree covering all three subagent
|
|
409
|
-
scopes, and an `instructions/` tree covering the import and glob edge cases. Your own
|
|
410
|
-
`~/.claude/projects` is additionally checked when it exists, always on a
|
|
411
|
-
throwaway copy.
|
|
412
|
-
|
|
413
|
-
### Releasing
|
|
414
|
-
|
|
415
|
-
Publishing runs in CI. You do not bump anything by hand.
|
|
416
|
-
|
|
417
|
-
Go to **Actions, Release, Run workflow**, pick `patch`, `minor` or `major`, and
|
|
418
|
-
run it. The workflow tests, bumps `package.json` and `package-lock.json`, commits
|
|
419
|
-
and tags the bump, pushes both, then publishes to npm.
|
|
420
|
-
|
|
421
|
-
If you would rather cut the version locally, that still works:
|
|
422
|
-
|
|
423
|
-
```bash
|
|
424
|
-
npm version patch
|
|
425
|
-
git push --follow-tags
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
Pushing a `v*` tag publishes whatever is in `package.json`, after checking the
|
|
429
|
-
two agree. Either route refuses to publish a version that is already on npm,
|
|
430
|
-
because npm never allows one to be replaced.
|
|
431
|
-
|
|
432
|
-
It needs one repository secret:
|
|
433
|
-
|
|
434
|
-
| Secret | Value |
|
|
435
|
-
| --- | --- |
|
|
436
|
-
| `NPM_TOKEN` | an npm **Automation** access token with publish rights |
|
|
437
|
-
|
|
438
|
-
Add it under *Settings, Secrets and variables, Actions, New repository secret*.
|
|
439
|
-
An Automation token is the right kind because it bypasses 2FA, which an
|
|
440
|
-
unattended workflow cannot satisfy.
|
|
441
|
-
|
|
442
|
-
Releases are published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements),
|
|
443
|
-
so every version carries a signed, verifiable record of the workflow run and
|
|
444
|
-
commit that built it. That needs the `id-token: write` permission the workflow
|
|
445
|
-
already requests, a public repository, and the `repository` field in
|
|
446
|
-
`package.json` pointing at this repo.
|
|
447
|
-
|
|
448
|
-
Two things that will bite if they apply to you: the workflow pushes the bump
|
|
449
|
-
commit to the default branch, so a branch protection rule that blocks pushes
|
|
450
|
-
will stop it; and the tag it pushes uses `GITHUB_TOKEN`, which by design does
|
|
451
|
-
not trigger other workflows, so there is no double publish.
|
|
452
|
-
|
|
453
|
-
### Screenshots and demo data
|
|
454
|
-
|
|
455
|
-
The screenshots come from an invented store, never a real one:
|
|
456
|
-
|
|
457
|
-
```bash
|
|
458
|
-
node scripts/demo-store.mjs /tmp/demo-store
|
|
459
|
-
npm start -- --root /tmp/demo-store
|
|
460
|
-
```
|
|
461
|
-
|
|
462
429
|
## License
|
|
463
430
|
|
|
464
431
|
MIT
|
package/package.json
CHANGED
package/public/app.mjs
CHANGED
|
@@ -1,33 +1,23 @@
|
|
|
1
|
-
import { renderGraph } from '/graph.mjs';
|
|
2
1
|
import { isDialogOpen } from '/dialog.mjs';
|
|
3
2
|
import { el, node } from '/dom.mjs';
|
|
4
3
|
import * as ui from '/ui.mjs';
|
|
5
4
|
import { state } from '/state.mjs';
|
|
6
5
|
import { toast } from '/api.mjs';
|
|
7
6
|
import { register, paint } from '/bus.mjs';
|
|
8
|
-
import { openStore, reloadStores
|
|
7
|
+
import { openStore, reloadStores } from '/store.mjs';
|
|
9
8
|
import { renderStores } from '/views/stores.mjs';
|
|
10
9
|
import { renderTabs, renderStoreHeader } from '/views/header.mjs';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { renderHealth } from '/views/health.mjs';
|
|
15
|
-
import { renderSessions } from '/views/sessions.mjs';
|
|
16
|
-
import { renderSettings } from '/views/settings.mjs';
|
|
17
|
-
import { renderTrash } from '/views/trash.mjs';
|
|
18
|
-
import { renderPrune } from '/views/prune.mjs';
|
|
10
|
+
import { renderMemory } from '/views/memory.mjs';
|
|
11
|
+
import { renderCleanup } from '/views/cleanup.mjs';
|
|
12
|
+
import { renderEnvironment } from '/views/environment.mjs';
|
|
19
13
|
import { renderSearch, scheduleSearch, clearSearch } from '/views/search.mjs';
|
|
20
14
|
import { openStoreDeleteDialog } from '/dialogs/store-delete.mjs';
|
|
15
|
+
import { openTrashDialog } from '/dialogs/trash.mjs';
|
|
21
16
|
|
|
22
17
|
const TAB_VIEWS = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
sessions: renderSessions,
|
|
27
|
-
index: renderIndex,
|
|
28
|
-
health: renderHealth,
|
|
29
|
-
settings: renderSettings,
|
|
30
|
-
trash: renderTrash,
|
|
18
|
+
memory: renderMemory,
|
|
19
|
+
cleanup: renderCleanup,
|
|
20
|
+
environment: renderEnvironment,
|
|
31
21
|
};
|
|
32
22
|
|
|
33
23
|
function renderView() {
|
|
@@ -42,21 +32,7 @@ function renderTab() {
|
|
|
42
32
|
const container = el('tab-content');
|
|
43
33
|
container.textContent = '';
|
|
44
34
|
const view = TAB_VIEWS[state.tab];
|
|
45
|
-
if (view)
|
|
46
|
-
if (state.tab !== 'graph') return;
|
|
47
|
-
|
|
48
|
-
const wrap = node('div', { id: 'graph-wrap', class: ui.graphWrap });
|
|
49
|
-
container.append(wrap);
|
|
50
|
-
renderGraph(wrap, state.store.graph, {
|
|
51
|
-
selected: state.selected,
|
|
52
|
-
spread: state.spread,
|
|
53
|
-
onSelect: (file) => selectMemory(file),
|
|
54
|
-
onSpreadChange: (value) => {
|
|
55
|
-
state.spread = value;
|
|
56
|
-
localStorage.setItem('graphSpread', String(value));
|
|
57
|
-
paint('tab');
|
|
58
|
-
},
|
|
59
|
-
});
|
|
35
|
+
if (view) view(container);
|
|
60
36
|
}
|
|
61
37
|
|
|
62
38
|
function applyCollapsed() {
|
|
@@ -89,7 +65,9 @@ function toggleTheme() {
|
|
|
89
65
|
function setCollapsed(value) {
|
|
90
66
|
state.collapsed = value;
|
|
91
67
|
applyCollapsed();
|
|
92
|
-
if (state.tab === '
|
|
68
|
+
if (state.tab === 'memory' && state.segment.memory === 'graph' && state.store) {
|
|
69
|
+
requestAnimationFrame(() => paint('tab'));
|
|
70
|
+
}
|
|
93
71
|
}
|
|
94
72
|
|
|
95
73
|
function applyStyles() {
|
|
@@ -119,6 +97,7 @@ async function init() {
|
|
|
119
97
|
el('collapse').addEventListener('click', () => setCollapsed(true));
|
|
120
98
|
el('expand').addEventListener('click', () => setCollapsed(false));
|
|
121
99
|
el('delete-project').addEventListener('click', openStoreDeleteDialog);
|
|
100
|
+
el('undo').addEventListener('click', openTrashDialog);
|
|
122
101
|
el('theme-toggle').addEventListener('click', toggleTheme);
|
|
123
102
|
const scheme = matchMedia('(prefers-color-scheme: dark)');
|
|
124
103
|
scheme.addEventListener('change', (event) => {
|
package/public/dialog.mjs
CHANGED
|
@@ -7,6 +7,13 @@ const TEXT_ENTRY = /^(text|search|url|email|tel|number|password)$/;
|
|
|
7
7
|
|
|
8
8
|
export const isDialogOpen = () => Boolean(root().firstElementChild);
|
|
9
9
|
|
|
10
|
+
export function closeDialog() {
|
|
11
|
+
const dialog = root().firstElementChild;
|
|
12
|
+
if (!dialog) return;
|
|
13
|
+
dialog.close();
|
|
14
|
+
dialog.remove();
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export function openDialog({ title, subtitle, body, actions, focus }) {
|
|
11
18
|
const previous = root().firstElementChild;
|
|
12
19
|
if (previous) {
|
|
@@ -46,7 +46,8 @@ export async function openBulkDeleteDialog(files) {
|
|
|
46
46
|
method: 'POST',
|
|
47
47
|
body: JSON.stringify({ files, label: `${memories.length} pruned memories` }),
|
|
48
48
|
});
|
|
49
|
-
state.
|
|
49
|
+
state.listSelection.clear();
|
|
50
|
+
state.selecting = false;
|
|
50
51
|
if (doomed.has(state.selected)) state.selected = null;
|
|
51
52
|
await openStore(state.storeId, { keepTab: true });
|
|
52
53
|
toast(`Deleted ${memories.length} memories`, {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as ui from '/ui.mjs';
|
|
2
|
+
import { node } from '/dom.mjs';
|
|
3
|
+
import { openDialog } from '/dialog.mjs';
|
|
4
|
+
import { state } from '/state.mjs';
|
|
5
|
+
import { restoreFromTrash } from '/store.mjs';
|
|
6
|
+
|
|
7
|
+
const INDEX_EDIT_LABELS = {
|
|
8
|
+
hook: 'MEMORY.md hook shortened',
|
|
9
|
+
move: 'MEMORY.md entry moved',
|
|
10
|
+
add: 'MEMORY.md entry added',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function describe(record) {
|
|
14
|
+
const when = String(record.deletedAt).replace('T', ' ').replace(/\..*$/, '');
|
|
15
|
+
if (record.kind === 'wikilink') return `unlinked in ${record.sourceFile} · ${when}`;
|
|
16
|
+
if (record.kind === 'index-edit') return `${INDEX_EDIT_LABELS[record.op] || 'MEMORY.md edited'} · ${when}`;
|
|
17
|
+
if (record.kind === 'merge') return `merged into ${record.into} · ${when} · ${record.backups?.length || 0} file(s) rewritten`;
|
|
18
|
+
return `${record.files.length} file(s)${record.indexTrashedFile ? ' + MEMORY.md' : ''} · ${when} · ${record.removedLines?.length || 0} index line(s) removed`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function openTrashDialog() {
|
|
22
|
+
const trash = state.store.trash;
|
|
23
|
+
const body = [];
|
|
24
|
+
|
|
25
|
+
if (!trash.length) {
|
|
26
|
+
body.push(node('p', { class: ui.note, text: 'Nothing to undo. Deleted memories and index edits land here and can be restored.' }));
|
|
27
|
+
} else {
|
|
28
|
+
body.push(node('p', { class: ui.noteTight, text: 'One undo per operation, however many files it touched. Everything lives in memory/.trash/ until you restore it.' }));
|
|
29
|
+
for (const record of trash) {
|
|
30
|
+
const detail = describe(record);
|
|
31
|
+
body.push(node('div', { class: ui.issue(!record.present) }, [
|
|
32
|
+
node('div', { class: ui.issueBody }, [
|
|
33
|
+
node('div', { class: ui.issueTitle, text: record.label || record.id }),
|
|
34
|
+
node('div', { class: ui.issueDetail, text: record.present ? detail : `${detail}, backup missing, cannot restore` }),
|
|
35
|
+
]),
|
|
36
|
+
record.present
|
|
37
|
+
? node('button', {
|
|
38
|
+
class: ui.buttonPrimarySmall,
|
|
39
|
+
text: 'Restore',
|
|
40
|
+
onclick: () => restoreFromTrash(record.id, { close: true }),
|
|
41
|
+
})
|
|
42
|
+
: null,
|
|
43
|
+
]));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return openDialog({
|
|
48
|
+
title: 'Undo',
|
|
49
|
+
subtitle: trash.length ? `${trash.length} restorable operation${trash.length === 1 ? '' : 's'}` : null,
|
|
50
|
+
body,
|
|
51
|
+
actions: [{ label: 'Close' }],
|
|
52
|
+
});
|
|
53
|
+
}
|
package/public/index.html
CHANGED
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
<header id="project-head" data-ui="projectHead">
|
|
57
57
|
<div data-ui="projectHeadRow">
|
|
58
58
|
<h2 id="project-title" data-ui="projectTitle"></h2>
|
|
59
|
+
<button id="undo" data-ui="buttonSmall" title="Restore something you deleted" hidden></button>
|
|
59
60
|
<button id="delete-project" data-ui="buttonDangerSmall">Delete all memory</button>
|
|
60
61
|
</div>
|
|
61
62
|
<p id="project-sub" data-ui="projectSub"></p>
|
package/public/parts.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ui from '/ui.mjs';
|
|
2
2
|
import { node } from '/dom.mjs';
|
|
3
|
-
import { state } from '/state.mjs';
|
|
3
|
+
import { state, SEGMENT_STORAGE } from '/state.mjs';
|
|
4
4
|
import { paint } from '/bus.mjs';
|
|
5
5
|
|
|
6
6
|
export function issue(title, detail, { bad = false, action, secondary } = {}) {
|
|
@@ -14,11 +14,18 @@ export function issue(title, detail, { bad = false, action, secondary } = {}) {
|
|
|
14
14
|
]);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function
|
|
18
|
-
state.tab =
|
|
17
|
+
export function goTo(tab, segment) {
|
|
18
|
+
state.tab = tab;
|
|
19
|
+
if (segment && SEGMENT_STORAGE[tab]) {
|
|
20
|
+
state.segment[tab] = segment;
|
|
21
|
+
localStorage.setItem(SEGMENT_STORAGE[tab], segment);
|
|
22
|
+
}
|
|
19
23
|
paint('tabs', 'tab');
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
export const isAt = (tab, segment) => state.tab === tab
|
|
27
|
+
&& (!segment || state.segment[tab] === segment);
|
|
28
|
+
|
|
22
29
|
export function cutMarker(cutoff) {
|
|
23
30
|
return node('div', { class: ui.cutLine }, [
|
|
24
31
|
node('span', { class: ui.cutLabel, text: `not loaded past here — ${cutoff.droppedLines} lines dropped on ${cutoff.by}` }),
|
package/public/state.mjs
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
+
export const SEGMENT_STORAGE = {
|
|
2
|
+
memory: 'navMemorySegment',
|
|
3
|
+
environment: 'navEnvironmentSegment',
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const SEGMENTS = {
|
|
7
|
+
memory: ['list', 'index', 'graph'],
|
|
8
|
+
environment: ['instructions', 'settings', 'sessions', 'tools'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const storedSegment = (tab) => {
|
|
12
|
+
const value = localStorage.getItem(SEGMENT_STORAGE[tab]);
|
|
13
|
+
return SEGMENTS[tab].includes(value) ? value : SEGMENTS[tab][0];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const LIST_SORTS = ['section', 'oldest', 'largest', 'unlinked', 'name'];
|
|
17
|
+
const storedListSort = () => {
|
|
18
|
+
const value = localStorage.getItem('memoryListSort');
|
|
19
|
+
return LIST_SORTS.includes(value) ? value : 'section';
|
|
20
|
+
};
|
|
21
|
+
|
|
1
22
|
export const state = {
|
|
2
23
|
stores: [],
|
|
24
|
+
tools: [],
|
|
3
25
|
storeId: null,
|
|
4
26
|
store: null,
|
|
5
|
-
tab: '
|
|
27
|
+
tab: 'memory',
|
|
28
|
+
segment: {
|
|
29
|
+
memory: storedSegment('memory'),
|
|
30
|
+
environment: storedSegment('environment'),
|
|
31
|
+
},
|
|
6
32
|
selected: null,
|
|
7
33
|
showAll: false,
|
|
8
34
|
collapsed: localStorage.getItem('sidebarCollapsed') === '1',
|
|
@@ -10,10 +36,11 @@ export const state = {
|
|
|
10
36
|
spread: Number(localStorage.getItem('graphSpread')) || 1.6,
|
|
11
37
|
query: '',
|
|
12
38
|
search: null,
|
|
13
|
-
|
|
14
|
-
|
|
39
|
+
listSort: storedListSort(),
|
|
40
|
+
listSelection: new Set(),
|
|
41
|
+
selecting: false,
|
|
15
42
|
indexView: localStorage.getItem('memoryIndexView') === 'source' ? 'source' : 'rendered',
|
|
16
|
-
aux: { instructions: null, settings: null, sessions: null, sessionDay: null },
|
|
43
|
+
aux: { instructions: null, settings: null, sessions: null, sessionDay: null, rtk: null },
|
|
17
44
|
pathCheck: null,
|
|
18
45
|
};
|
|
19
46
|
|