memgineering 0.1.0 → 0.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/CHANGELOG.md +79 -0
- package/assets/MEMGINEERING.md +18 -2
- package/assets/memgineering-memory/SKILL.md +55 -5
- package/dist/index.js +316 -38
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,85 @@ language the reader wants. The bilingual rule the monorepo applies to
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.2.0] — 2026-08-09
|
|
15
|
+
|
|
16
|
+
The record can say why, and you can see what a memory is actually worth.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`evidence <ref>`** — how much a memory has been used, and why it last
|
|
21
|
+
changed: how often recall surfaced it, how often anyone opened it, when it
|
|
22
|
+
was last read, how many times it has been revised, and the reason attached to
|
|
23
|
+
the most recent change. Every number comes from records that already existed;
|
|
24
|
+
nothing new is collected, and running it records nothing of its own — so
|
|
25
|
+
measuring a memory cannot change where `resurface` ranks it.
|
|
26
|
+
- **A standing for each memory** — `well-used`, `surfaced, never opened`, or
|
|
27
|
+
`never recalled` — computed from usage rather than declared. It is withheld
|
|
28
|
+
entirely when this machine has no recall history yet, since that log is local
|
|
29
|
+
state and a brain copied to a new machine starts empty.
|
|
30
|
+
- **`--reason` on every write verb.** `remember`, `exclude`, `unexclude` and
|
|
31
|
+
`undo` accept one for the first time; `revise`, `retire` and `unretire`
|
|
32
|
+
already did. The reason on an `undo` is usually the most valuable of the set.
|
|
33
|
+
- **`log` shows the reason** without needing `--verbose`.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **`revise --reason` and `retire --reason` were silently discarded.** Both
|
|
38
|
+
flags shipped in 0.1.0, and `retire --help` described the reason as "kept in
|
|
39
|
+
the record". It was not kept anywhere: only creating a brand-new note ever
|
|
40
|
+
read the field, so on the ordinary path — changing a note that already exists
|
|
41
|
+
— the text went nowhere. Reasons given to those commands are now recorded in
|
|
42
|
+
the operation ledger, where `log` and `evidence` can show them.
|
|
43
|
+
|
|
44
|
+
- **`recall` now names the language barrier** when a query cannot reach the
|
|
45
|
+
brain. Search matches text, so an English query never reaches Korean notes
|
|
46
|
+
however it is worded — and the old zero-result footer suggested broader
|
|
47
|
+
wording, the one remedy that cannot work. Four testers hit this and came
|
|
48
|
+
within one command of telling their user the brain held nothing on a subject
|
|
49
|
+
it had notes about. The warning also appears above results, because the
|
|
50
|
+
failure is rarely a clean zero: a couple of English scaffolding notes match
|
|
51
|
+
and look like an answer.
|
|
52
|
+
- **`use` with no argument now says which brain you are on**, and how it
|
|
53
|
+
resolved — by pointer, by containment, or by being the only one linked. Its
|
|
54
|
+
own help had always promised this; it reported only whether a pointer file
|
|
55
|
+
existed, and three testers went and read the config file by hand instead.
|
|
56
|
+
- **`open` no longer tells you to hand-edit frontmatter** when a note states no
|
|
57
|
+
conclusion. It points at `memgineering revise --claim`, which is the
|
|
58
|
+
sanctioned path — the previous hint contradicted the bolded rule against
|
|
59
|
+
editing brain files directly, and a hand edit is invisible to `undo`.
|
|
60
|
+
- **`evidence` no longer implies a well-used memory is a correct one.** A note
|
|
61
|
+
the repository flatly contradicted was described as backed by the record.
|
|
62
|
+
Usage is attendance, and where a memory contradicts another the output now
|
|
63
|
+
says plainly that usage cannot settle which is right.
|
|
64
|
+
- **`evidence` sees a contradiction declared by either note.** The `contradicts`
|
|
65
|
+
edge lives on whichever note a revise happened to touch, so the warning above
|
|
66
|
+
fired on only one of the two — and stayed silent on the other, which is the
|
|
67
|
+
side a reader is more likely to be trusting by mistake.
|
|
68
|
+
- **`evidence` divides the open rate by the number printed beside it.** It used
|
|
69
|
+
to divide by recalls while `surfaced` also counted resurfacings, so the two
|
|
70
|
+
figures and the percentage reconciled with none of each other.
|
|
71
|
+
- **`remember --reason` help disambiguated** — it records why you are writing
|
|
72
|
+
something down, not why the fact is true. One tester nearly filed the
|
|
73
|
+
substance of a note there, where recall would never show it.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- A reason that looks like it contains credential material is refused, with the
|
|
78
|
+
pattern named and the value never echoed. The ledger travels with the brain,
|
|
79
|
+
so an unscreened reason would become a permanent synced copy of a secret —
|
|
80
|
+
and the deny layer that screens notes does not look at the ledger. The write
|
|
81
|
+
itself is unaffected: rephrase, or drop the flag.
|
|
82
|
+
- Operation records carry a `rationale` field and `rule_version` is now 2.
|
|
83
|
+
Ledgers written by 0.1.x still read correctly; an older line simply reports
|
|
84
|
+
no reason given, which is what happened.
|
|
85
|
+
|
|
86
|
+
### Agent guidance
|
|
87
|
+
|
|
88
|
+
The bundled hub and the `memgineering-memory` skill now cover `evidence` and
|
|
89
|
+
ask for `--reason` on every write, including the rule that a newer memory does
|
|
90
|
+
not automatically win. **Restart your agent session after upgrading** so the
|
|
91
|
+
new guidance is loaded.
|
|
92
|
+
|
|
14
93
|
## [0.1.0] — 2026-08-09
|
|
15
94
|
|
|
16
95
|
First release. A brain is a folder of your own notes; every agent you use
|
package/assets/MEMGINEERING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: memgineering
|
|
3
3
|
description: Use whenever the user refers to something they told you before, asks what was decided, or tells you something worth keeping. The memory lives in their own folder and outlives this session; check it before answering from guesswork, and write to it when you learn something durable.
|
|
4
4
|
type: skill
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.2.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# memgineering
|
|
@@ -47,17 +47,33 @@ memgineering open <handle> --detail full # the whole note
|
|
|
47
47
|
`open` also takes a note's exact title or its path, so you do not need to
|
|
48
48
|
recall first when you already know what you want.
|
|
49
49
|
|
|
50
|
+
**Check the evidence before trusting an old memory,** or when two of them
|
|
51
|
+
disagree:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
memgineering evidence <handle>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
How often it has been surfaced, how often anyone read it, when it last changed
|
|
58
|
+
and why. **Newer does not win**: a rule that has stood for eleven months has
|
|
59
|
+
eleven months of nobody contradicting it behind it. Use whether it was actually
|
|
60
|
+
reached for, not its date.
|
|
61
|
+
|
|
50
62
|
**Remember when you learn something durable** — a decision, a constraint, a
|
|
51
63
|
correction the user made, how something actually works:
|
|
52
64
|
|
|
53
65
|
```
|
|
54
|
-
memgineering remember "deploys are manual — launchctl by hand, no CD"
|
|
66
|
+
memgineering remember "deploys are manual — launchctl by hand, no CD" --reason "watched it twice"
|
|
55
67
|
```
|
|
56
68
|
|
|
57
69
|
No approval step, by design. It is recorded and reversible (`memgineering
|
|
58
70
|
undo`), so the cost of a wrong entry is one command, not a permanent mistake.
|
|
59
71
|
Do not ask permission for ordinary observations — write them.
|
|
60
72
|
|
|
73
|
+
**Always pass `--reason`.** Every write verb takes it and it goes in the ledger.
|
|
74
|
+
It is the only part of a record that still means anything to whoever reads it
|
|
75
|
+
six months from now, and it is refused if it looks like it carries a credential.
|
|
76
|
+
|
|
61
77
|
**Revise when a conclusion changes:**
|
|
62
78
|
|
|
63
79
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memgineering-memory
|
|
3
|
-
description: Use when the user asks about something they told you before, refers to a past decision, wants their own notes searched, or says something worth keeping. Triggers include "what did I decide about X", "check my notes", "how do we deploy again", "remember this", "예전에 뭐라고 정했더라", "내 노트에서 찾아줘", "이거 기억해둬", "what do you know about my setup". Covers recall, open, remember, revise, undo, and resurface against the user's own brain folder.
|
|
3
|
+
description: Use when the user asks about something they told you before, refers to a past decision, wants their own notes searched, or says something worth keeping. Triggers include "what did I decide about X", "check my notes", "how do we deploy again", "remember this", "is this still true", "which one do I go with", "예전에 뭐라고 정했더라", "내 노트에서 찾아줘", "이거 기억해둬", "what do you know about my setup". Covers recall, open, evidence, remember, revise, undo, and resurface against the user's own brain folder.
|
|
4
4
|
type: skill
|
|
5
5
|
allowed-tools: Bash(memgineering:*)
|
|
6
6
|
---
|
|
@@ -74,20 +74,70 @@ Say so. Only titles, aliases and summaries are searched, so suggest broader
|
|
|
74
74
|
words — but do not go read their folder yourself to compensate. A brain that
|
|
75
75
|
answers "nothing" is giving you real information.
|
|
76
76
|
|
|
77
|
+
## Deciding which one to trust
|
|
78
|
+
|
|
79
|
+
When two memories disagree, or one looks old enough to doubt, do not settle it
|
|
80
|
+
by date.
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
memgineering evidence deploy-manual
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
surfaced 34× (12× in this folder, 22× elsewhere)
|
|
88
|
+
opened 12× (35% of the recalls that surfaced it)
|
|
89
|
+
last used 3 days ago
|
|
90
|
+
in the ledger since 2026-05-02 · changed 2×
|
|
91
|
+
|
|
92
|
+
**well-used** — it has been reached for and read. The record backs it.
|
|
93
|
+
|
|
94
|
+
why it last changed:
|
|
95
|
+
> CD was removed from the repo
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Newer does not win.** A rule that has stood for eleven months has eleven
|
|
99
|
+
months of nobody contradicting it behind it; a note written last week has a
|
|
100
|
+
week. What separates them is whether either was ever actually used, and what
|
|
101
|
+
the last person to change their mind said. Both are in the output above.
|
|
102
|
+
|
|
103
|
+
Three standings, and each is a fact rather than a grade:
|
|
104
|
+
|
|
105
|
+
- **well-used** — recall returned it and somebody read it
|
|
106
|
+
- **surfaced, never opened** — it keeps coming back and nobody reads it. Either
|
|
107
|
+
it does not answer those questions, or its summary does not say what it knows
|
|
108
|
+
- **never recalled** — nothing has ever surfaced it. It may be perfectly true
|
|
109
|
+
and simply never needed, but nothing supports it either
|
|
110
|
+
|
|
111
|
+
If it says this machine has no recall history, the numbers mean nothing yet —
|
|
112
|
+
that log is local, so a brain synced to a new laptop starts empty. Say that
|
|
113
|
+
rather than reporting the memory as unused.
|
|
114
|
+
|
|
77
115
|
## Writing things down
|
|
78
116
|
|
|
79
117
|
```
|
|
80
|
-
memgineering remember "deploys are manual — launchctl by hand, no CD"
|
|
118
|
+
memgineering remember "deploys are manual — launchctl by hand, no CD" \
|
|
119
|
+
--reason "watched the deploy happen by hand twice"
|
|
81
120
|
```
|
|
82
121
|
|
|
83
122
|
Lands immediately as its own note and is recallable at once. There is no
|
|
84
123
|
approval queue: the design trades permission-before for correction-after, and
|
|
85
124
|
every write records how to reverse it.
|
|
86
125
|
|
|
126
|
+
**Pass `--reason` on every write.** It goes in the ledger, and it is the only
|
|
127
|
+
part of the record that survives being read six months later by somebody — the
|
|
128
|
+
user, or you in a new session — who no longer remembers the conversation. Every
|
|
129
|
+
write verb takes it: `remember`, `revise`, `retire`, `exclude`, `undo`. The one
|
|
130
|
+
on `undo` is usually the most valuable of all, because it is the moment an
|
|
131
|
+
earlier conclusion turned out to be wrong.
|
|
132
|
+
|
|
133
|
+
It is refused if it looks like it contains a credential. The ledger travels
|
|
134
|
+
with the brain, so that would be a permanent synced copy — say what changed
|
|
135
|
+
without the value, or drop the flag; the write itself is unaffected.
|
|
136
|
+
|
|
87
137
|
```
|
|
88
|
-
memgineering undo
|
|
138
|
+
memgineering undo --reason "that belonged to the other project" # the last change
|
|
89
139
|
memgineering undo <op_id> # a specific one
|
|
90
|
-
memgineering log # what
|
|
140
|
+
memgineering log # what changed, why, and what can still be undone
|
|
91
141
|
```
|
|
92
142
|
|
|
93
143
|
Write down: decisions and their reasons, constraints, corrections the user
|
|
@@ -122,7 +172,7 @@ the user's; neither this command nor you should rewrite it uninvited.
|
|
|
122
172
|
|
|
123
173
|
```
|
|
124
174
|
memgineering retire <ref> --reason "the date moved" # no longer current, still visible
|
|
125
|
-
memgineering exclude path/to/note.md
|
|
175
|
+
memgineering exclude path/to/note.md --reason "names an internal host" # stop reading it
|
|
126
176
|
```
|
|
127
177
|
|
|
128
178
|
Retiring keeps the note in recall, ranked last and labelled. Excluding takes it
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command17 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/help.ts
|
|
7
7
|
import { Help } from "commander";
|
|
@@ -145,7 +145,7 @@ var COMMAND_GROUPS = [
|
|
|
145
145
|
{
|
|
146
146
|
title: "MEMORY",
|
|
147
147
|
blurb: "day to day \u2014 what your agent reaches for while working",
|
|
148
|
-
commands: ["recall", "open", "remember", "revise", "undo", "resurface"]
|
|
148
|
+
commands: ["recall", "open", "evidence", "remember", "revise", "undo", "resurface"]
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
title: "BRAIN",
|
|
@@ -2547,12 +2547,36 @@ var OpRecordSchema = z7.object({
|
|
|
2547
2547
|
hash_after: z7.string().nullable(),
|
|
2548
2548
|
/** The frontmatter block region, for reversing a `revise`. */
|
|
2549
2549
|
patch: z7.object({ before: z7.string(), after: z7.string() }).nullable().default(null),
|
|
2550
|
+
/**
|
|
2551
|
+
* Why this was done, in the words of whoever did it. Never derived.
|
|
2552
|
+
*
|
|
2553
|
+
* Defaulted rather than required so a ledger written by 0.1.x — which had no
|
|
2554
|
+
* such field — still parses. An old line reads as "no reason was given",
|
|
2555
|
+
* which is exactly what happened.
|
|
2556
|
+
*/
|
|
2557
|
+
rationale: z7.string().nullable().default(null),
|
|
2550
2558
|
/** Which generation of the write rules produced this. */
|
|
2551
2559
|
rule_version: z7.number().int().positive(),
|
|
2552
2560
|
/** The op this reverses or replaces, when it does. */
|
|
2553
2561
|
supersedes: z7.string().nullable().default(null)
|
|
2554
2562
|
});
|
|
2555
|
-
var RULE_VERSION =
|
|
2563
|
+
var RULE_VERSION = 2;
|
|
2564
|
+
var RATIONALE_MAX = 500;
|
|
2565
|
+
function sanitizeRationale(raw) {
|
|
2566
|
+
if (raw == null) return null;
|
|
2567
|
+
const clean = stripControl(raw).trim();
|
|
2568
|
+
if (clean === "") return null;
|
|
2569
|
+
const hits = secretContentHits(clean);
|
|
2570
|
+
if (hits.length > 0) {
|
|
2571
|
+
throw memgError(
|
|
2572
|
+
"invalid_input",
|
|
2573
|
+
`the reason looks like it contains credential material (${hits.map((h) => h.code).join(", ")})`,
|
|
2574
|
+
"The ledger travels with the brain, so this would be a permanent synced copy. Say what changed and why without the value itself, or drop --reason \u2014 the write itself is unaffected."
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
const capped = clean.length > RATIONALE_MAX ? `${clean.slice(0, RATIONALE_MAX - 1)}\u2026` : clean;
|
|
2578
|
+
return capped;
|
|
2579
|
+
}
|
|
2556
2580
|
function ledgerDir(vaultRoot) {
|
|
2557
2581
|
return join6(vaultRoot, BRAND.vaultLedgerDir, "ledger");
|
|
2558
2582
|
}
|
|
@@ -2581,6 +2605,7 @@ async function appendOp(vaultRoot, op) {
|
|
|
2581
2605
|
hash_before: op.hashBefore,
|
|
2582
2606
|
hash_after: op.hashAfter,
|
|
2583
2607
|
patch: op.patch ?? null,
|
|
2608
|
+
rationale: op.rationale ?? null,
|
|
2584
2609
|
rule_version: RULE_VERSION,
|
|
2585
2610
|
supersedes: op.supersedes ?? null
|
|
2586
2611
|
});
|
|
@@ -3499,7 +3524,10 @@ function openCommand() {
|
|
|
3499
3524
|
);
|
|
3500
3525
|
printHuman(
|
|
3501
3526
|
c.gray(
|
|
3502
|
-
|
|
3527
|
+
`To state one:
|
|
3528
|
+
memgineering revise ${handle} --claim "<the conclusion>" --summary "<one line>"
|
|
3529
|
+
Add --contradicts <id> when it disagrees with another memory; that edge is what
|
|
3530
|
+
makes the two surface together as a conflict.`
|
|
3503
3531
|
)
|
|
3504
3532
|
);
|
|
3505
3533
|
}
|
|
@@ -3928,7 +3956,8 @@ Brain: ${root}`));
|
|
|
3928
3956
|
function lifecycleCommand(action) {
|
|
3929
3957
|
return new Command5(action).description(
|
|
3930
3958
|
action === "retire" ? "mark a memory as no longer current \u2014 the note itself is not changed" : "mark a retired memory as current again"
|
|
3931
|
-
).argument("<ref>", "handle, id, path, or exact title").option("--reason <text>", "why \u2014 kept in the
|
|
3959
|
+
).argument("<ref>", "handle, id, path, or exact title").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").action(async (ref, opts) => {
|
|
3960
|
+
const rationale = sanitizeRationale(opts.reason);
|
|
3932
3961
|
const { brain } = await resolveBrain(opts.vault);
|
|
3933
3962
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
3934
3963
|
createPrefix: brain.observationsDir
|
|
@@ -4003,6 +4032,7 @@ function lifecycleCommand(action) {
|
|
|
4003
4032
|
hashBefore: hashContent(before.content),
|
|
4004
4033
|
hashAfter: hashContent(outcome.content),
|
|
4005
4034
|
patch: { before: preview.before, after: preview.after },
|
|
4035
|
+
rationale,
|
|
4006
4036
|
snapshotBefore: before.content
|
|
4007
4037
|
});
|
|
4008
4038
|
});
|
|
@@ -4030,7 +4060,8 @@ Undo: \`memgineering undo ${record.op_id}\``));
|
|
|
4030
4060
|
function ignoreCommand(mode) {
|
|
4031
4061
|
return new Command5(mode).description(
|
|
4032
4062
|
mode === "exclude" ? "stop reading a note entirely \u2014 the note itself is not changed" : "read a note again after excluding it"
|
|
4033
|
-
).argument("<path>", "path inside the brain, e.g. decisions/launch-date.md").option("--vault <path>", "which brain the note is in").action(async (path, opts) => {
|
|
4063
|
+
).argument("<path>", "path inside the brain, e.g. decisions/launch-date.md").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the note is in").action(async (path, opts) => {
|
|
4064
|
+
const rationale = sanitizeRationale(opts.reason);
|
|
4034
4065
|
const { brain } = await resolveBrain(opts.vault);
|
|
4035
4066
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
4036
4067
|
createPrefix: brain.observationsDir
|
|
@@ -4054,6 +4085,7 @@ function ignoreCommand(mode) {
|
|
|
4054
4085
|
hashBefore: hashContent(base.content),
|
|
4055
4086
|
hashAfter: hashContent(result.content),
|
|
4056
4087
|
patch: { before: base.content, after: result.content },
|
|
4088
|
+
rationale,
|
|
4057
4089
|
snapshotBefore: base.content
|
|
4058
4090
|
});
|
|
4059
4091
|
});
|
|
@@ -4101,12 +4133,170 @@ var unretireCommand = () => lifecycleCommand("unretire");
|
|
|
4101
4133
|
var excludeCommand = () => ignoreCommand("exclude");
|
|
4102
4134
|
var unexcludeCommand = () => ignoreCommand("unexclude");
|
|
4103
4135
|
|
|
4136
|
+
// src/commands/evidence.ts
|
|
4137
|
+
import { Command as Command6 } from "commander";
|
|
4138
|
+
function evidenceCommand() {
|
|
4139
|
+
return new Command6("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").action(async (ref, opts) => {
|
|
4140
|
+
const { brain } = await resolveBrain(opts.vault);
|
|
4141
|
+
const adapter = await openBrainOrExplain(brain.root);
|
|
4142
|
+
const built = await openIndex(adapter, brain.root);
|
|
4143
|
+
const entry = resolveRef(built.entries, ref);
|
|
4144
|
+
const [events, ledger] = await Promise.all([readEvents(brain.root), readOps(brain.root)]);
|
|
4145
|
+
const now = /* @__PURE__ */ new Date();
|
|
4146
|
+
const report = assembleEvidence(entry, events, ledger.ops, {
|
|
4147
|
+
contextDir: process.cwd(),
|
|
4148
|
+
now,
|
|
4149
|
+
brain: built.entries
|
|
4150
|
+
});
|
|
4151
|
+
printDual({
|
|
4152
|
+
json: report,
|
|
4153
|
+
human: () => {
|
|
4154
|
+
printHuman(`## evidence: ${entry.memory.title}
|
|
4155
|
+
`);
|
|
4156
|
+
printHuman(`\`id: ${report.id}\` \xB7 \`${report.path}\`
|
|
4157
|
+
`);
|
|
4158
|
+
if (report.critical) {
|
|
4159
|
+
printHuman(c.yellow("\u26A0 critical \u2014 this note defines how the agent behaves.") + "\n");
|
|
4160
|
+
}
|
|
4161
|
+
if (!report.brain_has_history) {
|
|
4162
|
+
printHuman(
|
|
4163
|
+
c.yellow("This machine has no recall history for this brain yet.") + c.gray(
|
|
4164
|
+
"\nThe usage numbers below are zero because nothing has been recorded here,\nnot because the memory is unused. The log is local state and does not sync.\n"
|
|
4165
|
+
)
|
|
4166
|
+
);
|
|
4167
|
+
}
|
|
4168
|
+
const rows = [
|
|
4169
|
+
[
|
|
4170
|
+
"surfaced",
|
|
4171
|
+
report.surfaced === 0 ? "never" : `${report.surfaced}\xD7 (${report.surfaced_here}\xD7 in this folder, ${report.surfaced - report.surfaced_here}\xD7 elsewhere)`
|
|
4172
|
+
],
|
|
4173
|
+
[
|
|
4174
|
+
"opened",
|
|
4175
|
+
report.opened === 0 ? "never" : `${report.opened}\xD7${report.open_rate === null ? "" : ` (${report.opened} of the ${report.surfaced} times it surfaced)`}`
|
|
4176
|
+
],
|
|
4177
|
+
["last used", report.last_touched ? ago(report.last_touched, now) : "\u2014"],
|
|
4178
|
+
[
|
|
4179
|
+
"in the ledger",
|
|
4180
|
+
report.first_recorded ? `since ${report.first_recorded.slice(0, 10)} \xB7 changed ${report.changed}\xD7${report.undone > 0 ? ` \xB7 undone ${report.undone}\xD7` : ""}` : "nothing recorded \u2014 this note predates the ledger, or was only edited by hand"
|
|
4181
|
+
]
|
|
4182
|
+
];
|
|
4183
|
+
if (report.declared_from) {
|
|
4184
|
+
rows.push(["declared", `valid from ${report.declared_from.slice(0, 10)}`]);
|
|
4185
|
+
}
|
|
4186
|
+
if (report.supersedes > 0 || report.contradicts > 0) {
|
|
4187
|
+
rows.push([
|
|
4188
|
+
"relations",
|
|
4189
|
+
[
|
|
4190
|
+
report.supersedes > 0 ? `supersedes ${report.supersedes}` : null,
|
|
4191
|
+
report.contradicts > 0 ? `contradicts ${report.contradicts}` : null
|
|
4192
|
+
].filter(Boolean).join(" \xB7 ")
|
|
4193
|
+
]);
|
|
4194
|
+
}
|
|
4195
|
+
const pad = rows.reduce((w, [label]) => Math.max(w, label.length), 0) + 2;
|
|
4196
|
+
for (const [label, value] of rows) printHuman(` ${label.padEnd(pad)}${value}`);
|
|
4197
|
+
if (report.standing) {
|
|
4198
|
+
printHuman(`
|
|
4199
|
+
**${report.standing}** \u2014 ${STANDING_MEANS[report.standing]}`);
|
|
4200
|
+
}
|
|
4201
|
+
if (report.contradicts > 0) {
|
|
4202
|
+
printHuman(
|
|
4203
|
+
c.yellow(
|
|
4204
|
+
`
|
|
4205
|
+
\u26A0 this memory contradicts ${report.contradicts} other${report.contradicts === 1 ? "" : "s"}.`
|
|
4206
|
+
) + c.gray(
|
|
4207
|
+
"\n Both sides can be well-used. Usage cannot settle which one is right \u2014 compare the\n reasons above, or check something outside the brain."
|
|
4208
|
+
)
|
|
4209
|
+
);
|
|
4210
|
+
}
|
|
4211
|
+
if (report.last_reason) {
|
|
4212
|
+
printHuman(
|
|
4213
|
+
report.last_reason.verb === "remember" ? "\nwhy it was recorded:" : "\nwhy it last changed:"
|
|
4214
|
+
);
|
|
4215
|
+
printHuman(quoteBlock(report.last_reason.text));
|
|
4216
|
+
printHuman(
|
|
4217
|
+
c.gray(` ${report.last_reason.verb}, ${report.last_reason.ts.slice(0, 10)}`)
|
|
4218
|
+
);
|
|
4219
|
+
} else if (report.changed > 0) {
|
|
4220
|
+
printHuman(
|
|
4221
|
+
c.gray(
|
|
4222
|
+
"\nIt has changed, and no reason was recorded. Pass `--reason` when you write and the next one will say why."
|
|
4223
|
+
)
|
|
4224
|
+
);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
});
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
var STANDING_MEANS = {
|
|
4231
|
+
"well-used": "it gets reached for and read. That is attendance, not correctness.",
|
|
4232
|
+
"surfaced, never opened": "recall keeps returning it and nobody reads it. Either it does not answer those questions, or its summary does not say what it knows.",
|
|
4233
|
+
"never recalled": "nothing has ever surfaced it. It may be true and simply never needed \u2014 but nothing here supports it either."
|
|
4234
|
+
};
|
|
4235
|
+
function assembleEvidence(entry, events, ops, opts) {
|
|
4236
|
+
const id = entry.memory.id;
|
|
4237
|
+
const surfacing = events.filter(
|
|
4238
|
+
(e) => (e.verb === "recall" || e.verb === "resurface") && e.returned_ids.includes(id)
|
|
4239
|
+
);
|
|
4240
|
+
const opens = events.filter((e) => e.verb === "open" && e.opened_id === id);
|
|
4241
|
+
const touched = [...surfacing, ...opens].map((e) => e.ts).sort().at(-1);
|
|
4242
|
+
const mine = ops.filter((o) => o.target === entry.path || o.memory_id === id);
|
|
4243
|
+
const changed = mine.filter((o) => o.verb !== "undo" && o.verb !== "remember").length;
|
|
4244
|
+
const undone = mine.filter((o) => o.verb === "undo").length;
|
|
4245
|
+
const withReason = mine.filter((o) => o.rationale !== null).at(-1);
|
|
4246
|
+
const relationsOfType = (type) => entry.relations.filter((r) => r.type === type && r.from === id).length;
|
|
4247
|
+
const disputedWith = /* @__PURE__ */ new Set();
|
|
4248
|
+
for (const relation of entry.relations) {
|
|
4249
|
+
if (relation.type === "contradicts" && relation.from === id) disputedWith.add(relation.to);
|
|
4250
|
+
}
|
|
4251
|
+
for (const other of opts.brain ?? []) {
|
|
4252
|
+
if (other.memory.id === id) continue;
|
|
4253
|
+
for (const relation of other.relations) {
|
|
4254
|
+
if (relation.type === "contradicts" && relation.to === id) disputedWith.add(relation.from);
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
return {
|
|
4258
|
+
id,
|
|
4259
|
+
path: entry.path,
|
|
4260
|
+
title: entry.memory.title,
|
|
4261
|
+
critical: entry.path.startsWith("01_BASE/"),
|
|
4262
|
+
brain_has_history: events.length > 0,
|
|
4263
|
+
surfaced: surfacing.length,
|
|
4264
|
+
surfaced_here: surfacing.filter((e) => isInside(e.context_dir, opts.contextDir)).length,
|
|
4265
|
+
opened: opens.length,
|
|
4266
|
+
// Divided by the number printed directly above it. It used to be divided by
|
|
4267
|
+
// recalls only, while `surfaced` also counted resurfacings — so a reader saw
|
|
4268
|
+
// two numbers and a percentage that reconciled with neither.
|
|
4269
|
+
open_rate: surfacing.length === 0 ? null : opens.length / surfacing.length,
|
|
4270
|
+
last_touched: touched ?? null,
|
|
4271
|
+
first_recorded: mine[0]?.ts ?? null,
|
|
4272
|
+
declared_from: entry.claims.find((claim) => claim.valid_from)?.valid_from ?? null,
|
|
4273
|
+
changed,
|
|
4274
|
+
undone,
|
|
4275
|
+
supersedes: relationsOfType("supersedes"),
|
|
4276
|
+
contradicts: disputedWith.size,
|
|
4277
|
+
last_reason: withReason ? { verb: withReason.verb, ts: withReason.ts, text: withReason.rationale } : null,
|
|
4278
|
+
// Withheld rather than guessed when the log is empty. "Never recalled" on a
|
|
4279
|
+
// brain that arrived this morning would be a confident wrong answer, which
|
|
4280
|
+
// is the one thing a memory tool must not produce.
|
|
4281
|
+
standing: events.length === 0 ? null : opens.length > 0 ? "well-used" : surfacing.length > 0 ? "surfaced, never opened" : "never recalled"
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
function ago(iso, now) {
|
|
4285
|
+
const days = Math.floor((now.getTime() - Date.parse(iso)) / 864e5);
|
|
4286
|
+
if (!Number.isFinite(days)) return iso;
|
|
4287
|
+
if (days <= 0) return "today";
|
|
4288
|
+
if (days === 1) return "yesterday";
|
|
4289
|
+
if (days < 30) return `${days} days ago`;
|
|
4290
|
+
if (days < 365) return `${Math.floor(days / 30)} months ago`;
|
|
4291
|
+
return `${Math.floor(days / 365)} years ago`;
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4104
4294
|
// src/commands/init.ts
|
|
4105
4295
|
import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
|
|
4106
4296
|
import { join as join13, resolve as resolve6 } from "path";
|
|
4107
|
-
import { Command as
|
|
4297
|
+
import { Command as Command7 } from "commander";
|
|
4108
4298
|
function initCommand() {
|
|
4109
|
-
return new
|
|
4299
|
+
return new Command7("init").description("create a new brain, laid out and ready to write into").argument("<path>", "where the brain should live").option("--name <name>", "what to call it in the hub", "My brain").action(async (path, opts) => {
|
|
4110
4300
|
const root = resolve6(path);
|
|
4111
4301
|
const existing = await readdir5(root).catch(() => null);
|
|
4112
4302
|
if (existing && existing.filter((e) => !e.startsWith(".")).length > 0) {
|
|
@@ -4274,9 +4464,9 @@ function files(name) {
|
|
|
4274
4464
|
}
|
|
4275
4465
|
|
|
4276
4466
|
// src/commands/log.ts
|
|
4277
|
-
import { Command as
|
|
4467
|
+
import { Command as Command8 } from "commander";
|
|
4278
4468
|
function logCommand() {
|
|
4279
|
-
return new
|
|
4469
|
+
return new Command8("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").action(async (opts) => {
|
|
4280
4470
|
const { brain } = await resolveBrain(opts.vault);
|
|
4281
4471
|
const adapter = await openBrainOrExplain(brain.root);
|
|
4282
4472
|
const { ops, skipped } = await readOps(brain.root);
|
|
@@ -4322,6 +4512,7 @@ function logCommand() {
|
|
|
4322
4512
|
`- \`${op.op_id}\` ${op.verb} \`${op.target}\`${marks.length ? c.yellow(` (${marks.join(", ")})`) : ""}`
|
|
4323
4513
|
);
|
|
4324
4514
|
printHuman(c.gray(` ${op.ts} \xB7 ${op.actor}`));
|
|
4515
|
+
if (op.rationale) printHuman(c.gray(quoteBlock(op.rationale, " > ")));
|
|
4325
4516
|
if (opts.verbose) {
|
|
4326
4517
|
printHuman(c.gray(` ${op.hash_before ?? "\u2205"} \u2192 ${op.hash_after ?? "\u2205"}`));
|
|
4327
4518
|
}
|
|
@@ -4357,6 +4548,7 @@ function summarize(op) {
|
|
|
4357
4548
|
verb: op.verb,
|
|
4358
4549
|
target: op.target,
|
|
4359
4550
|
memory_id: op.memory_id,
|
|
4551
|
+
rationale: op.rationale,
|
|
4360
4552
|
supersedes: op.supersedes
|
|
4361
4553
|
};
|
|
4362
4554
|
}
|
|
@@ -4369,9 +4561,9 @@ async function readOrNull(adapter, path) {
|
|
|
4369
4561
|
}
|
|
4370
4562
|
|
|
4371
4563
|
// src/commands/recall.ts
|
|
4372
|
-
import { Command as
|
|
4564
|
+
import { Command as Command9 } from "commander";
|
|
4373
4565
|
function recallCommand() {
|
|
4374
|
-
return new
|
|
4566
|
+
return new Command9("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option("--detail <level>", "title | card | summary | chunks | full (default: card)").action(
|
|
4375
4567
|
async (query, opts) => {
|
|
4376
4568
|
const detail = parseDetail(opts.detail, "card");
|
|
4377
4569
|
const { brain } = await resolveBrain(opts.vault);
|
|
@@ -4411,10 +4603,27 @@ function recallCommand() {
|
|
|
4411
4603
|
conflicts: result.conflicts
|
|
4412
4604
|
},
|
|
4413
4605
|
human: () => {
|
|
4606
|
+
const otherScript = unreachableScript(query, built.entries);
|
|
4607
|
+
const sayWhy = () => {
|
|
4608
|
+
printHuman(
|
|
4609
|
+
c.yellow(
|
|
4610
|
+
`\u26A0 This brain's notes are mostly written in ${otherScript}, and this query is not.`
|
|
4611
|
+
) + c.gray(
|
|
4612
|
+
`
|
|
4613
|
+
Search matches text, not meaning, so no rewording in another language will reach them.
|
|
4614
|
+
Ask again in ${otherScript}, or run \`memgineering resurface\` to see what is here.
|
|
4615
|
+
`
|
|
4616
|
+
)
|
|
4617
|
+
);
|
|
4618
|
+
};
|
|
4414
4619
|
if (rows.length === 0) {
|
|
4415
4620
|
printHuman(`## recall: ${query}
|
|
4416
4621
|
`);
|
|
4417
4622
|
printHuman("_Nothing matched._\n");
|
|
4623
|
+
if (otherScript) {
|
|
4624
|
+
sayWhy();
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4418
4627
|
printHuman(
|
|
4419
4628
|
c.gray(
|
|
4420
4629
|
"Only titles, aliases and summaries are searched \u2014 try fewer or broader words, or drop --scope."
|
|
@@ -4424,6 +4633,7 @@ function recallCommand() {
|
|
|
4424
4633
|
}
|
|
4425
4634
|
printHuman(`## recall: ${query} (${rows.length} cards)
|
|
4426
4635
|
`);
|
|
4636
|
+
if (otherScript) sayWhy();
|
|
4427
4637
|
for (const [i, { card, handle, stage }] of rows.entries()) {
|
|
4428
4638
|
printHuman(renderCard(i + 1, card, handle, detail, stage));
|
|
4429
4639
|
}
|
|
@@ -4471,11 +4681,62 @@ function renderCard(n, card, handle, detail, stage) {
|
|
|
4471
4681
|
out.push("");
|
|
4472
4682
|
return out.join("\n");
|
|
4473
4683
|
}
|
|
4684
|
+
function unreachableScript(query, entries) {
|
|
4685
|
+
const queryScripts = scriptsIn(query);
|
|
4686
|
+
if (queryScripts.size === 0) return null;
|
|
4687
|
+
const written = entries.filter((e) => !e.path.startsWith("01_BASE/"));
|
|
4688
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4689
|
+
for (const entry of written) {
|
|
4690
|
+
const script = dominantScriptOf(`${entry.memory.title} ${entry.memory.summary ?? ""}`);
|
|
4691
|
+
if (script) counts.set(script, (counts.get(script) ?? 0) + 1);
|
|
4692
|
+
}
|
|
4693
|
+
if (counts.size === 0) return null;
|
|
4694
|
+
const [dominant, seen] = [...counts].sort((a, b) => b[1] - a[1])[0];
|
|
4695
|
+
if (queryScripts.has(dominant)) return null;
|
|
4696
|
+
return seen >= Math.max(2, written.length / 2) ? dominant : null;
|
|
4697
|
+
}
|
|
4698
|
+
var SCRIPT_RANGES = [
|
|
4699
|
+
["Korean", /[가-힣ᄀ-ᇿ]/g],
|
|
4700
|
+
["Kana", /[-ヿ]/g],
|
|
4701
|
+
["Han", /[一-鿿]/g],
|
|
4702
|
+
["Cyrillic", /[Ѐ-ӿ]/g],
|
|
4703
|
+
["Arabic", /[-ۿ]/g],
|
|
4704
|
+
["English", /[A-Za-z]/g]
|
|
4705
|
+
];
|
|
4706
|
+
function scriptCounts(text) {
|
|
4707
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4708
|
+
for (const [name, pattern] of SCRIPT_RANGES) {
|
|
4709
|
+
const n = (text.match(pattern) ?? []).length;
|
|
4710
|
+
if (n > 0) counts.set(name, n);
|
|
4711
|
+
}
|
|
4712
|
+
const han = counts.get("Han") ?? 0;
|
|
4713
|
+
if (counts.has("Kana")) {
|
|
4714
|
+
counts.set("Japanese", (counts.get("Kana") ?? 0) + han);
|
|
4715
|
+
counts.delete("Kana");
|
|
4716
|
+
counts.delete("Han");
|
|
4717
|
+
} else if (han > 0) {
|
|
4718
|
+
counts.set("Chinese", han);
|
|
4719
|
+
counts.delete("Han");
|
|
4720
|
+
}
|
|
4721
|
+
return counts;
|
|
4722
|
+
}
|
|
4723
|
+
function dominantScriptOf(text) {
|
|
4724
|
+
const ranked = [...scriptCounts(text)].sort((a, b) => b[1] - a[1]);
|
|
4725
|
+
if (ranked.length === 0) return null;
|
|
4726
|
+
if (ranked.length > 1 && ranked[0][1] === ranked[1][1]) return null;
|
|
4727
|
+
return ranked[0][0];
|
|
4728
|
+
}
|
|
4729
|
+
function scriptsIn(text) {
|
|
4730
|
+
return new Set(scriptCounts(text).keys());
|
|
4731
|
+
}
|
|
4474
4732
|
|
|
4475
4733
|
// src/commands/remember.ts
|
|
4476
|
-
import { Command as
|
|
4734
|
+
import { Command as Command10 } from "commander";
|
|
4477
4735
|
function rememberCommand() {
|
|
4478
|
-
return new
|
|
4736
|
+
return new Command10("remember").description("write something down now, with no approval step").argument("<text>", "what to remember").option("--vault <path>", "which brain to write to").option("--scope <scope>", "what this is about \u2014 a project, an area").option(
|
|
4737
|
+
"--reason <text>",
|
|
4738
|
+
"why you are recording this \u2014 goes in the ledger, not in the note (the fact itself belongs in <text>)"
|
|
4739
|
+
).action(async (text, opts) => {
|
|
4479
4740
|
const body = stripControl(text).trim();
|
|
4480
4741
|
if (body === "") {
|
|
4481
4742
|
throw memgError(
|
|
@@ -4484,6 +4745,7 @@ function rememberCommand() {
|
|
|
4484
4745
|
'Pass the text: `memgineering remember "deploys are manual \u2014 launchctl by hand"`'
|
|
4485
4746
|
);
|
|
4486
4747
|
}
|
|
4748
|
+
const rationale = sanitizeRationale(opts.reason);
|
|
4487
4749
|
const { brain } = await resolveBrain(opts.vault);
|
|
4488
4750
|
await openBrainOrExplain(brain.root);
|
|
4489
4751
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -4519,6 +4781,7 @@ function rememberCommand() {
|
|
|
4519
4781
|
memoryId: id,
|
|
4520
4782
|
hashBefore: null,
|
|
4521
4783
|
hashAfter: hashContent(content),
|
|
4784
|
+
rationale,
|
|
4522
4785
|
now
|
|
4523
4786
|
});
|
|
4524
4787
|
});
|
|
@@ -4562,9 +4825,9 @@ function yamlString(value) {
|
|
|
4562
4825
|
}
|
|
4563
4826
|
|
|
4564
4827
|
// src/commands/resurface.ts
|
|
4565
|
-
import { Command as
|
|
4828
|
+
import { Command as Command11 } from "commander";
|
|
4566
4829
|
function resurfaceCommand() {
|
|
4567
|
-
return new
|
|
4830
|
+
return new Command11("resurface").description("what is worth having in view right now, without being asked").option("--context-dir <path>", "where the work is happening (default: cwd)").option("--limit <n>", "how many cards", (v) => parseInt(v, 10), 5).option("--vault <path>", "which brain").action(async (opts) => {
|
|
4568
4831
|
const contextDir = opts.contextDir ?? process.cwd();
|
|
4569
4832
|
const { brain } = await resolveBrain(opts.vault, contextDir);
|
|
4570
4833
|
const adapter = await openBrainOrExplain(brain.root);
|
|
@@ -4690,9 +4953,9 @@ function rank(entries, events, opts) {
|
|
|
4690
4953
|
|
|
4691
4954
|
// src/commands/revise.ts
|
|
4692
4955
|
import { readFile as readFile14 } from "fs/promises";
|
|
4693
|
-
import { Command as
|
|
4956
|
+
import { Command as Command12 } from "commander";
|
|
4694
4957
|
function reviseCommand() {
|
|
4695
|
-
return new
|
|
4958
|
+
return new Command12("revise").description("change a memory's conclusion, and record how to undo it").argument("<ref>", "handle, id, path, or exact title").option("--claim <text>", "what is now true").option("--summary <text>", "the one line recall will show (defaults to the claim)").option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--contradicts <ids...>", "memories this disagrees with").option("--reason <text>", "why this change is right").option("--because <text>", "the passage from the note that justifies it").option("--input <json>", "the whole change as JSON (file path, `-`, or inline)").option("--dry-run", "show the diff and write nothing").option("--vault <path>", "which brain to write to").action(async (ref, opts) => {
|
|
4696
4959
|
const { brain } = await resolveBrain(opts.vault);
|
|
4697
4960
|
await openBrainOrExplain(brain.root);
|
|
4698
4961
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
@@ -4701,6 +4964,9 @@ function reviseCommand() {
|
|
|
4701
4964
|
const built = await openIndex(adapter, brain.root);
|
|
4702
4965
|
const entry = resolveRef(built.entries, ref);
|
|
4703
4966
|
const input = await buildCommitInput(entry, opts);
|
|
4967
|
+
const rationale = sanitizeRationale(
|
|
4968
|
+
input.reason === NO_REASON_GIVEN ? null : input.reason ?? null
|
|
4969
|
+
);
|
|
4704
4970
|
const result = commit(asSingleMatch(entry), input);
|
|
4705
4971
|
if (result.action === "ignore") {
|
|
4706
4972
|
throw memgError("invalid_input", "nothing to revise", "Pass --claim, or --input with one.");
|
|
@@ -4756,6 +5022,7 @@ function reviseCommand() {
|
|
|
4756
5022
|
// would build a permanent second copy of something the author may
|
|
4757
5023
|
// later delete on purpose.
|
|
4758
5024
|
patch: { before: preview.before, after: preview.after },
|
|
5025
|
+
rationale,
|
|
4759
5026
|
snapshotBefore: before.content
|
|
4760
5027
|
});
|
|
4761
5028
|
return { record, content: outcome.content };
|
|
@@ -4788,6 +5055,7 @@ Undo: \`memgineering undo ${applied.record.op_id}\``));
|
|
|
4788
5055
|
});
|
|
4789
5056
|
});
|
|
4790
5057
|
}
|
|
5058
|
+
var NO_REASON_GIVEN = "revised directly";
|
|
4791
5059
|
async function buildCommitInput(entry, opts) {
|
|
4792
5060
|
if (opts.input) {
|
|
4793
5061
|
const raw = await readInput(opts.input);
|
|
@@ -4823,7 +5091,7 @@ async function buildCommitInput(entry, opts) {
|
|
|
4823
5091
|
relates_to: [],
|
|
4824
5092
|
contradicts: opts.contradicts ?? []
|
|
4825
5093
|
},
|
|
4826
|
-
reason: opts.reason ??
|
|
5094
|
+
reason: opts.reason ?? NO_REASON_GIVEN,
|
|
4827
5095
|
// The engine refuses to submit without an excerpt, because approving a
|
|
4828
5096
|
// diff on an assertion alone is not consent. Nothing here is approved by a
|
|
4829
5097
|
// person, but the requirement still earns its keep: it forces the record
|
|
@@ -4890,10 +5158,10 @@ import { access, mkdir as mkdir9, readFile as readFile15 } from "fs/promises";
|
|
|
4890
5158
|
import { homedir as homedir3 } from "os";
|
|
4891
5159
|
import { dirname as dirname3, join as join14 } from "path";
|
|
4892
5160
|
import { fileURLToPath } from "url";
|
|
4893
|
-
import { Command as
|
|
5161
|
+
import { Command as Command13 } from "commander";
|
|
4894
5162
|
import prompts2 from "prompts";
|
|
4895
5163
|
function setupCommand() {
|
|
4896
|
-
return new
|
|
5164
|
+
return new Command13("setup").description("install into your agents, and point at a brain").option("--agent", "non-interactive: take every answer from flags").option("--human", "interactive: choose from a list").option("--tools <list>", "comma-separated: claude, codex, gemini (default: all detected)").option("--auto-update <choice>", "on | off (default: on, recommended)").option("--hook <choice>", "on | off \u2014 SessionStart resurface, Claude Code only").option("--dry-run", "report what would change, write nothing").option("--silent", "only speak if something changed").action(async (opts) => {
|
|
4897
5165
|
const mode = await resolveMode(opts);
|
|
4898
5166
|
const detected = await detectTools();
|
|
4899
5167
|
if (detected.length === 0) {
|
|
@@ -5258,7 +5526,7 @@ async function exists(path) {
|
|
|
5258
5526
|
}
|
|
5259
5527
|
|
|
5260
5528
|
// src/commands/update.ts
|
|
5261
|
-
import { Command as
|
|
5529
|
+
import { Command as Command14 } from "commander";
|
|
5262
5530
|
|
|
5263
5531
|
// src/version.ts
|
|
5264
5532
|
import { readFileSync } from "fs";
|
|
@@ -5500,7 +5768,7 @@ function shortMessage(err) {
|
|
|
5500
5768
|
|
|
5501
5769
|
// src/commands/update.ts
|
|
5502
5770
|
function updateCommand() {
|
|
5503
|
-
return new
|
|
5771
|
+
return new Command14("update").description("update memgineering itself").option("--check", "report what is available and change nothing").action(async (opts) => {
|
|
5504
5772
|
const status = await checkForUpdate(VERSION);
|
|
5505
5773
|
const provenance = await detectProvenance();
|
|
5506
5774
|
const previous = await readUpdateState();
|
|
@@ -5608,9 +5876,10 @@ function reportBelowMinimum(below) {
|
|
|
5608
5876
|
// src/commands/undo.ts
|
|
5609
5877
|
import { readFile as readFile17, rm as rm8 } from "fs/promises";
|
|
5610
5878
|
import { join as join18 } from "path";
|
|
5611
|
-
import { Command as
|
|
5879
|
+
import { Command as Command15 } from "commander";
|
|
5612
5880
|
function undoCommand() {
|
|
5613
|
-
return new
|
|
5881
|
+
return new Command15("undo").description("take back the last change, or a named one").argument("[op]", "operation id (default: the most recent)").option("--reason <text>", "why it is being taken back \u2014 kept in the ledger").option("--vault <path>", "which brain to act on").action(async (opId, opts) => {
|
|
5882
|
+
const rationale = sanitizeRationale(opts.reason);
|
|
5614
5883
|
const { brain } = await resolveBrain(opts.vault);
|
|
5615
5884
|
await openBrainOrExplain(brain.root);
|
|
5616
5885
|
const { ops } = await readOps(brain.root);
|
|
@@ -5658,6 +5927,7 @@ function undoCommand() {
|
|
|
5658
5927
|
memoryId: target.memory_id,
|
|
5659
5928
|
hashBefore: actualHash,
|
|
5660
5929
|
hashAfter: reverted === null ? null : hashContent(reverted),
|
|
5930
|
+
rationale,
|
|
5661
5931
|
supersedes: target.op_id
|
|
5662
5932
|
});
|
|
5663
5933
|
});
|
|
@@ -5747,25 +6017,32 @@ The text from before that change is still on this machine: ${join18(snapshotDir(
|
|
|
5747
6017
|
|
|
5748
6018
|
// src/commands/use.ts
|
|
5749
6019
|
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve8 } from "path";
|
|
5750
|
-
import { Command as
|
|
6020
|
+
import { Command as Command16 } from "commander";
|
|
6021
|
+
var VIA_MEANS = {
|
|
6022
|
+
flag: "because --vault named it",
|
|
6023
|
+
pointer: `because a \`${BRAND.pointerFileName}\` file here points at it`,
|
|
6024
|
+
cwd: "because this folder is inside it",
|
|
6025
|
+
only: "because it is the only brain linked"
|
|
6026
|
+
};
|
|
5751
6027
|
function useCommand() {
|
|
5752
|
-
return new
|
|
6028
|
+
return new Command16("use").description("bind this folder (and everything under it) to one brain").argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--here <dir>", "write the pointer in this directory instead of the cwd").action(async (brainPath, opts) => {
|
|
5753
6029
|
const where = await canonicalize(resolve8(opts.here ?? process.cwd()));
|
|
5754
6030
|
if (!brainPath) {
|
|
5755
6031
|
const pointer = await readPointer(where);
|
|
6032
|
+
const resolved = await resolveBrain(void 0, where);
|
|
5756
6033
|
printDual({
|
|
5757
|
-
json:
|
|
6034
|
+
json: {
|
|
6035
|
+
brain: resolved.brain.root,
|
|
6036
|
+
via: resolved.via,
|
|
6037
|
+
pointer: pointer ? pointer.from : null
|
|
6038
|
+
},
|
|
5758
6039
|
human: () => {
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
)
|
|
5765
|
-
);
|
|
5766
|
-
return;
|
|
6040
|
+
printHuman(`Using \`${resolved.brain.root}\`
|
|
6041
|
+
`);
|
|
6042
|
+
printHuman(c.gray(` ${VIA_MEANS[resolved.via]}`));
|
|
6043
|
+
if (resolved.via !== "pointer") {
|
|
6044
|
+
printHuman(c.gray(" Bind this folder explicitly with `memgineering use <brain>`."));
|
|
5767
6045
|
}
|
|
5768
|
-
printHuman(`\`${pointer.from}\` \u2192 \`${pointer.root}\``);
|
|
5769
6046
|
}
|
|
5770
6047
|
});
|
|
5771
6048
|
return;
|
|
@@ -5804,13 +6081,14 @@ A pointer to an unlinked folder would fail on every command instead of at this o
|
|
|
5804
6081
|
|
|
5805
6082
|
// src/program.ts
|
|
5806
6083
|
function buildProgram() {
|
|
5807
|
-
const program = new
|
|
6084
|
+
const program = new Command17("memgineering").version(VERSION).description("memgineering \u2014 one memory for the AI you connect").option("--json", "emit JSON to stdout instead of markdown", false).hook("preAction", (thisCommand) => {
|
|
5808
6085
|
const opts = thisCommand.optsWithGlobals();
|
|
5809
6086
|
if (opts.json || process.env[BRAND.jsonEnvVar] === "1") setJsonMode(true);
|
|
5810
6087
|
});
|
|
5811
6088
|
configureGroupedHelp(program);
|
|
5812
6089
|
program.addCommand(recallCommand());
|
|
5813
6090
|
program.addCommand(openCommand());
|
|
6091
|
+
program.addCommand(evidenceCommand());
|
|
5814
6092
|
program.addCommand(rememberCommand());
|
|
5815
6093
|
program.addCommand(reviseCommand());
|
|
5816
6094
|
program.addCommand(undoCommand());
|
package/package.json
CHANGED