memgineering 0.7.3 → 0.7.5
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 +110 -0
- package/assets/MEMGINEERING.md +1 -1
- package/assets/memgineering-setup/SKILL.md +13 -1
- package/assets/memgineering-writing/SKILL.md +13 -2
- package/dist/index.js +249 -118
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,116 @@ language the reader wants. The bilingual rule the monorepo applies to
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.7.5] — 2026-08-23
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **The approval list now shows the claim — what the note would say — above the
|
|
19
|
+
reason it is being changed.** `proposals approve` has no confirmation step: it
|
|
20
|
+
applies the change and shows the diff afterwards, so the list is the only
|
|
21
|
+
place a decision is made, and it was carrying the argument for a change while
|
|
22
|
+
withholding the change itself. Cut to one line on screen; whole under
|
|
23
|
+
`--json`. Null for `retire`, `unretire` and `reclassify`, which assert no
|
|
24
|
+
conclusion. Showing it for a **hosted** brain needs the brain server deployed
|
|
25
|
+
as well; against an older server the row says the claim is unavailable rather
|
|
26
|
+
than rendering as though the action had none. Brains in a folder need nothing.
|
|
27
|
+
- **`consolidate` candidates name their notes.** A row gave an id and a path,
|
|
28
|
+
which say which FILE a candidate is about and nothing about what it says — so
|
|
29
|
+
you could not tell which notes a candidate was even about without opening both.
|
|
30
|
+
The row still does not carry their claims: deciding which of a pair is right
|
|
31
|
+
means reading them, and the row is what tells you which two to read.
|
|
32
|
+
Titles are cut at 72 characters with an ellipsis, so a short title and a
|
|
33
|
+
truncated one are never confusable. (Characters, not columns — a Korean title
|
|
34
|
+
is about twice as wide on screen.)
|
|
35
|
+
- **`reindex` reports what it left out.** It listed notes refused for their name
|
|
36
|
+
or their content and said nothing about ones a `.memgignore` rule or a symlink
|
|
37
|
+
kept out of the index, so "✓ rebuilt 3 note(s)" could not answer "where is my
|
|
38
|
+
note". It now prints the same four-rule block `push` prints. `--json` gains
|
|
39
|
+
`not_indexed`, `denied_explicitly`, `excluded_by_rule` and `skipped_symlinks`
|
|
40
|
+
— the count is `not_indexed` rather than push's `not_sent`, because nothing is
|
|
41
|
+
sent from here.
|
|
42
|
+
- **A title or claim cut mid-emoji no longer comes out as broken text.** The
|
|
43
|
+
truncation counted UTF-16 units, so a cut landing between the halves of a
|
|
44
|
+
surrogate pair emitted a lone surrogate. It counts characters now.
|
|
45
|
+
- **A rule that excludes a FOLDER now names the folder.** `push` and `reindex`
|
|
46
|
+
both stop walking at an excluded directory, so the notes inside are never
|
|
47
|
+
enumerated — and both reported a held-back count of zero and printed nothing
|
|
48
|
+
else, so a brain whose entire archive was excluded looked exactly like a brain
|
|
49
|
+
that lost nothing. The count is still an honest floor; `excluded_folders` in
|
|
50
|
+
`--json` and a named line on screen say what it is a floor of.
|
|
51
|
+
- **Note text is stripped of C1 controls and bidi format characters, on both
|
|
52
|
+
sides.** Only the `ESC`-introduced escape sequences were removed. A terminal in
|
|
53
|
+
8-bit mode reads U+009B and U+009D as the same two introducers with no `ESC`
|
|
54
|
+
byte; U+0085 is a line break, which broke the one-line guarantee the new title
|
|
55
|
+
and claim rows depend on; and a bidi override can make a line display in an
|
|
56
|
+
order its bytes do not have. U+2028 and U+2029 go too — `quoteBlock` splits on
|
|
57
|
+
`\n` alone, so a note containing one could resume outside the quote that marks
|
|
58
|
+
it as the note's words rather than the tool's. Escape sequences are read by
|
|
59
|
+
ECMA-48's grammar now rather than run to the next plausible-looking byte, so a
|
|
60
|
+
stray marker costs a few characters instead of the rest of the line. Measured:
|
|
61
|
+
one character before pure Hangul, two before `launchctl 로 한다`, five before
|
|
62
|
+
`21 August`, six before `123 abc` — against the whole remainder before. U+0085,
|
|
63
|
+
U+2028 and U+2029 are translated to a newline rather than dropped, so a break
|
|
64
|
+
in a stored note stays a break instead of welding the words on either side.
|
|
65
|
+
- **Security: text pasted out of a terminal or a chat window can now be refused,
|
|
66
|
+
and a hidden character can no longer sneak a password past the check that looks
|
|
67
|
+
for one.** If a proposal or a `--reason` is rejected for carrying invisible
|
|
68
|
+
formatting, nothing was saved — retype the words as plain text and run it again.
|
|
69
|
+
What was wrong: the check read normalised text, and normalising could not
|
|
70
|
+
win: deleting the marker left its parameters between a label and its value,
|
|
71
|
+
and consuming the sequence deleted label characters instead — each fix opened
|
|
72
|
+
the other hole. The screen now refuses a proposal whose prose contains an
|
|
73
|
+
escape or control introducer at all, which has no such seam. The same rule now
|
|
74
|
+
guards `--reason` on every write verb: that text goes into the ledger, which is
|
|
75
|
+
permanent and travels with the brain. Note text itself is still stored rather
|
|
76
|
+
than refused — blocking a save is the worse trade on the main write path, where
|
|
77
|
+
the commonest source of a stray control byte is a mis-decoded file — and is
|
|
78
|
+
protected instead by the credential detector, which now reads the text with
|
|
79
|
+
control introducers removed as well as with them left in, so a label split by a
|
|
80
|
+
hidden byte is still seen as a label. That widening cuts both ways and the
|
|
81
|
+
second half is worth knowing: a note ALREADY on disk whose text hides a label
|
|
82
|
+
this way is now refused on READ, where 0.7.4 read it fine. Both codes involved
|
|
83
|
+
are weak ones, so `.memgallow` overrules them. The two copies of the normaliser are
|
|
84
|
+
hardened and a parity test compares them as source, because they had already
|
|
85
|
+
drifted once.
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- `reindex` lists markdown only where it used to list every denied path, on
|
|
90
|
+
screen and in `denied_by_name` alike, matching `push`. A denied `.env` was
|
|
91
|
+
never a note anybody expected in a note index.
|
|
92
|
+
- `push`'s nothing-to-upload error no longer tells someone to check their path
|
|
93
|
+
when the path was right and their own folder rule emptied the brain.
|
|
94
|
+
|
|
95
|
+
### Agent guidance
|
|
96
|
+
|
|
97
|
+
`memgineering-writing` and `memgineering-setup` describe the claim row and what
|
|
98
|
+
`reindex` now reports. Restart your agent session after upgrading so it picks
|
|
99
|
+
them up.
|
|
100
|
+
|
|
101
|
+
## [0.7.4] — 2026-08-19
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- **Signing in now asks the person to agree, and an account is not created until
|
|
106
|
+
they do.** The approval screen carries three required consents — the terms,
|
|
107
|
+
the handling of personal data, and being 14 or older — as three separate
|
|
108
|
+
boxes, because they are three separate statements. An agent can run every
|
|
109
|
+
other step of a setup; it cannot answer these, and `login` now says so in both
|
|
110
|
+
its human output and its `next` hint so the agent stops and hands the question
|
|
111
|
+
over.
|
|
112
|
+
|
|
113
|
+
- **The terms and the privacy policy exist**, served by the brain server at
|
|
114
|
+
`/terms` and `/privacy` and linked from the approval screen. The privacy
|
|
115
|
+
policy was written against the database schema rather than from a template:
|
|
116
|
+
it names what is stored, what is not (IP addresses, request URLs, auth
|
|
117
|
+
headers), that the embedding model runs on the server so notes reach no third
|
|
118
|
+
party, and that recall queries are kept for 90 days. A test fails if a table
|
|
119
|
+
holding something a person wrote stops being described there.
|
|
120
|
+
|
|
121
|
+
This arrives with the server, not with this package — an older CLI reaches the
|
|
122
|
+
same screen and gets the same gate.
|
|
123
|
+
|
|
14
124
|
## [0.7.3] — 2026-08-18
|
|
15
125
|
|
|
16
126
|
### Fixed
|
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, tells you something worth keeping, or settles something that should hold next time. 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.7.
|
|
5
|
+
version: 0.7.5
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# memgineering
|
|
@@ -282,7 +282,19 @@ and needs no model anywhere; that is a decision, not a gap.
|
|
|
282
282
|
by hand in the ordinary case — writes keep the index in step on their own.
|
|
283
283
|
`memgineering reindex` reaches either kind of brain: on a hosted one it backfills
|
|
284
284
|
what is missing and clears stored rows for notes the brain has since been told
|
|
285
|
-
not to read, reporting both; `--local` forces the folder brain.
|
|
285
|
+
not to read, reporting both; `--local` forces the folder brain. On a brain in a
|
|
286
|
+
FOLDER it also names what it left out — notes a deny name, a `.memgignore` rule
|
|
287
|
+
or a symlink kept from the index, and any folder a rule excluded whole — so "3
|
|
288
|
+
notes rebuilt" is never the whole answer when a fourth was dropped. A hosted
|
|
289
|
+
reindex cannot say that: the rules are applied on the machine that pushed, so
|
|
290
|
+
the server only ever saw what arrived.
|
|
291
|
+
|
|
292
|
+
Under `--json` that is `not_indexed` with `denied_by_name`, `denied_explicitly`,
|
|
293
|
+
`excluded_by_rule`, `skipped_symlinks` and `excluded_folders`. **Read the count
|
|
294
|
+
together with `excluded_folders`**: a rule that names a folder stops the walk at
|
|
295
|
+
the folder, so nothing inside it is ever counted — `not_indexed: 0` beside a
|
|
296
|
+
non-empty `excluded_folders` means the count is a floor, not the answer. `push`
|
|
297
|
+
reports the same set with its total under `not_sent`. `--rebuild`
|
|
286
298
|
exists for one situation only, where an embedding model changed the shape of its
|
|
287
299
|
answers without changing its name and the index has to be laid down again.
|
|
288
300
|
|
|
@@ -140,8 +140,11 @@ only the last of them touches a note.
|
|
|
140
140
|
memgineering consolidate # what looks worth merging or reclassifying — reads only
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
Each candidate names its notes — id, title, path — so you can tell WHICH notes
|
|
144
|
+
it is about without opening them. It does not carry their claims, so deciding
|
|
145
|
+
which of a pair is right still means reading them: two notes can share a title
|
|
146
|
+
and hold opposite conclusions, which is exactly the case a proposal exists for. It returns them with the evidence that caught them and the action it
|
|
147
|
+
would suggest, under four rules: `similar_title`; `shared_context`, one scope naming
|
|
145
148
|
the same thing; `contradiction`, a disagreement nobody settled; and
|
|
146
149
|
`kind_promotion`, a note marked as binding but filed as something other than a
|
|
147
150
|
decision.
|
|
@@ -179,6 +182,14 @@ memgineering proposals reject <id>
|
|
|
179
182
|
memgineering proposals rebase <id>
|
|
180
183
|
```
|
|
181
184
|
|
|
185
|
+
**Read the list before you approve.** A row carries the claim — what the note
|
|
186
|
+
would say — above the reason it is being changed. Three actions assert no
|
|
187
|
+
conclusion and so have none: `retire`, `unretire` and `reclassify`. A row with no
|
|
188
|
+
claim line is one of those, not a row that lost something. There is no confirmation step:
|
|
189
|
+
`approve` applies the change and shows you the diff afterwards, so the list is
|
|
190
|
+
where the decision is actually made. The claim is cut to one line on screen and
|
|
191
|
+
whole under `--json`.
|
|
192
|
+
|
|
182
193
|
`approve` is the only command in this flow that writes to a note, and it goes
|
|
183
194
|
through the ledger like every other write — one `undo` away from not having
|
|
184
195
|
happened. `approve` and `reject` each take `--reason`, and it is kept.
|
package/dist/index.js
CHANGED
|
@@ -120,32 +120,46 @@ function stripControl(input) {
|
|
|
120
120
|
let out = "";
|
|
121
121
|
for (let i = 0; i < input.length; i++) {
|
|
122
122
|
const code = input.charCodeAt(i);
|
|
123
|
-
if (code === 27) {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
else if (input.charCodeAt(j) === 27) j += 2;
|
|
132
|
-
i = j - 1;
|
|
123
|
+
if (code === 27 && input[i + 1] === "]" || code === 157) {
|
|
124
|
+
const from = code === 157 ? i + 1 : i + 2;
|
|
125
|
+
let j = from;
|
|
126
|
+
while (j < input.length && input.charCodeAt(j) !== 7 && input.charCodeAt(j) !== 156 && !(input.charCodeAt(j) === 27 && input[j + 1] === "\\")) {
|
|
127
|
+
j++;
|
|
128
|
+
}
|
|
129
|
+
if (j >= input.length) {
|
|
130
|
+
i = from - 1;
|
|
133
131
|
continue;
|
|
134
132
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
133
|
+
j += input.charCodeAt(j) === 27 ? 2 : 1;
|
|
134
|
+
i = j - 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (code === 27 && input[i + 1] === "[" || code === 155) {
|
|
138
|
+
const from = code === 155 ? i + 1 : i + 2;
|
|
139
|
+
let j = from;
|
|
140
|
+
while (j < input.length && input.charCodeAt(j) >= 48 && input.charCodeAt(j) <= 63) j++;
|
|
141
|
+
while (j < input.length && input.charCodeAt(j) >= 32 && input.charCodeAt(j) <= 47) j++;
|
|
142
|
+
const final = j < input.length ? input.charCodeAt(j) : -1;
|
|
143
|
+
if (final >= 64 && final <= 126) {
|
|
140
144
|
i = j;
|
|
141
145
|
continue;
|
|
142
146
|
}
|
|
147
|
+
i = from - 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (code === 27) {
|
|
143
151
|
i += 1;
|
|
144
152
|
continue;
|
|
145
153
|
}
|
|
146
|
-
if (code
|
|
154
|
+
if (code === 133 || code === 8232 || code === 8233) {
|
|
155
|
+
out += "\n";
|
|
147
156
|
continue;
|
|
148
157
|
}
|
|
158
|
+
if (code <= 31 && code !== 10 && code !== 9 || code === 127) continue;
|
|
159
|
+
if (code >= 128 && code <= 159) continue;
|
|
160
|
+
if (code === 1564 || code === 8206 || code === 8207) continue;
|
|
161
|
+
if (code >= 8234 && code <= 8238) continue;
|
|
162
|
+
if (code >= 8294 && code <= 8297) continue;
|
|
149
163
|
out += input[i];
|
|
150
164
|
}
|
|
151
165
|
return out;
|
|
@@ -153,6 +167,12 @@ function stripControl(input) {
|
|
|
153
167
|
function quoteBlock(text, prefix = "> ") {
|
|
154
168
|
return stripControl(text).split("\n").map((line) => prefix + line).join("\n");
|
|
155
169
|
}
|
|
170
|
+
function oneLine(text, max) {
|
|
171
|
+
const flat = stripControl(text).replace(/\s+/g, " ").trim();
|
|
172
|
+
const chars = [...flat];
|
|
173
|
+
if (chars.length <= max) return flat;
|
|
174
|
+
return `${chars.slice(0, Math.max(1, max - 1)).join("").trimEnd()}\u2026`;
|
|
175
|
+
}
|
|
156
176
|
function progress(text) {
|
|
157
177
|
if (isInteractive()) process.stderr.write(kleur.gray(` ${text}
|
|
158
178
|
`));
|
|
@@ -167,12 +187,13 @@ function undoHint(opId, vault) {
|
|
|
167
187
|
function quoteArg(value) {
|
|
168
188
|
return /^[A-Za-z0-9._\/-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
169
189
|
}
|
|
170
|
-
var jsonMode, c;
|
|
190
|
+
var jsonMode, CONTROL_INTRODUCER, c;
|
|
171
191
|
var init_ui = __esm({
|
|
172
192
|
"src/ui.ts"() {
|
|
173
193
|
"use strict";
|
|
174
194
|
init_brand();
|
|
175
195
|
jsonMode = false;
|
|
196
|
+
CONTROL_INTRODUCER = /[\u001b\u009b\u009d]/;
|
|
176
197
|
c = kleur;
|
|
177
198
|
}
|
|
178
199
|
});
|
|
@@ -969,10 +990,12 @@ function secretContentHits(text) {
|
|
|
969
990
|
if (pattern.test(text)) hits.push({ code });
|
|
970
991
|
}
|
|
971
992
|
const plain = stripMarkdownEmphasis(text);
|
|
972
|
-
|
|
993
|
+
const bare = withoutIntroducers(text);
|
|
994
|
+
const bothWays = (check) => check(text) || check(plain) || bare !== text && (check(bare) || check(stripMarkdownEmphasis(bare)));
|
|
995
|
+
if (bothWays(hasLabelledCredential)) {
|
|
973
996
|
hits.push({ code: "labelled_credential" });
|
|
974
997
|
}
|
|
975
|
-
if (
|
|
998
|
+
if (bothWays(hasSpacedCredential)) {
|
|
976
999
|
hits.push({ code: "spaced_credential" });
|
|
977
1000
|
}
|
|
978
1001
|
if (hasCredentialNearPrivateIp(text)) hits.push({ code: "credential_near_private_ip" });
|
|
@@ -1003,7 +1026,7 @@ function isExplicitlyDenied(relPath, deny = EMPTY_DENY_LIST) {
|
|
|
1003
1026
|
function isNameAllowlisted(relPath, allow = EMPTY_DENY_LIST) {
|
|
1004
1027
|
return isExplicitlyDenied(relPath, allow);
|
|
1005
1028
|
}
|
|
1006
|
-
var SENSITIVE_NAME_TERMS, foldForMatch, FOLDED_TERMS, SECRET_CONTENT_PATTERNS, CREDENTIAL_LABEL, LABEL_TAIL, LABEL_QUALIFIER, SEPARATOR, HEADER_CELL_IS_LABEL, stripMarkdownEmphasis, LABELLED_CREDENTIAL_RE, SPACED_CREDENTIAL_RE, LOOKS_LIKE_A_CREDENTIAL, FINANCIAL_NUMBER_RE, SHORT_SECRET_RE, PRIVATE_IP_RE, ACCESS_WORD_RE, VALUE_IS_NOT_A_SECRET, DeniedReadError, WEAK_CODES, isWeakSecretCode, EMPTY_DENY_LIST;
|
|
1029
|
+
var SENSITIVE_NAME_TERMS, foldForMatch, FOLDED_TERMS, SECRET_CONTENT_PATTERNS, CREDENTIAL_LABEL, LABEL_TAIL, LABEL_QUALIFIER, SEPARATOR, HEADER_CELL_IS_LABEL, stripMarkdownEmphasis, LABELLED_CREDENTIAL_RE, SPACED_CREDENTIAL_RE, LOOKS_LIKE_A_CREDENTIAL, FINANCIAL_NUMBER_RE, SHORT_SECRET_RE, PRIVATE_IP_RE, ACCESS_WORD_RE, VALUE_IS_NOT_A_SECRET, DeniedReadError, WEAK_CODES, isWeakSecretCode, withoutIntroducers, EMPTY_DENY_LIST;
|
|
1007
1030
|
var init_deny = __esm({
|
|
1008
1031
|
"../../packages/storage-adapter/src/local-file/deny.ts"() {
|
|
1009
1032
|
"use strict";
|
|
@@ -1123,6 +1146,12 @@ var init_deny = __esm({
|
|
|
1123
1146
|
"credential_table"
|
|
1124
1147
|
]);
|
|
1125
1148
|
isWeakSecretCode = (code) => WEAK_CODES.has(code);
|
|
1149
|
+
withoutIntroducers = (text) => (
|
|
1150
|
+
// `ESC [` and `ESC ]` are two characters, so removing the ESC alone would
|
|
1151
|
+
// leave a bracket where the hidden byte was and the label would still not
|
|
1152
|
+
// read as itself.
|
|
1153
|
+
text.replace(/\u001b[[\]]?|[\u009b\u009d]/g, "")
|
|
1154
|
+
);
|
|
1126
1155
|
EMPTY_DENY_LIST = { entries: [] };
|
|
1127
1156
|
}
|
|
1128
1157
|
});
|
|
@@ -3129,6 +3158,7 @@ async function walkVault(root, opts = {}) {
|
|
|
3129
3158
|
const files2 = [];
|
|
3130
3159
|
const skippedSymlinks = [];
|
|
3131
3160
|
const excluded = [];
|
|
3161
|
+
const excludedDirs = [];
|
|
3132
3162
|
const visit = async (dirAbs, relDir, depth) => {
|
|
3133
3163
|
if (depth > maxDepth) return;
|
|
3134
3164
|
let entries;
|
|
@@ -3146,6 +3176,7 @@ async function walkVault(root, opts = {}) {
|
|
|
3146
3176
|
const isDir = entry.isDirectory();
|
|
3147
3177
|
if (isExcluded(rel, isDir, opts.ignore)) {
|
|
3148
3178
|
excluded.push(rel);
|
|
3179
|
+
if (isDir) excludedDirs.push(rel);
|
|
3149
3180
|
continue;
|
|
3150
3181
|
}
|
|
3151
3182
|
if (isDir) {
|
|
@@ -3157,7 +3188,7 @@ async function walkVault(root, opts = {}) {
|
|
|
3157
3188
|
};
|
|
3158
3189
|
await visit(rootAbs, "", 0);
|
|
3159
3190
|
files2.sort();
|
|
3160
|
-
return { files: files2, skippedSymlinks, excluded };
|
|
3191
|
+
return { files: files2, skippedSymlinks, excluded, excludedDirs };
|
|
3161
3192
|
}
|
|
3162
3193
|
function resolveInside(root, relPath) {
|
|
3163
3194
|
if (relPath === "" || relPath.startsWith("/") || relPath.includes("\0")) {
|
|
@@ -4864,7 +4895,7 @@ function emit(pending, ctx) {
|
|
|
4864
4895
|
browser_opened: ctx.opened,
|
|
4865
4896
|
api_url: ctx.base,
|
|
4866
4897
|
waiting: ctx.blocking,
|
|
4867
|
-
next: ctx.blocking ? "Waiting for approval in the browser." : "SHOW user_code to the user in your reply, then run any memgineering command \u2014 the token is claimed automatically once they approve."
|
|
4898
|
+
next: ctx.blocking ? "Waiting for approval in the browser." : "SHOW user_code to the user in your reply, then run any memgineering command \u2014 the token is claimed automatically once they approve. The approval screen asks THEM to agree to the terms and the privacy policy and to confirm they are 14 or older; you cannot answer that for them, and an account is not created until they do."
|
|
4868
4899
|
},
|
|
4869
4900
|
human: () => {
|
|
4870
4901
|
printHuman("Show this code to whoever is signing in:\n");
|
|
@@ -4876,6 +4907,11 @@ function emit(pending, ctx) {
|
|
|
4876
4907
|
printHuman("Open this to approve:\n");
|
|
4877
4908
|
printHuman(` ${c.cyan(stripControl(pending.verification_url))}`);
|
|
4878
4909
|
}
|
|
4910
|
+
printHuman(
|
|
4911
|
+
c.gray(
|
|
4912
|
+
"\nThey agree to the terms and the privacy policy there, and confirm they are 14 or older.\nNo account is created until they do."
|
|
4913
|
+
)
|
|
4914
|
+
);
|
|
4879
4915
|
if (ctx.opened && ctx.showUrl) {
|
|
4880
4916
|
printHuman(c.gray(` ${stripControl(pending.verification_url)}`));
|
|
4881
4917
|
}
|
|
@@ -8270,6 +8306,120 @@ async function createCloudBrain(token, baseUrl, name) {
|
|
|
8270
8306
|
// src/commands/brain-admin.ts
|
|
8271
8307
|
init_config();
|
|
8272
8308
|
|
|
8309
|
+
// src/lib/held-back.ts
|
|
8310
|
+
init_ui();
|
|
8311
|
+
var isNote = (p) => /\.(md|markdown)$/i.test(p);
|
|
8312
|
+
function heldBack(scan) {
|
|
8313
|
+
const denied = scan.denied ?? [];
|
|
8314
|
+
const byName = denied.filter((d) => d.kind === "name" && isNote(d.path)).map((d) => ({ path: d.path, term: d.term }));
|
|
8315
|
+
const explicitly = denied.filter((d) => d.kind === "explicit" && isNote(d.path)).map((d) => d.path);
|
|
8316
|
+
const byRule = scan.excluded.filter(isNote);
|
|
8317
|
+
const symlinked = scan.skippedSymlinks.filter(isNote);
|
|
8318
|
+
return {
|
|
8319
|
+
total: byName.length + explicitly.length + byRule.length + symlinked.length,
|
|
8320
|
+
folders: scan.excludedDirs ?? [],
|
|
8321
|
+
byName,
|
|
8322
|
+
explicitly,
|
|
8323
|
+
byRule,
|
|
8324
|
+
symlinked
|
|
8325
|
+
};
|
|
8326
|
+
}
|
|
8327
|
+
function heldBackJson(held, totalKey = "not_sent") {
|
|
8328
|
+
return {
|
|
8329
|
+
// The count's key is the caller's, because `not_sent` is a claim about
|
|
8330
|
+
// sending and `reindex` sends nothing. Naming its total `not_sent` would
|
|
8331
|
+
// have an agent reporting a failed upload from a command that never
|
|
8332
|
+
// contacts a server. `push` keeps the key it has always emitted.
|
|
8333
|
+
[totalKey]: held.total,
|
|
8334
|
+
denied_by_name: held.byName,
|
|
8335
|
+
denied_explicitly: held.explicitly,
|
|
8336
|
+
excluded_by_rule: held.byRule,
|
|
8337
|
+
skipped_symlinks: held.symlinked,
|
|
8338
|
+
// Present so a zero total can be read correctly. A non-empty list here means
|
|
8339
|
+
// the count above is a floor, not the answer — nothing inside these was
|
|
8340
|
+
// walked, so nobody knows how many notes they hold.
|
|
8341
|
+
excluded_folders: held.folders
|
|
8342
|
+
};
|
|
8343
|
+
}
|
|
8344
|
+
var NAME_GUESS_LABEL = "name looks private";
|
|
8345
|
+
function heldBackGroups(held) {
|
|
8346
|
+
return [
|
|
8347
|
+
{
|
|
8348
|
+
label: NAME_GUESS_LABEL,
|
|
8349
|
+
rows: held.byName.map((d) => ({ path: d.path, detail: `matched "${d.term ?? "?"}"` }))
|
|
8350
|
+
},
|
|
8351
|
+
{ label: "listed in .memgdeny", rows: held.explicitly.map((p) => ({ path: p, detail: null })) },
|
|
8352
|
+
{ label: "excluded by a rule", rows: held.byRule.map((p) => ({ path: p, detail: null })) },
|
|
8353
|
+
{
|
|
8354
|
+
label: "a symlink, which is never followed",
|
|
8355
|
+
rows: held.symlinked.map((p) => ({ path: p, detail: null }))
|
|
8356
|
+
}
|
|
8357
|
+
];
|
|
8358
|
+
}
|
|
8359
|
+
function describeHeldBack(held) {
|
|
8360
|
+
return [
|
|
8361
|
+
...heldBackGroups(held).flatMap(
|
|
8362
|
+
({ label, rows }) => rows.map(
|
|
8363
|
+
(r) => `${stripControl(r.path)} \u2014 ${label}${r.detail === null ? "" : ` (${stripControl(r.detail)})`}`
|
|
8364
|
+
)
|
|
8365
|
+
),
|
|
8366
|
+
...held.folders.map(
|
|
8367
|
+
(dir) => `${stripControl(dir)}/ \u2014 an excluded folder, whose contents were never walked`
|
|
8368
|
+
)
|
|
8369
|
+
];
|
|
8370
|
+
}
|
|
8371
|
+
function printHeldBack(held, wording) {
|
|
8372
|
+
if (held.total === 0 && held.folders.length === 0) return;
|
|
8373
|
+
const guessed = held.byName;
|
|
8374
|
+
const chosen = heldBackGroups(held).filter(({ label }) => label !== NAME_GUESS_LABEL);
|
|
8375
|
+
const chosenCount = chosen.reduce((n, g) => n + g.rows.length, 0);
|
|
8376
|
+
if (chosenCount > 0 || held.folders.length > 0) {
|
|
8377
|
+
printHuman(
|
|
8378
|
+
c.gray(
|
|
8379
|
+
chosenCount === 0 ? `
|
|
8380
|
+
Some notes are not in ${wording.where}, because your rules say so:` : `
|
|
8381
|
+
${chosenCount} note(s) are not in ${wording.where}, because your rules say so:`
|
|
8382
|
+
)
|
|
8383
|
+
);
|
|
8384
|
+
for (const { label, rows } of chosen) {
|
|
8385
|
+
if (rows.length === 0) continue;
|
|
8386
|
+
printHuman(c.gray(` ${rows.length} \u2014 ${label}:`));
|
|
8387
|
+
for (const r of rows.slice(0, 10)) printHuman(c.gray(` ${stripControl(r.path)}`));
|
|
8388
|
+
if (rows.length > 10) printHuman(c.gray(` \u2026 and ${rows.length - 10} more`));
|
|
8389
|
+
}
|
|
8390
|
+
if (held.folders.length > 0) {
|
|
8391
|
+
printHuman(
|
|
8392
|
+
c.gray(
|
|
8393
|
+
` ${held.folders.length} excluded folder(s) \u2014 their contents were not walked, so notes
|
|
8394
|
+
inside them are not counted above:`
|
|
8395
|
+
)
|
|
8396
|
+
);
|
|
8397
|
+
for (const dir of held.folders.slice(0, 10)) printHuman(c.gray(` ${stripControl(dir)}/`));
|
|
8398
|
+
if (held.folders.length > 10) {
|
|
8399
|
+
printHuman(c.gray(` \u2026 and ${held.folders.length - 10} more`));
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
}
|
|
8403
|
+
if (guessed.length > 0) {
|
|
8404
|
+
printHuman(
|
|
8405
|
+
c.yellow(
|
|
8406
|
+
`
|
|
8407
|
+
\u26A0 ${guessed.length} note(s) are not in ${wording.where}, held back on this machine by a GUESS about the name:`
|
|
8408
|
+
)
|
|
8409
|
+
);
|
|
8410
|
+
for (const d of guessed.slice(0, 10)) {
|
|
8411
|
+
printHuman(c.gray(` ${stripControl(d.path)} (matched "${stripControl(d.term ?? "?")}")`));
|
|
8412
|
+
}
|
|
8413
|
+
if (guessed.length > 10) printHuman(c.gray(` \u2026 and ${guessed.length - 10} more`));
|
|
8414
|
+
printHuman(
|
|
8415
|
+
c.gray(
|
|
8416
|
+
` This may be a false alarm \u2014 rename the note, or list it in .memgallow,
|
|
8417
|
+
and ${wording.retry}.`
|
|
8418
|
+
)
|
|
8419
|
+
);
|
|
8420
|
+
}
|
|
8421
|
+
}
|
|
8422
|
+
|
|
8273
8423
|
// src/lib/ledger.ts
|
|
8274
8424
|
init_deny();
|
|
8275
8425
|
init_brand();
|
|
@@ -8408,6 +8558,13 @@ var RULE_VERSION = 2;
|
|
|
8408
8558
|
var RATIONALE_MAX = 500;
|
|
8409
8559
|
function sanitizeRationale(raw) {
|
|
8410
8560
|
if (raw == null) return null;
|
|
8561
|
+
if (CONTROL_INTRODUCER.test(raw)) {
|
|
8562
|
+
throw memgError(
|
|
8563
|
+
"invalid_input",
|
|
8564
|
+
"the reason contains a terminal control sequence",
|
|
8565
|
+
"Nothing was saved. The text carries invisible formatting \u2014 usually from pasting out of a terminal, a chat window, or a file that was decoded wrongly \u2014 and stored prose that hides characters can read as something other than what it says. Type the same words in again as plain text and run it once more. Keep --reason: the reason is the part of a memory that still means anything later."
|
|
8566
|
+
);
|
|
8567
|
+
}
|
|
8411
8568
|
const clean = stripControl(raw).trim();
|
|
8412
8569
|
if (clean === "") return null;
|
|
8413
8570
|
const hits = secretContentHits(clean);
|
|
@@ -8598,14 +8755,17 @@ function reindexCommand() {
|
|
|
8598
8755
|
protectedVault: true
|
|
8599
8756
|
});
|
|
8600
8757
|
const scan = await adapter.scan();
|
|
8601
|
-
const
|
|
8758
|
+
const held = heldBack(scan);
|
|
8602
8759
|
printDual({
|
|
8603
8760
|
json: {
|
|
8604
8761
|
brain: brain.root,
|
|
8605
8762
|
indexed: built.entries.length,
|
|
8606
8763
|
cache_dir: built.cacheDir,
|
|
8607
8764
|
issues: built.issues,
|
|
8608
|
-
denied_by_name
|
|
8765
|
+
// Carries `denied_by_name` under the key it always had, plus the three
|
|
8766
|
+
// this command used to drop. The total is `not_indexed` rather than
|
|
8767
|
+
// push's `not_sent`: nothing is sent from here.
|
|
8768
|
+
...heldBackJson(held, "not_indexed"),
|
|
8609
8769
|
denied_by_content: built.denied.filter((d) => d.reason === "content")
|
|
8610
8770
|
},
|
|
8611
8771
|
human: () => {
|
|
@@ -8618,14 +8778,21 @@ function reindexCommand() {
|
|
|
8618
8778
|
)
|
|
8619
8779
|
);
|
|
8620
8780
|
}
|
|
8781
|
+
printHeldBack(held, { where: "the index", retry: "reindex" });
|
|
8621
8782
|
const byContent = built.denied.filter((d) => d.reason === "content");
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8783
|
+
if (byContent.length > 0) {
|
|
8784
|
+
printHuman(
|
|
8785
|
+
c.yellow(
|
|
8786
|
+
`
|
|
8787
|
+
\u26A0 ${byContent.length} note(s) refused \u2014 content looks like a credential:`
|
|
8788
|
+
)
|
|
8789
|
+
);
|
|
8790
|
+
for (const d of byContent.slice(0, 10)) {
|
|
8791
|
+
printHuman(c.gray(` ${stripControl(d.path)} (${stripControl(d.detail)})`));
|
|
8792
|
+
}
|
|
8793
|
+
if (byContent.length > 10) {
|
|
8794
|
+
printHuman(c.gray(` \u2026 and ${byContent.length - 10} more`));
|
|
8795
|
+
}
|
|
8629
8796
|
}
|
|
8630
8797
|
}
|
|
8631
8798
|
});
|
|
@@ -10061,11 +10228,17 @@ function proposalsCommand() {
|
|
|
10061
10228
|
opts.all === true ? { all: true } : {}
|
|
10062
10229
|
) : await listLocally(where.brain.root, opts.all === true);
|
|
10063
10230
|
const shown = listed.proposals;
|
|
10231
|
+
const unavailable = shown.filter(
|
|
10232
|
+
(p) => p.claim === void 0 && !carriesNoClaim(p.action)
|
|
10233
|
+
);
|
|
10234
|
+
const rows = shown.map(
|
|
10235
|
+
(p) => p.claim === void 0 && !carriesNoClaim(p.action) ? { ...p, claim: null, claim_unavailable: true } : p
|
|
10236
|
+
);
|
|
10064
10237
|
printDual({
|
|
10065
10238
|
json: {
|
|
10066
|
-
proposals:
|
|
10067
|
-
count:
|
|
10068
|
-
hint: hintForList(shown.length, listed.total, opts.all === true)
|
|
10239
|
+
proposals: rows,
|
|
10240
|
+
count: rows.length,
|
|
10241
|
+
hint: unavailable.length > 0 ? `${unavailable.length} of these came back without a claim because this brain's server is older than the CLI \u2014 they are marked \`claim_unavailable\`, and \`memgineering open <id>\` shows what each note says now. ${hintForList(shown.length, listed.total, opts.all === true)}` : hintForList(shown.length, listed.total, opts.all === true)
|
|
10069
10242
|
},
|
|
10070
10243
|
human: () => {
|
|
10071
10244
|
if (shown.length === 0) {
|
|
@@ -10106,6 +10279,7 @@ function summarize(p) {
|
|
|
10106
10279
|
action: p.action,
|
|
10107
10280
|
memory_id: p.memory_id,
|
|
10108
10281
|
target: p.target.path,
|
|
10282
|
+
claim: p.claim?.text ?? null,
|
|
10109
10283
|
reason: p.reason,
|
|
10110
10284
|
proposed_kind: p.proposed_kind,
|
|
10111
10285
|
supersedes: p.relations.supersedes,
|
|
@@ -10113,12 +10287,24 @@ function summarize(p) {
|
|
|
10113
10287
|
expires_at: p.expiresAt
|
|
10114
10288
|
};
|
|
10115
10289
|
}
|
|
10290
|
+
var CLAIM_WIDTH = 160;
|
|
10116
10291
|
function renderSummary(p) {
|
|
10292
|
+
const claimless = carriesNoClaim(p.action);
|
|
10293
|
+
const claim = p.claim === void 0 || p.claim === null ? null : oneLine(p.claim, CLAIM_WIDTH);
|
|
10294
|
+
const unavailable = p.claim === void 0 && !claimless;
|
|
10117
10295
|
const lines2 = [
|
|
10118
10296
|
`- **${stripControl(p.action)}**${p.proposed_kind ? ` \u2192 ${p.proposed_kind}` : ""} \`${stripControl(p.target)}\``,
|
|
10119
|
-
` ${p.id} ${p.state}
|
|
10120
|
-
quoteBlock(p.reason, " > ")
|
|
10297
|
+
` ${p.id} ${p.state}`
|
|
10121
10298
|
];
|
|
10299
|
+
if (claim !== null && claim !== "") lines2.push(` \u2192 ${claim}`);
|
|
10300
|
+
else if (unavailable) {
|
|
10301
|
+
lines2.push(
|
|
10302
|
+
c.gray(
|
|
10303
|
+
` \u2192 (claim unavailable: this brain's server is older than the CLI. \`memgineering open ${stripControl(p.memory_id ?? p.target)}\` shows what the note says NOW; upgrading the server restores the claim.)`
|
|
10304
|
+
)
|
|
10305
|
+
);
|
|
10306
|
+
}
|
|
10307
|
+
lines2.push(quoteBlock(p.reason, " > "));
|
|
10122
10308
|
if (p.supersedes.length > 0) {
|
|
10123
10309
|
lines2.push(c.gray(` replaces: ${p.supersedes.map(stripControl).join(", ")}`));
|
|
10124
10310
|
}
|
|
@@ -10466,14 +10652,25 @@ function consolidateCommand() {
|
|
|
10466
10652
|
});
|
|
10467
10653
|
});
|
|
10468
10654
|
}
|
|
10655
|
+
var TITLE_WIDTH = 72;
|
|
10656
|
+
function noteLines(id, title, path) {
|
|
10657
|
+
const shown = oneLine(title, TITLE_WIDTH);
|
|
10658
|
+
return [
|
|
10659
|
+
shown === "" ? ` ${stripControl(id)}` : ` ${stripControl(id)} ${shown}`,
|
|
10660
|
+
// Backticked, as it was before the title moved onto the row above it. This
|
|
10661
|
+
// output is read by agents as markdown, and a path outside code formatting
|
|
10662
|
+
// is a path they have to guess the extent of.
|
|
10663
|
+
c.gray(` \`${stripControl(path)}\``)
|
|
10664
|
+
];
|
|
10665
|
+
}
|
|
10469
10666
|
function renderCandidate(candidate) {
|
|
10470
10667
|
const lines2 = [
|
|
10471
10668
|
`- **${stripControl(candidate.rule)}** \u2014 ${stripControl(candidate.evidence)}`,
|
|
10472
|
-
|
|
10669
|
+
...noteLines(candidate.memory_id, candidate.title, candidate.path)
|
|
10473
10670
|
];
|
|
10474
10671
|
if (candidate.other_id !== null) {
|
|
10475
10672
|
lines2.push(
|
|
10476
|
-
|
|
10673
|
+
...noteLines(candidate.other_id, candidate.other_title ?? "", candidate.other_path ?? "")
|
|
10477
10674
|
);
|
|
10478
10675
|
}
|
|
10479
10676
|
lines2.push(
|
|
@@ -12142,6 +12339,11 @@ init_api_client();
|
|
|
12142
12339
|
import { Command as Command17 } from "commander";
|
|
12143
12340
|
init_vault();
|
|
12144
12341
|
init_ui();
|
|
12342
|
+
var HELD_BACK_WORDING = {
|
|
12343
|
+
where: "the hosted brain",
|
|
12344
|
+
retry: "push again"
|
|
12345
|
+
};
|
|
12346
|
+
var printHeldBack2 = (held) => printHeldBack(held, HELD_BACK_WORDING);
|
|
12145
12347
|
function pushCommand() {
|
|
12146
12348
|
return new Command17("push").description("upload the brain on this machine to your hosted one").option("--vault <path>", "which local brain to upload").option("--brain <name>", "the hosted brain to upload into (created if it does not exist)").option("--dry-run", "list what would be uploaded and send nothing").action(async (opts) => {
|
|
12147
12349
|
const { brain: local } = await resolveBrain(opts.vault);
|
|
@@ -12150,11 +12352,16 @@ function pushCommand() {
|
|
|
12150
12352
|
const paths = scan.files.filter(isNote);
|
|
12151
12353
|
const held = heldBack(scan);
|
|
12152
12354
|
if (paths.length === 0) {
|
|
12355
|
+
const sawFiles = scan.files.length > 0;
|
|
12356
|
+
const excluded = describeHeldBack(held).slice(0, 10).map((row) => ` ${row}`).join("\n");
|
|
12153
12357
|
throw memgError(
|
|
12154
12358
|
"invalid_input",
|
|
12155
12359
|
`there is nothing to upload in ${local.root}`,
|
|
12156
|
-
held.total
|
|
12157
|
-
|
|
12360
|
+
held.total > 0 ? `This brain has ${held.total} markdown note(s) and the rules on this machine hold back every one:
|
|
12361
|
+
${excluded}
|
|
12362
|
+
Rename one, or list it in .memgallow, if the name is a false alarm.` : held.folders.length > 0 ? `No markdown notes were found. ${held.folders.length} folder(s) were excluded by a rule and never looked inside:
|
|
12363
|
+
${excluded}
|
|
12364
|
+
If your notes are in one of those, that rule is why \u2014 nothing here can see them. If they are somewhere else, check the path: \`--vault\` takes the folder your notes live in.` : sawFiles ? "There are files here but no markdown notes. Check the path \u2014 `--vault` takes the folder your notes live in." : "This folder is empty. Check the path \u2014 `--vault` takes the folder your notes live in."
|
|
12158
12365
|
);
|
|
12159
12366
|
}
|
|
12160
12367
|
if (opts.dryRun) {
|
|
@@ -12174,7 +12381,7 @@ function pushCommand() {
|
|
|
12174
12381
|
`);
|
|
12175
12382
|
for (const path of paths.slice(0, 20)) printHuman(c.gray(` ${path}`));
|
|
12176
12383
|
if (paths.length > 20) printHuman(c.gray(` \u2026 and ${paths.length - 20} more`));
|
|
12177
|
-
|
|
12384
|
+
printHeldBack2(held);
|
|
12178
12385
|
printHuman(
|
|
12179
12386
|
c.gray(
|
|
12180
12387
|
"\nNothing was sent. What a dry run cannot know is the other screen: a note\nwhose CONTENT looks like a credential is held back \u2014 by this machine before\nsending, and by the server on arrival \u2014 and deciding that needs the bytes,\nwhich this does not read. Those appear in the real run\u2019s summary."
|
|
@@ -12279,7 +12486,7 @@ function pushCommand() {
|
|
|
12279
12486
|
printHuman(c.gray(` \u2026 and ${refused.length - 10} more`));
|
|
12280
12487
|
}
|
|
12281
12488
|
}
|
|
12282
|
-
|
|
12489
|
+
printHeldBack2(held);
|
|
12283
12490
|
printRepoint(repointed, destination);
|
|
12284
12491
|
printHuman(
|
|
12285
12492
|
c.gray(
|
|
@@ -12317,82 +12524,6 @@ brain before. \`memgineering recall --local\` still reads the folder.`
|
|
|
12317
12524
|
)
|
|
12318
12525
|
);
|
|
12319
12526
|
}
|
|
12320
|
-
var isNote = (p) => /\.(md|markdown)$/i.test(p);
|
|
12321
|
-
function heldBack(scan) {
|
|
12322
|
-
const denied = scan.denied ?? [];
|
|
12323
|
-
const byName = denied.filter((d) => d.kind === "name" && isNote(d.path)).map((d) => ({ path: d.path, term: d.term }));
|
|
12324
|
-
const explicitly = denied.filter((d) => d.kind === "explicit" && isNote(d.path)).map((d) => d.path);
|
|
12325
|
-
const byRule = scan.excluded.filter(isNote);
|
|
12326
|
-
const symlinked = scan.skippedSymlinks.filter(isNote);
|
|
12327
|
-
return {
|
|
12328
|
-
total: byName.length + explicitly.length + byRule.length + symlinked.length,
|
|
12329
|
-
byName,
|
|
12330
|
-
explicitly,
|
|
12331
|
-
byRule,
|
|
12332
|
-
symlinked
|
|
12333
|
-
};
|
|
12334
|
-
}
|
|
12335
|
-
function heldBackJson(held) {
|
|
12336
|
-
return {
|
|
12337
|
-
not_sent: held.total,
|
|
12338
|
-
denied_by_name: held.byName,
|
|
12339
|
-
denied_explicitly: held.explicitly,
|
|
12340
|
-
excluded_by_rule: held.byRule,
|
|
12341
|
-
skipped_symlinks: held.symlinked
|
|
12342
|
-
};
|
|
12343
|
-
}
|
|
12344
|
-
function heldBackGroups(held) {
|
|
12345
|
-
return [
|
|
12346
|
-
{
|
|
12347
|
-
label: "name looks private",
|
|
12348
|
-
rows: held.byName.map((d) => ({ path: d.path, detail: `matched "${d.term ?? "?"}"` }))
|
|
12349
|
-
},
|
|
12350
|
-
{ label: "listed in .memgdeny", rows: held.explicitly.map((p) => ({ path: p, detail: null })) },
|
|
12351
|
-
{ label: "excluded by a rule", rows: held.byRule.map((p) => ({ path: p, detail: null })) },
|
|
12352
|
-
{
|
|
12353
|
-
label: "a symlink, which is never followed",
|
|
12354
|
-
rows: held.symlinked.map((p) => ({ path: p, detail: null }))
|
|
12355
|
-
}
|
|
12356
|
-
];
|
|
12357
|
-
}
|
|
12358
|
-
function describeHeldBack(held) {
|
|
12359
|
-
return heldBackGroups(held).flatMap(
|
|
12360
|
-
({ label, rows }) => rows.map(
|
|
12361
|
-
(r) => `${stripControl(r.path)} \u2014 ${label}${r.detail === null ? "" : ` (${stripControl(r.detail)})`}`
|
|
12362
|
-
)
|
|
12363
|
-
);
|
|
12364
|
-
}
|
|
12365
|
-
function printHeldBack(held) {
|
|
12366
|
-
if (held.total === 0) return;
|
|
12367
|
-
printHuman(
|
|
12368
|
-
c.yellow(`
|
|
12369
|
-
\u26A0 ${held.total} note(s) held back on this machine \u2014 they never reach the brain:`)
|
|
12370
|
-
);
|
|
12371
|
-
for (const { label, rows } of heldBackGroups(held)) {
|
|
12372
|
-
if (rows.length === 0) continue;
|
|
12373
|
-
printHuman(c.gray(` ${rows.length} \u2014 ${label}:`));
|
|
12374
|
-
for (const r of rows.slice(0, 10)) {
|
|
12375
|
-
printHuman(
|
|
12376
|
-
c.gray(
|
|
12377
|
-
` ${stripControl(r.path)}${r.detail === null ? "" : ` (${stripControl(r.detail)})`}`
|
|
12378
|
-
)
|
|
12379
|
-
);
|
|
12380
|
-
}
|
|
12381
|
-
if (rows.length > 10) printHuman(c.gray(` \u2026 and ${rows.length - 10} more`));
|
|
12382
|
-
}
|
|
12383
|
-
if (held.byName.length > 0) {
|
|
12384
|
-
printHuman(
|
|
12385
|
-
c.gray(
|
|
12386
|
-
" The name list is a GUESS and this may be a false alarm \u2014 rename the note,\n or list it in .memgallow, and push again."
|
|
12387
|
-
)
|
|
12388
|
-
);
|
|
12389
|
-
}
|
|
12390
|
-
printHuman(
|
|
12391
|
-
c.gray(
|
|
12392
|
-
" Counted per file. A rule that excludes a whole FOLDER stops the walk there,\n so notes inside one are not listed above."
|
|
12393
|
-
)
|
|
12394
|
-
);
|
|
12395
|
-
}
|
|
12396
12527
|
async function resolveDestination(named, root) {
|
|
12397
12528
|
const base = apiUrl();
|
|
12398
12529
|
const credentials = await requireCredentials(base);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memgineering",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "One memory for the AI you connect. Recall, remember, and revise a brain your agents share
|
|
5
|
+
"description": "One memory for the AI you connect. Recall, remember, and revise a brain your agents share \u2014 stored in your own folder.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"homepage": "https://memgineering.com",
|
|
8
8
|
"repository": {
|