memgineering 0.7.4 → 0.8.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/dist/index.js CHANGED
@@ -15,6 +15,9 @@ import { join } from "path";
15
15
  function brandHome() {
16
16
  return process.env[BRAND.homeEnvVar] ?? join(homedir(), BRAND.homeDirName);
17
17
  }
18
+ function agentHome() {
19
+ return process.env[BRAND.agentHomeEnvVar] ?? homedir();
20
+ }
18
21
  var BRAND;
19
22
  var init_brand = __esm({
20
23
  "src/brand.ts"() {
@@ -28,6 +31,16 @@ var init_brand = __esm({
28
31
  * real `~/.memgineering`.
29
32
  */
30
33
  homeEnvVar: "MEMGINEERING_HOME",
34
+ /**
35
+ * Where `setup` installs its guidance — the AGENT tools' homes, not ours.
36
+ *
37
+ * A separate variable from `MEMGINEERING_HOME` because they are separate
38
+ * things: that one moves this tool's own state, and this one moves the files
39
+ * it writes into somebody else's `~/.claude` and `~/.codex`. Conflating them
40
+ * would mean anyone relocating their state dir silently stopped installing
41
+ * into their real agents.
42
+ */
43
+ agentHomeEnvVar: "MEMGINEERING_AGENT_HOME",
31
44
  /** Force `--json` framing without passing the flag (used by wrappers). */
32
45
  jsonEnvVar: "MEMGINEERING_JSON",
33
46
  /**
@@ -120,32 +133,46 @@ function stripControl(input) {
120
133
  let out = "";
121
134
  for (let i = 0; i < input.length; i++) {
122
135
  const code = input.charCodeAt(i);
123
- if (code === 27) {
124
- const next = input[i + 1];
125
- if (next === "]") {
126
- let j = i + 2;
127
- while (j < input.length && input.charCodeAt(j) !== 7 && !(input.charCodeAt(j) === 27 && input[j + 1] === "\\")) {
128
- j++;
129
- }
130
- if (input.charCodeAt(j) === 7) j++;
131
- else if (input.charCodeAt(j) === 27) j += 2;
132
- i = j - 1;
136
+ if (code === 27 && input[i + 1] === "]" || code === 157) {
137
+ const from = code === 157 ? i + 1 : i + 2;
138
+ let j = from;
139
+ while (j < input.length && input.charCodeAt(j) !== 7 && input.charCodeAt(j) !== 156 && !(input.charCodeAt(j) === 27 && input[j + 1] === "\\")) {
140
+ j++;
141
+ }
142
+ if (j >= input.length) {
143
+ i = from - 1;
133
144
  continue;
134
145
  }
135
- if (next === "[") {
136
- let j = i + 2;
137
- while (j < input.length && !(input.charCodeAt(j) >= 64 && input.charCodeAt(j) <= 126)) {
138
- j++;
139
- }
146
+ j += input.charCodeAt(j) === 27 ? 2 : 1;
147
+ i = j - 1;
148
+ continue;
149
+ }
150
+ if (code === 27 && input[i + 1] === "[" || code === 155) {
151
+ const from = code === 155 ? i + 1 : i + 2;
152
+ let j = from;
153
+ while (j < input.length && input.charCodeAt(j) >= 48 && input.charCodeAt(j) <= 63) j++;
154
+ while (j < input.length && input.charCodeAt(j) >= 32 && input.charCodeAt(j) <= 47) j++;
155
+ const final = j < input.length ? input.charCodeAt(j) : -1;
156
+ if (final >= 64 && final <= 126) {
140
157
  i = j;
141
158
  continue;
142
159
  }
160
+ i = from - 1;
161
+ continue;
162
+ }
163
+ if (code === 27) {
143
164
  i += 1;
144
165
  continue;
145
166
  }
146
- if (code <= 31 && code !== 10 && code !== 9 || code === 127) {
167
+ if (code === 133 || code === 8232 || code === 8233) {
168
+ out += "\n";
147
169
  continue;
148
170
  }
171
+ if (code <= 31 && code !== 10 && code !== 9 || code === 127) continue;
172
+ if (code >= 128 && code <= 159) continue;
173
+ if (code === 1564 || code === 8206 || code === 8207) continue;
174
+ if (code >= 8234 && code <= 8238) continue;
175
+ if (code >= 8294 && code <= 8297) continue;
149
176
  out += input[i];
150
177
  }
151
178
  return out;
@@ -153,6 +180,15 @@ function stripControl(input) {
153
180
  function quoteBlock(text, prefix = "> ") {
154
181
  return stripControl(text).split("\n").map((line) => prefix + line).join("\n");
155
182
  }
183
+ function oneLine(text, max) {
184
+ const flat = stripControl(text).replace(/\s+/g, " ").trim();
185
+ const chars = [...flat];
186
+ if (chars.length <= max) return flat;
187
+ return `${chars.slice(0, Math.max(1, max - 1)).join("").trimEnd()}\u2026`;
188
+ }
189
+ function printBlock(text) {
190
+ printHuman(stripControl(text));
191
+ }
156
192
  function progress(text) {
157
193
  if (isInteractive()) process.stderr.write(kleur.gray(` ${text}
158
194
  `));
@@ -167,12 +203,23 @@ function undoHint(opId, vault) {
167
203
  function quoteArg(value) {
168
204
  return /^[A-Za-z0-9._\/-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
169
205
  }
170
- var jsonMode, c;
206
+ var jsonMode, CONTROL_INTRODUCER, WIDTH, c;
171
207
  var init_ui = __esm({
172
208
  "src/ui.ts"() {
173
209
  "use strict";
174
210
  init_brand();
175
211
  jsonMode = false;
212
+ CONTROL_INTRODUCER = /[\u001b\u009b\u009d]/;
213
+ WIDTH = {
214
+ /** A brain name, a note title — a heading-shaped thing. */
215
+ title: 72,
216
+ /** A vault path. Longer, because the tail is the part that identifies it. */
217
+ path: 120,
218
+ /** An id or an `open:` handle. */
219
+ handle: 64,
220
+ /** A claim, a summary, a server hint — a sentence rather than a label. */
221
+ text: 200
222
+ };
176
223
  c = kleur;
177
224
  }
178
225
  });
@@ -969,10 +1016,12 @@ function secretContentHits(text) {
969
1016
  if (pattern.test(text)) hits.push({ code });
970
1017
  }
971
1018
  const plain = stripMarkdownEmphasis(text);
972
- if (hasLabelledCredential(text) || hasLabelledCredential(plain)) {
1019
+ const bare = withoutIntroducers(text);
1020
+ const bothWays = (check) => check(text) || check(plain) || bare !== text && (check(bare) || check(stripMarkdownEmphasis(bare)));
1021
+ if (bothWays(hasLabelledCredential)) {
973
1022
  hits.push({ code: "labelled_credential" });
974
1023
  }
975
- if (hasSpacedCredential(text) || hasSpacedCredential(plain)) {
1024
+ if (bothWays(hasSpacedCredential)) {
976
1025
  hits.push({ code: "spaced_credential" });
977
1026
  }
978
1027
  if (hasCredentialNearPrivateIp(text)) hits.push({ code: "credential_near_private_ip" });
@@ -1003,7 +1052,7 @@ function isExplicitlyDenied(relPath, deny = EMPTY_DENY_LIST) {
1003
1052
  function isNameAllowlisted(relPath, allow = EMPTY_DENY_LIST) {
1004
1053
  return isExplicitlyDenied(relPath, allow);
1005
1054
  }
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;
1055
+ 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
1056
  var init_deny = __esm({
1008
1057
  "../../packages/storage-adapter/src/local-file/deny.ts"() {
1009
1058
  "use strict";
@@ -1123,6 +1172,12 @@ var init_deny = __esm({
1123
1172
  "credential_table"
1124
1173
  ]);
1125
1174
  isWeakSecretCode = (code) => WEAK_CODES.has(code);
1175
+ withoutIntroducers = (text) => (
1176
+ // `ESC [` and `ESC ]` are two characters, so removing the ESC alone would
1177
+ // leave a bracket where the hidden byte was and the label would still not
1178
+ // read as itself.
1179
+ text.replace(/\u001b[[\]]?|[\u009b\u009d]/g, "")
1180
+ );
1126
1181
  EMPTY_DENY_LIST = { entries: [] };
1127
1182
  }
1128
1183
  });
@@ -3129,6 +3184,7 @@ async function walkVault(root, opts = {}) {
3129
3184
  const files2 = [];
3130
3185
  const skippedSymlinks = [];
3131
3186
  const excluded = [];
3187
+ const excludedDirs = [];
3132
3188
  const visit = async (dirAbs, relDir, depth) => {
3133
3189
  if (depth > maxDepth) return;
3134
3190
  let entries;
@@ -3146,6 +3202,7 @@ async function walkVault(root, opts = {}) {
3146
3202
  const isDir = entry.isDirectory();
3147
3203
  if (isExcluded(rel, isDir, opts.ignore)) {
3148
3204
  excluded.push(rel);
3205
+ if (isDir) excludedDirs.push(rel);
3149
3206
  continue;
3150
3207
  }
3151
3208
  if (isDir) {
@@ -3157,7 +3214,7 @@ async function walkVault(root, opts = {}) {
3157
3214
  };
3158
3215
  await visit(rootAbs, "", 0);
3159
3216
  files2.sort();
3160
- return { files: files2, skippedSymlinks, excluded };
3217
+ return { files: files2, skippedSymlinks, excluded, excludedDirs };
3161
3218
  }
3162
3219
  function resolveInside(root, relPath) {
3163
3220
  if (relPath === "" || relPath.startsWith("/") || relPath.includes("\0")) {
@@ -4399,9 +4456,11 @@ function linkCommand() {
4399
4456
  Nothing was indexed. This is what WOULD be stored:
4400
4457
  `));
4401
4458
  for (const r of rows) {
4402
- printHuman(` ${c.bold(stripControl(r.title))}`);
4403
- printHuman(c.gray(` ${stripControl(r.summary ?? "(no summary \u2014 title only)")}`));
4404
- printHuman(c.gray(` from ${stripControl(r.path)}`));
4459
+ printHuman(` ${c.bold(oneLine(r.title, WIDTH.title))}`);
4460
+ printHuman(
4461
+ c.gray(` ${oneLine(r.summary ?? "(no summary \u2014 title only)", WIDTH.text)}`)
4462
+ );
4463
+ printHuman(c.gray(` from ${oneLine(r.path, WIDTH.path)}`));
4405
4464
  }
4406
4465
  printHuman(
4407
4466
  c.gray(`
@@ -4674,8 +4733,8 @@ Brain: ${root}`));
4674
4733
  if (samples.length > 0) {
4675
4734
  printHuman(c.gray("\n For example, these exact lines would be stored:"));
4676
4735
  for (const r of samples) {
4677
- printHuman(c.gray(` ${stripControl(r.title)}`));
4678
- printHuman(c.gray(` ${stripControl(r.summary ?? "(no summary \u2014 title only)")}`));
4736
+ printHuman(c.gray(` ${oneLine(r.title, WIDTH.title)}`));
4737
+ printHuman(c.gray(` ${oneLine(r.summary ?? "(no summary \u2014 title only)", WIDTH.text)}`));
4679
4738
  }
4680
4739
  if (notes.length > samples.length) {
4681
4740
  printHuman(
@@ -4795,7 +4854,7 @@ function loginCommand() {
4795
4854
  },
4796
4855
  human: () => {
4797
4856
  printHuman(
4798
- `Already signed in${already.display_name ? ` as ${c.bold(stripControl(already.display_name))}` : ""}.`
4857
+ `Already signed in${already.display_name ? ` as ${c.bold(oneLine(already.display_name, WIDTH.title))}` : ""}.`
4799
4858
  );
4800
4859
  printHuman(c.gray(" `memgineering logout` first if you want a different account."));
4801
4860
  }
@@ -4868,13 +4927,13 @@ function emit(pending, ctx) {
4868
4927
  },
4869
4928
  human: () => {
4870
4929
  printHuman("Show this code to whoever is signing in:\n");
4871
- printHuman(` ${c.bold().yellow(stripControl(pending.user_code))}
4930
+ printHuman(` ${c.bold().yellow(oneLine(pending.user_code, WIDTH.handle))}
4872
4931
  `);
4873
4932
  if (ctx.opened) {
4874
4933
  printHuman("Opened the approval page in your browser.");
4875
4934
  } else {
4876
4935
  printHuman("Open this to approve:\n");
4877
- printHuman(` ${c.cyan(stripControl(pending.verification_url))}`);
4936
+ printHuman(` ${c.cyan(oneLine(pending.verification_url, WIDTH.path))}`);
4878
4937
  }
4879
4938
  printHuman(
4880
4939
  c.gray(
@@ -4882,7 +4941,7 @@ function emit(pending, ctx) {
4882
4941
  )
4883
4942
  );
4884
4943
  if (ctx.opened && ctx.showUrl) {
4885
- printHuman(c.gray(` ${stripControl(pending.verification_url)}`));
4944
+ printHuman(c.gray(` ${oneLine(pending.verification_url, WIDTH.path)}`));
4886
4945
  }
4887
4946
  printHuman(
4888
4947
  c.gray(
@@ -4924,7 +4983,7 @@ async function waitForApproval(pending, base) {
4924
4983
  human: () => {
4925
4984
  printHuman(
4926
4985
  `
4927
- Signed in${claimed.display_name ? ` as ${c.bold(stripControl(claimed.display_name))}` : ""}.`
4986
+ Signed in${claimed.display_name ? ` as ${c.bold(oneLine(claimed.display_name, WIDTH.title))}` : ""}.`
4928
4987
  );
4929
4988
  printHuman(c.gray(" The token is stored on this machine only, readable by you alone."));
4930
4989
  }
@@ -5025,17 +5084,19 @@ function printSignInStep(step) {
5025
5084
  switch (step.status) {
5026
5085
  case "signed-in":
5027
5086
  printHuman(
5028
- c.gray(` signed in${step.displayName ? ` as ${stripControl(step.displayName)}` : ""}
5029
- `)
5087
+ c.gray(
5088
+ ` signed in${step.displayName ? ` as ${oneLine(step.displayName, WIDTH.title)}` : ""}
5089
+ `
5090
+ )
5030
5091
  );
5031
5092
  return;
5032
5093
  case "started":
5033
5094
  printHuman("Show this code to whoever is signing in:\n");
5034
- printHuman(` ${c.bold().yellow(stripControl(step.userCode))}
5095
+ printHuman(` ${c.bold().yellow(oneLine(step.userCode, WIDTH.handle))}
5035
5096
  `);
5036
5097
  printHuman(
5037
5098
  step.opened ? c.gray(" The approval page is open in the browser.") : `Open this to approve:
5038
- ${c.cyan(stripControl(step.verificationUrl))}`
5099
+ ${c.cyan(oneLine(step.verificationUrl, WIDTH.path))}`
5039
5100
  );
5040
5101
  printHuman(
5041
5102
  c.gray(" Not waiting \u2014 setup carries on, and the next command collects the token.\n")
@@ -5043,12 +5104,14 @@ function printSignInStep(step) {
5043
5104
  return;
5044
5105
  case "pending":
5045
5106
  printHuman(
5046
- c.gray(` a sign-in is waiting for approval \u2014 code ${stripControl(step.userCode)}
5047
- `)
5107
+ c.gray(
5108
+ ` a sign-in is waiting for approval \u2014 code ${oneLine(step.userCode, WIDTH.handle)}
5109
+ `
5110
+ )
5048
5111
  );
5049
5112
  return;
5050
5113
  case "unavailable":
5051
- printHuman(c.yellow(` Could not start a sign-in: ${stripControl(step.because)}`));
5114
+ printHuman(c.yellow(` Could not start a sign-in: ${oneLine(step.because, WIDTH.text)}`));
5052
5115
  printHuman(c.gray(" Setup continues \u2014 an account is not needed for any of it.\n"));
5053
5116
  return;
5054
5117
  case "would-start":
@@ -7356,11 +7419,11 @@ var init_setup_web = __esm({
7356
7419
  // src/commands/setup.ts
7357
7420
  import { createHash as createHash8 } from "crypto";
7358
7421
  import { access, mkdir as mkdir13, readFile as readFile19 } from "fs/promises";
7359
- import { homedir as homedir4 } from "os";
7360
7422
  import { dirname as dirname5, join as join19 } from "path";
7361
7423
  import { fileURLToPath } from "url";
7362
7424
  import { Command as Command22 } from "commander";
7363
7425
  import prompts3 from "prompts";
7426
+ import { homedir as homedir4 } from "os";
7364
7427
  function setupCommand() {
7365
7428
  return new Command22("setup").description("install into your agents, and point at a brain (--dry-run to only report)").option("--agent", "non-interactive: take every answer from flags").option("--human", "interactive: choose from a list").option("--web", "open a screen in the browser and answer it there").option("--print-url", "with --web: print the URL instead of opening a browser").option(
7366
7429
  "--tools <list>",
@@ -7432,6 +7495,18 @@ function setupCommand() {
7432
7495
  ...signInStepJson(signIn),
7433
7496
  defaults_used: defaultsTaken,
7434
7497
  dry_run: opts.dryRun === true,
7498
+ /*
7499
+ * The home these paths are relative TO, and it is only here when it
7500
+ * is not the ordinary one.
7501
+ *
7502
+ * Every path in `changes` is reported relative to the home being
7503
+ * installed into, so `.claude/skills/…` names a file without saying
7504
+ * whose. That ambiguity is why an agent writing into a real machine
7505
+ * from inside a sandbox had nothing in front of it that looked
7506
+ * wrong. Present only when scoped, so the common case stays quiet
7507
+ * and the unusual one cannot be mistaken for it.
7508
+ */
7509
+ ...agentHome() !== homedir4() ? { agent_home: agentHome() } : {},
7435
7510
  tools: choice.tools.map((t) => t.id),
7436
7511
  auto_update: choice.autoUpdate,
7437
7512
  hook: choice.hook,
@@ -7455,9 +7530,27 @@ function setupCommand() {
7455
7530
  if (opts.silent && changed.length === 0) return;
7456
7531
  if (opts.dryRun) {
7457
7532
  printSignInStep(signIn);
7458
- printHuman(`Would change ${changed.length} file(s):
7533
+ if (changed.length === 0) {
7534
+ printHuman(
7535
+ `Nothing to change \u2014 ${choice.tools.map((t) => t.label).join(", ")} ${choice.tools.length === 1 ? "is" : "are"} already up to date.
7536
+ `
7537
+ );
7538
+ printHuman(
7539
+ c.gray(
7540
+ ` ${results.length} file(s) checked. Run without \`--dry-run\` to reinstall them anyway.`
7541
+ )
7542
+ );
7543
+ return;
7544
+ }
7545
+ printHuman(`Would change ${changed.length} of ${results.length} file(s):
7459
7546
  `);
7460
7547
  for (const r of changed) printHuman(c.gray(` ${r.tool} \xB7 ${r.what}`));
7548
+ printHuman(
7549
+ c.gray(
7550
+ `
7551
+ The rest are already up to date. Tools found: ${choice.tools.map((t) => t.label).join(", ")}.`
7552
+ )
7553
+ );
7461
7554
  return;
7462
7555
  }
7463
7556
  printSignInStep(signIn);
@@ -7558,7 +7651,7 @@ async function applySetup(choice, dryRun) {
7558
7651
  const hub = await loadAsset(BRAND.hubFileName);
7559
7652
  const installed = { ...(await loadConfig()).system.installed };
7560
7653
  const mark = (path, hash) => {
7561
- const key2 = path.startsWith(homedir4()) ? path.slice(homedir4().length + 1) : path;
7654
+ const key2 = path.startsWith(agentHome()) ? path.slice(agentHome().length + 1) : path;
7562
7655
  installed[key2] = hash;
7563
7656
  };
7564
7657
  const persist = async () => {
@@ -7570,11 +7663,11 @@ async function applySetup(choice, dryRun) {
7570
7663
  });
7571
7664
  };
7572
7665
  const wasEdited = (path, onDisk) => {
7573
- const key2 = path.startsWith(homedir4()) ? path.slice(homedir4().length + 1) : path;
7666
+ const key2 = path.startsWith(agentHome()) ? path.slice(agentHome().length + 1) : path;
7574
7667
  const known = installed[key2];
7575
7668
  return known !== void 0 && onDisk !== null && fingerprint(onDisk) !== known;
7576
7669
  };
7577
- const keyOf = (path) => path.startsWith(homedir4()) ? path.slice(homedir4().length + 1) : path;
7670
+ const keyOf = (path) => path.startsWith(agentHome()) ? path.slice(agentHome().length + 1) : path;
7578
7671
  try {
7579
7672
  return await write();
7580
7673
  } finally {
@@ -7582,7 +7675,7 @@ async function applySetup(choice, dryRun) {
7582
7675
  }
7583
7676
  async function write() {
7584
7677
  for (const tool of choice.tools) {
7585
- const hubPath = join19(homedir4(), tool.hubFile);
7678
+ const hubPath = join19(agentHome(), tool.hubFile);
7586
7679
  const written = await injectBlock(
7587
7680
  hubPath,
7588
7681
  hub + tailFor(tool),
@@ -7598,7 +7691,7 @@ async function applySetup(choice, dryRun) {
7598
7691
  if (!dryRun && written.after !== null) mark(hubPath, written.after);
7599
7692
  for (const skill of BRAND.subSkills) {
7600
7693
  const content = await loadAsset(join19(skill, "SKILL.md"));
7601
- const path = join19(homedir4(), tool.skillsDir, skill, "SKILL.md");
7694
+ const path = join19(agentHome(), tool.skillsDir, skill, "SKILL.md");
7602
7695
  const out = await writeIfDifferent(path, content, dryRun);
7603
7696
  results.push({
7604
7697
  tool: tool.label,
@@ -7668,7 +7761,7 @@ async function resolveMode(opts) {
7668
7761
  return { kind: mode ?? "agent", chosen: mode !== void 0 };
7669
7762
  }
7670
7763
  async function detectTools() {
7671
- const home = homedir4();
7764
+ const home = agentHome();
7672
7765
  const found = [];
7673
7766
  for (const tool of TOOLS) {
7674
7767
  if (await exists2(join19(home, tool.dir))) found.push(tool);
@@ -7801,7 +7894,7 @@ async function writeIfDifferent(filePath, content, dryRun) {
7801
7894
  return { outcome: existing === null ? "injected" : "updated", before: existing, after };
7802
7895
  }
7803
7896
  async function installHook(spec, tool, dryRun) {
7804
- const settingsPath = join19(homedir4(), ...(tool.hooksFile ?? "").split("/"));
7897
+ const settingsPath = join19(agentHome(), ...(tool.hooksFile ?? "").split("/"));
7805
7898
  const raw = await readFile19(settingsPath, "utf8").catch(() => null);
7806
7899
  let settings = {};
7807
7900
  if (raw !== null) {
@@ -7851,7 +7944,7 @@ async function installHook(spec, tool, dryRun) {
7851
7944
  return raw === null ? "injected" : "updated";
7852
7945
  }
7853
7946
  async function removeHook(spec, tool, dryRun) {
7854
- const settingsPath = join19(homedir4(), ...(tool.hooksFile ?? "").split("/"));
7947
+ const settingsPath = join19(agentHome(), ...(tool.hooksFile ?? "").split("/"));
7855
7948
  const raw = await readFile19(settingsPath, "utf8").catch(() => null);
7856
7949
  if (raw === null) return "unchanged";
7857
7950
  let parsed;
@@ -8062,7 +8155,7 @@ For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around t
8062
8155
  });
8063
8156
 
8064
8157
  // src/program.ts
8065
- import { Command as Command27 } from "commander";
8158
+ import { Command as Command28 } from "commander";
8066
8159
 
8067
8160
  // src/help.ts
8068
8161
  init_ui();
@@ -8086,7 +8179,7 @@ var COMMAND_GROUPS = [
8086
8179
  {
8087
8180
  title: "ACCOUNT",
8088
8181
  blurb: "optional \u2014 connect a hosted brain; everything above works without it",
8089
- commands: ["login", "logout", "whoami", "push", "pull"]
8182
+ commands: ["login", "logout", "whoami", "brains", "push", "pull"]
8090
8183
  },
8091
8184
  {
8092
8185
  title: "SYSTEM",
@@ -8099,7 +8192,12 @@ var ENV_ROWS = [
8099
8192
  ["MEMGINEERING_HOME=<dir>", "state + derived index (default: ~/.memgineering)"],
8100
8193
  ["MEMGINEERING_JSON=1", "force --json framing without the flag"],
8101
8194
  ["MEMGINEERING_NO_UPDATE=1", "skip the update check for this run"],
8102
- ["MEMGINEERING_API_URL=<url>", "hosted brain server (default: https://api.memgineering.com)"]
8195
+ ["MEMGINEERING_API_URL=<url>", "hosted brain server (default: https://api.memgineering.com)"],
8196
+ // The one that keeps `setup` off a real machine, and it was missing from this
8197
+ // list while the other four were here. An agent testing this CLI in a sandbox
8198
+ // had no documented handle for where `setup` writes — which is how one of them
8199
+ // overwrote the live skill files of the session testing it.
8200
+ ["MEMGINEERING_AGENT_HOME=<dir>", "where `setup` installs (default: your home)"]
8103
8201
  ];
8104
8202
  function commandLabel(sub) {
8105
8203
  const args = sub.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
@@ -8252,12 +8350,13 @@ var cloudRules = (brain, args = {}) => call(brain, "POST", "/rules", {
8252
8350
  var cloudReindex = (brain, args = {}) => call(brain, "POST", "/reindex", {
8253
8351
  ...args.rebuild === true ? { rebuild: true } : {}
8254
8352
  });
8255
- async function listCloudBrains(token, baseUrl) {
8353
+ async function listCloudBrains(token, baseUrl, opts = {}) {
8256
8354
  const res = await apiRequest({
8257
8355
  method: "GET",
8258
8356
  path: "/v1/brains",
8259
8357
  token,
8260
- baseUrl
8358
+ baseUrl,
8359
+ ...opts.retries === void 0 ? {} : { retries: opts.retries }
8261
8360
  });
8262
8361
  return res.body?.brains ?? [];
8263
8362
  }
@@ -8275,6 +8374,125 @@ async function createCloudBrain(token, baseUrl, name) {
8275
8374
  // src/commands/brain-admin.ts
8276
8375
  init_config();
8277
8376
 
8377
+ // src/lib/held-back.ts
8378
+ init_ui();
8379
+ var isNote = (p) => /\.(md|markdown)$/i.test(p);
8380
+ function heldBack(scan) {
8381
+ const denied = scan.denied ?? [];
8382
+ const byName = denied.filter((d) => d.kind === "name" && isNote(d.path)).map((d) => ({ path: d.path, term: d.term }));
8383
+ const explicitly = denied.filter((d) => d.kind === "explicit" && isNote(d.path)).map((d) => d.path);
8384
+ const byRule = scan.excluded.filter(isNote);
8385
+ const symlinked = scan.skippedSymlinks.filter(isNote);
8386
+ return {
8387
+ total: byName.length + explicitly.length + byRule.length + symlinked.length,
8388
+ folders: scan.excludedDirs ?? [],
8389
+ byName,
8390
+ explicitly,
8391
+ byRule,
8392
+ symlinked
8393
+ };
8394
+ }
8395
+ function heldBackJson(held, totalKey = "not_sent") {
8396
+ return {
8397
+ // The count's key is the caller's, because `not_sent` is a claim about
8398
+ // sending and `reindex` sends nothing. Naming its total `not_sent` would
8399
+ // have an agent reporting a failed upload from a command that never
8400
+ // contacts a server. `push` keeps the key it has always emitted.
8401
+ [totalKey]: held.total,
8402
+ denied_by_name: held.byName,
8403
+ denied_explicitly: held.explicitly,
8404
+ excluded_by_rule: held.byRule,
8405
+ skipped_symlinks: held.symlinked,
8406
+ // Present so a zero total can be read correctly. A non-empty list here means
8407
+ // the count above is a floor, not the answer — nothing inside these was
8408
+ // walked, so nobody knows how many notes they hold.
8409
+ excluded_folders: held.folders
8410
+ };
8411
+ }
8412
+ var NAME_GUESS_LABEL = "name looks private";
8413
+ function heldBackGroups(held) {
8414
+ return [
8415
+ {
8416
+ label: NAME_GUESS_LABEL,
8417
+ rows: held.byName.map((d) => ({ path: d.path, detail: `matched "${d.term ?? "?"}"` }))
8418
+ },
8419
+ { label: "listed in .memgdeny", rows: held.explicitly.map((p) => ({ path: p, detail: null })) },
8420
+ { label: "excluded by a rule", rows: held.byRule.map((p) => ({ path: p, detail: null })) },
8421
+ {
8422
+ label: "a symlink, which is never followed",
8423
+ rows: held.symlinked.map((p) => ({ path: p, detail: null }))
8424
+ }
8425
+ ];
8426
+ }
8427
+ function describeHeldBack(held) {
8428
+ return [
8429
+ ...heldBackGroups(held).flatMap(
8430
+ ({ label, rows }) => rows.map(
8431
+ (r) => `${oneLine(r.path, WIDTH.path)} \u2014 ${label}${r.detail === null ? "" : ` (${oneLine(r.detail, WIDTH.text)})`}`
8432
+ )
8433
+ ),
8434
+ ...held.folders.map(
8435
+ (dir) => `${oneLine(dir, WIDTH.path)}/ \u2014 an excluded folder, whose contents were never walked`
8436
+ )
8437
+ ];
8438
+ }
8439
+ function printHeldBack(held, wording) {
8440
+ if (held.total === 0 && held.folders.length === 0) return;
8441
+ const guessed = held.byName;
8442
+ const chosen = heldBackGroups(held).filter(({ label }) => label !== NAME_GUESS_LABEL);
8443
+ const chosenCount = chosen.reduce((n, g) => n + g.rows.length, 0);
8444
+ if (chosenCount > 0 || held.folders.length > 0) {
8445
+ printHuman(
8446
+ c.gray(
8447
+ chosenCount === 0 ? `
8448
+ Some notes are not in ${wording.where}, because your rules say so:` : `
8449
+ ${chosenCount} note(s) are not in ${wording.where}, because your rules say so:`
8450
+ )
8451
+ );
8452
+ for (const { label, rows } of chosen) {
8453
+ if (rows.length === 0) continue;
8454
+ printHuman(c.gray(` ${rows.length} \u2014 ${label}:`));
8455
+ for (const r of rows.slice(0, 10)) printHuman(c.gray(` ${oneLine(r.path, WIDTH.path)}`));
8456
+ if (rows.length > 10) printHuman(c.gray(` \u2026 and ${rows.length - 10} more`));
8457
+ }
8458
+ if (held.folders.length > 0) {
8459
+ printHuman(
8460
+ c.gray(
8461
+ ` ${held.folders.length} excluded folder(s) \u2014 their contents were not walked, so notes
8462
+ inside them are not counted above:`
8463
+ )
8464
+ );
8465
+ for (const dir of held.folders.slice(0, 10))
8466
+ printHuman(c.gray(` ${oneLine(dir, WIDTH.path)}/`));
8467
+ if (held.folders.length > 10) {
8468
+ printHuman(c.gray(` \u2026 and ${held.folders.length - 10} more`));
8469
+ }
8470
+ }
8471
+ }
8472
+ if (guessed.length > 0) {
8473
+ printHuman(
8474
+ c.yellow(
8475
+ `
8476
+ \u26A0 ${guessed.length} note(s) are not in ${wording.where}, held back on this machine by a GUESS about the name:`
8477
+ )
8478
+ );
8479
+ for (const d of guessed.slice(0, 10)) {
8480
+ printHuman(
8481
+ c.gray(
8482
+ ` ${oneLine(d.path, WIDTH.path)} (matched "${oneLine(d.term ?? "?", WIDTH.title)}")`
8483
+ )
8484
+ );
8485
+ }
8486
+ if (guessed.length > 10) printHuman(c.gray(` \u2026 and ${guessed.length - 10} more`));
8487
+ printHuman(
8488
+ c.gray(
8489
+ ` This may be a false alarm \u2014 rename the note, or list it in .memgallow,
8490
+ and ${wording.retry}.`
8491
+ )
8492
+ );
8493
+ }
8494
+ }
8495
+
8278
8496
  // src/lib/ledger.ts
8279
8497
  init_deny();
8280
8498
  init_brand();
@@ -8311,13 +8529,13 @@ function isOpaqueId(id) {
8311
8529
  return id.length === OPAQUE_ID_LENGTH && /^[0-9ABCDEFGHJKMNPQRSTVWXYZ]+$/.test(id);
8312
8530
  }
8313
8531
  function displayHandle(id, allIds) {
8314
- if (!isOpaqueId(id)) return stripControl(id);
8532
+ if (!isOpaqueId(id)) return oneLine(id, WIDTH.handle);
8315
8533
  for (let len = MIN_HANDLE_LENGTH; len < id.length; len++) {
8316
8534
  const prefix = id.slice(0, len);
8317
8535
  const matches = allIds.filter((other) => other.startsWith(prefix));
8318
8536
  if (matches.length === 1) return prefix;
8319
8537
  }
8320
- return stripControl(id);
8538
+ return oneLine(id, WIDTH.handle);
8321
8539
  }
8322
8540
  function matchIds(ref, allIds) {
8323
8541
  const exact = allIds.filter((id) => id === ref);
@@ -8413,6 +8631,13 @@ var RULE_VERSION = 2;
8413
8631
  var RATIONALE_MAX = 500;
8414
8632
  function sanitizeRationale(raw) {
8415
8633
  if (raw == null) return null;
8634
+ if (CONTROL_INTRODUCER.test(raw)) {
8635
+ throw memgError(
8636
+ "invalid_input",
8637
+ "the reason contains a terminal control sequence",
8638
+ "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."
8639
+ );
8640
+ }
8416
8641
  const clean = stripControl(raw).trim();
8417
8642
  if (clean === "") return null;
8418
8643
  const hits = secretContentHits(clean);
@@ -8537,14 +8762,83 @@ init_api_client();
8537
8762
  init_credentials();
8538
8763
  init_pending_login();
8539
8764
  init_config();
8765
+ init_ui();
8540
8766
  init_vault();
8541
8767
  async function resolveTarget(opts = {}) {
8542
8768
  if (opts.local !== true) {
8543
8769
  const cloud = await cloudTarget();
8544
- if (cloud) return cloud;
8770
+ if (cloud && cloud.kind === "cloud") {
8771
+ if (opts.vault !== void 0) {
8772
+ throw memgError(
8773
+ "invalid_input",
8774
+ `\`--vault\` names a folder on this machine, and this machine reads the hosted brain \`${oneLine(cloud.name, 72)}\``,
8775
+ `Nothing was read from \`${oneLine(opts.vault, 120)}\`, and nothing was changed.
8776
+
8777
+ To read that folder just this once: add \`--local\`
8778
+ To change which hosted brain this machine reads: \`memgineering use --brain <name>\`
8779
+
8780
+ This used to answer from the hosted brain without saying so, so a check of one
8781
+ brain silently reported another.`
8782
+ );
8783
+ }
8784
+ return cloud;
8785
+ }
8786
+ }
8787
+ try {
8788
+ const { brain, via } = await resolveBrain(opts.vault);
8789
+ return { kind: "local", brain, via };
8790
+ } catch (err) {
8791
+ if (!isNoVaultLinked(err) || opts.local === true) throw err;
8792
+ throw await accountAwareRefusal() ?? err;
8793
+ }
8794
+ }
8795
+ var isNoVaultLinked = (err) => typeof err === "object" && err !== null && err.code === "no_vault_linked";
8796
+ async function accountAwareRefusal() {
8797
+ try {
8798
+ const base = apiUrl();
8799
+ const credentials = await readCredentials();
8800
+ if (!credentials || credentials.api_url !== base) {
8801
+ const cloud = (await loadConfig()).brain.cloud;
8802
+ if (!cloud || cloud.api_url !== base) return null;
8803
+ return memgError(
8804
+ "not_signed_in",
8805
+ `this machine is signed out of \`${oneLine(cloud.name, 72)}\``,
8806
+ "Your notes are on that account \u2014 nothing is lost, and nothing here needs creating.\n\nSign back in: `memgineering login`\n\nDo NOT run `init` or `link` to fix this. They make a NEW empty brain on this\nmachine and leave the notes where they are."
8807
+ );
8808
+ }
8809
+ const brains = await listCloudBrains(credentials.token, base, { retries: 0 });
8810
+ if (brains.length === 0) return null;
8811
+ const names = brains.map((b) => ` ${oneLine(b.name, 72)}`).join("\n");
8812
+ return memgError(
8813
+ "no_vault_linked",
8814
+ "this machine is not pointed at any brain yet",
8815
+ `Your notes are on your account \u2014 nothing is lost, and nothing here needs creating.
8816
+ This account holds:
8817
+ ${names}
8818
+
8819
+ Point at one: \`memgineering use --brain <name>\`
8820
+
8821
+ Do NOT run \`init\` or \`link\` to fix this. They make a NEW empty brain on this
8822
+ machine, which leaves the notes above exactly where they are and adds an empty
8823
+ notebook that looks like the answer.`
8824
+ );
8825
+ } catch (err) {
8826
+ if (err instanceof MemgError && err.code !== "server_unreachable") {
8827
+ return memgError(
8828
+ "not_signed_in",
8829
+ "this machine cannot read the account it is signed in to",
8830
+ `The server refused: ${oneLine(err.message, 120)}
8831
+
8832
+ Your notes are on that account \u2014 nothing is lost, and nothing here needs creating.
8833
+
8834
+ Sign in again: \`memgineering login\`
8835
+
8836
+ Do NOT run \`init\` or \`link\` to fix this. They make a NEW empty brain on this
8837
+ machine and leave the notes where they are.`
8838
+ );
8839
+ }
8840
+ return null;
8545
8841
  }
8546
- const { brain, via } = await resolveBrain(opts.vault);
8547
- return { kind: "local", brain, via };
8548
8842
  }
8549
8843
  async function hostedPointer() {
8550
8844
  const base = apiUrl();
@@ -8603,14 +8897,17 @@ function reindexCommand() {
8603
8897
  protectedVault: true
8604
8898
  });
8605
8899
  const scan = await adapter.scan();
8606
- const nameDenied = (scan.denied ?? []).filter((d) => d.kind !== "explicit");
8900
+ const held = heldBack(scan);
8607
8901
  printDual({
8608
8902
  json: {
8609
8903
  brain: brain.root,
8610
8904
  indexed: built.entries.length,
8611
8905
  cache_dir: built.cacheDir,
8612
8906
  issues: built.issues,
8613
- denied_by_name: nameDenied.map((d) => ({ path: d.path, term: d.term })),
8907
+ // Carries `denied_by_name` under the key it always had, plus the three
8908
+ // this command used to drop. The total is `not_indexed` rather than
8909
+ // push's `not_sent`: nothing is sent from here.
8910
+ ...heldBackJson(held, "not_indexed"),
8614
8911
  denied_by_content: built.denied.filter((d) => d.reason === "content")
8615
8912
  },
8616
8913
  human: () => {
@@ -8623,14 +8920,23 @@ function reindexCommand() {
8623
8920
  )
8624
8921
  );
8625
8922
  }
8923
+ printHeldBack(held, { where: "the index", retry: "reindex" });
8626
8924
  const byContent = built.denied.filter((d) => d.reason === "content");
8627
- for (const [label, rows] of [
8628
- ["name looks private", nameDenied.map((d) => `${d.path} (matched "${d.term}")`)],
8629
- ["content looks like a credential", byContent.map((d) => `${d.path} (${d.detail})`)]
8630
- ]) {
8631
- if (rows.length === 0) continue;
8632
- printHuman(c.yellow(` ${rows.length} note(s) refused \u2014 ${label}:`));
8633
- for (const row of rows.slice(0, 10)) printHuman(c.gray(` ${row}`));
8925
+ if (byContent.length > 0) {
8926
+ printHuman(
8927
+ c.yellow(
8928
+ `
8929
+ \u26A0 ${byContent.length} note(s) refused \u2014 content looks like a credential:`
8930
+ )
8931
+ );
8932
+ for (const d of byContent.slice(0, 10)) {
8933
+ printHuman(
8934
+ c.gray(` ${oneLine(d.path, WIDTH.path)} (${oneLine(d.detail, WIDTH.text)})`)
8935
+ );
8936
+ }
8937
+ if (byContent.length > 10) {
8938
+ printHuman(c.gray(` \u2026 and ${byContent.length - 10} more`));
8939
+ }
8634
8940
  }
8635
8941
  }
8636
8942
  });
@@ -8647,7 +8953,7 @@ async function reindexHosted(target, rebuild) {
8647
8953
  // route documents, spelled the way the route spells it.
8648
8954
  json: { brain: { hosted: true, name: target.name }, ...answered },
8649
8955
  human: () => {
8650
- printHuman(`Reindexed ${stripControl(target.name)}.
8956
+ printHuman(`Reindexed ${oneLine(target.name, WIDTH.title)}.
8651
8957
  `);
8652
8958
  if (lexical === void 0) {
8653
8959
  printHuman(c.yellow("\u26A0 this brain\u2019s server did not report the card index"));
@@ -8685,7 +8991,7 @@ async function reindexHosted(target, rebuild) {
8685
8991
  if (answered.usable === false) {
8686
8992
  printHuman(
8687
8993
  c.yellow(
8688
- ` \u26A0 vector search is off for this brain (${stripControl(answered.unusable_reason ?? "reason not given")})`
8994
+ ` \u26A0 vector search is off for this brain (${oneLine(answered.unusable_reason ?? "reason not given", WIDTH.text)})`
8689
8995
  )
8690
8996
  );
8691
8997
  }
@@ -8704,7 +9010,7 @@ function printFailures(failed, failures) {
8704
9010
  for (const failure of (failures ?? []).slice(0, 3)) {
8705
9011
  printHuman(
8706
9012
  c.gray(
8707
- ` ${stripControl(failure.path)} (${stripControl(failure.kind)}: ${stripControl(failure.detail)})`
9013
+ ` ${oneLine(failure.path, WIDTH.path)} (${oneLine(failure.kind, WIDTH.title)}: ${oneLine(failure.detail, WIDTH.text)})`
8708
9014
  )
8709
9015
  );
8710
9016
  }
@@ -8721,18 +9027,39 @@ function nextStep(stopReason) {
8721
9027
  case "unusable_index":
8722
9028
  return "This brain holds vectors of more than one width, so none of them can be compared. `memgineering reindex --rebuild` re-derives them from the model as it is configured now.";
8723
9029
  default:
8724
- return `The server stopped for \`${stripControl(stopReason)}\`, which this version has no next step for. Running \`memgineering reindex\` again is safe.`;
9030
+ return `The server stopped for \`${oneLine(stopReason, WIDTH.title)}\`, which this version has no next step for. Running \`memgineering reindex\` again is safe.`;
8725
9031
  }
8726
9032
  }
8727
9033
  function unlinkCommand() {
8728
- return new Command2("unlink").description("stop reading a brain \u2014 your notes are not touched").argument("[path]", "which brain (default: the one this folder resolves to)").option("--purge", "also delete the local index, event log and undo snapshots").action(async (path, opts) => {
8729
- const { brain } = await resolveBrain(path);
9034
+ return new Command2("unlink").description("stop reading a brain \u2014 your notes are not touched").argument("[path]", "which brain (default: the one this folder resolves to)").option("--brain", "stop reading the HOSTED brain instead of a folder on this disk").option("--purge", "also delete the local index, event log and undo snapshots").action(async (path, opts) => {
9035
+ if (opts.brain === true) {
9036
+ await stopReadingHosted(path);
9037
+ return;
9038
+ }
9039
+ const hostedNow = await hostedPointer();
9040
+ const linkedNow = (await loadConfig()).brain.brains;
9041
+ if (hostedNow && linkedNow.length === 0) {
9042
+ throw memgError(
9043
+ "invalid_input",
9044
+ `nothing is linked on this disk \u2014 this machine reads the hosted brain "${oneLine(hostedNow.name, 72)}"`,
9045
+ "Nothing was changed, and nothing here creates a brain.\n`memgineering unlink --brain` stops reading the hosted one.\n`memgineering link <folder>` adds notes on this disk alongside it."
9046
+ );
9047
+ }
9048
+ const { brain } = await resolveBrain(path).catch(async (err) => {
9049
+ await resolveTarget({});
9050
+ throw err;
9051
+ });
8730
9052
  const cfg = await loadConfig();
8731
9053
  const remaining = cfg.brain.brains.filter((b) => b.root !== brain.root);
8732
9054
  const nextDefault = cfg.brain.default === brain.root ? remaining[0]?.root : cfg.brain.default;
9055
+ const { default: _dropped, ...restOfBrain } = cfg.brain;
8733
9056
  await saveConfig({
8734
9057
  ...cfg,
8735
- brain: { brains: remaining, ...nextDefault ? { default: nextDefault } : {} }
9058
+ brain: {
9059
+ ...restOfBrain,
9060
+ brains: remaining,
9061
+ ...nextDefault === void 0 ? {} : { default: nextDefault }
9062
+ }
8736
9063
  });
8737
9064
  if (opts.purge) {
8738
9065
  await clearCache(cacheDirFor(brain.root, brandHome()));
@@ -8820,6 +9147,54 @@ async function readLines(root, name) {
8820
9147
  const raw = await readFile10(join10(root, name), "utf8").catch(() => "");
8821
9148
  return raw.split("\n").map((l) => l.replace(/\r$/, "").trim()).filter((l) => l !== "" && !l.startsWith("#"));
8822
9149
  }
9150
+ async function stopReadingHosted(path) {
9151
+ if (path !== void 0) {
9152
+ throw memgError(
9153
+ "invalid_input",
9154
+ "`unlink --brain` does not take a path",
9155
+ "It clears the one hosted brain this machine reads, which is a single setting for the whole machine. To unlink a folder on this disk, run `memgineering unlink <path>` without `--brain`."
9156
+ );
9157
+ }
9158
+ const cfg = await loadConfig();
9159
+ const previous = cfg.brain.cloud ?? null;
9160
+ if (previous === null) {
9161
+ throw memgError(
9162
+ "invalid_input",
9163
+ "this machine is not reading a hosted brain",
9164
+ "Nothing was changed. `memgineering brains` lists what the account holds, and `memgineering use --brain <name>` points at one."
9165
+ );
9166
+ }
9167
+ await saveConfig({
9168
+ ...cfg,
9169
+ // Rebuilt without `cloud` rather than set to undefined, so what lands on
9170
+ // disk is a config with no such key rather than one asserting null.
9171
+ brain: {
9172
+ brains: cfg.brain.brains,
9173
+ ...cfg.brain.default === void 0 ? {} : { default: cfg.brain.default }
9174
+ }
9175
+ });
9176
+ printDual({
9177
+ json: {
9178
+ stopped_reading: { id: previous.id, name: previous.name, api_url: previous.api_url },
9179
+ remaining_local: cfg.brain.brains.length
9180
+ },
9181
+ human: () => {
9182
+ printHuman(`No longer reading \`${oneLine(previous.name, 72)}\`.
9183
+ `);
9184
+ printHuman(
9185
+ c.gray(
9186
+ " Nothing on the server was touched \u2014 the brain, its notes and its ledger\n are exactly where they were. `memgineering use --brain <name>` points back."
9187
+ )
9188
+ );
9189
+ if (cfg.brain.brains.length === 0) {
9190
+ printHuman(
9191
+ c.yellow("\n No brain is linked on this disk either, so nothing answers here now.")
9192
+ );
9193
+ printHuman(c.gray(" `memgineering link <folder>` connects notes you already have."));
9194
+ }
9195
+ }
9196
+ });
9197
+ }
8823
9198
 
8824
9199
  // src/commands/consolidate.ts
8825
9200
  init_src();
@@ -9117,17 +9492,23 @@ function openCommand() {
9117
9492
  brain: { hosted: true, name: target.name }
9118
9493
  },
9119
9494
  human: () => {
9120
- printHuman(`## ${stripControl(String(found.memory["title"] ?? found.path))}
9121
- `);
9495
+ printHuman(
9496
+ `## ${oneLine(String(found.memory["title"] ?? found.path), WIDTH.title)}
9497
+ `
9498
+ );
9122
9499
  const summary = found.memory["summary"];
9123
9500
  if (typeof summary === "string" && summary !== "") {
9124
- printHuman(`> ${stripControl(summary)}
9501
+ printHuman(`> ${oneLine(summary, WIDTH.text)}
9125
9502
  `);
9126
9503
  }
9127
- printHuman(c.gray(`${stripControl(found.path)} \xB7 ${stripControl(target.name)}`));
9504
+ printHuman(
9505
+ c.gray(
9506
+ `${oneLine(found.path, WIDTH.path)} \xB7 ${oneLine(target.name, WIDTH.title)}`
9507
+ )
9508
+ );
9128
9509
  if (detail !== "card" && found.body !== "") {
9129
- printHuman(`
9130
- ${stripControl(found.body)}`);
9510
+ printHuman("\n");
9511
+ printBlock(found.body);
9131
9512
  }
9132
9513
  }
9133
9514
  });
@@ -9164,7 +9545,7 @@ ${stripControl(found.body)}`);
9164
9545
  ...stage
9165
9546
  },
9166
9547
  human: () => {
9167
- printHuman(`## ${stripControl(entry.memory.title)}
9548
+ printHuman(`## ${oneLine(entry.memory.title, WIDTH.title)}
9168
9549
  `);
9169
9550
  if (scaffolding) {
9170
9551
  printHuman(
@@ -9177,13 +9558,17 @@ ${stripControl(found.body)}`);
9177
9558
  }
9178
9559
  printHuman(
9179
9560
  `
9180
- \`id: ${stripControl(entry.memory.id)}\` \xB7 \`${stripControl(entry.path)}\`
9561
+ \`id: ${oneLine(entry.memory.id, WIDTH.handle)}\` \xB7 \`${oneLine(entry.path, WIDTH.path)}\`
9181
9562
  `
9182
9563
  );
9183
9564
  if (entry.memory.summary) printHuman(quoteBlock(entry.memory.summary) + "\n");
9184
9565
  for (const claim of claims) {
9185
- printHuman(`- ${stripControl(claim.text)}`);
9186
- printHuman(c.gray(stripControl(` ${claim.confidence} \xB7 ${claim.status}`)));
9566
+ printHuman(`- ${oneLine(claim.text, WIDTH.text)}`);
9567
+ printHuman(
9568
+ c.gray(
9569
+ ` ${oneLine(claim.confidence, WIDTH.title)} \xB7 ${oneLine(claim.status, WIDTH.title)}`
9570
+ )
9571
+ );
9187
9572
  }
9188
9573
  if (claims.length === 0 && scaffolding) {
9189
9574
  printHuman(
@@ -9214,13 +9599,14 @@ makes the two surface together as a conflict.`
9214
9599
  }
9215
9600
  if (stage.outline && stage.outline.length > 0) {
9216
9601
  printHuman("\n### sections\n");
9217
- for (const heading of stage.outline) printHuman(`- ${stripControl(heading)}`);
9602
+ for (const heading of stage.outline)
9603
+ printHuman(`- ${oneLine(heading, WIDTH.title)}`);
9218
9604
  printHuman("");
9219
9605
  }
9220
9606
  if (stage.sections) {
9221
9607
  for (const section of stage.sections) {
9222
9608
  printHuman(`
9223
- ### ${stripControl(section.label)}
9609
+ ### ${oneLine(section.label, WIDTH.title)}
9224
9610
  `);
9225
9611
  printHuman(quoteBlock(section.body));
9226
9612
  }
@@ -9780,11 +10166,11 @@ To see what is there now: \`memgineering open ` + ref + "`"
9780
10166
  after: preview.after
9781
10167
  },
9782
10168
  human: () => {
9783
- printHuman(`## revise (dry run): ${stripControl(entry.memory.title)}
10169
+ printHuman(`## revise (dry run): ${oneLine(entry.memory.title, WIDTH.title)}
9784
10170
  `);
9785
- printHuman(`\`${stripControl(entry.path)}\`
10171
+ printHuman(`\`${oneLine(entry.path, WIDTH.path)}\`
9786
10172
  `);
9787
- printHuman(stripControl(renderDiff(preview.before, preview.after)));
10173
+ printBlock(renderDiff(preview.before, preview.after));
9788
10174
  printHuman(c.gray("\nNothing was written. Drop --dry-run to apply."));
9789
10175
  }
9790
10176
  });
@@ -9844,10 +10230,10 @@ To see what is there now: \`memgineering open ` + ref + "`"
9844
10230
  human: () => {
9845
10231
  if (proposal.action === "conflict") {
9846
10232
  printHuman(
9847
- `Recorded a disagreement about **${stripControl(entry.memory.title)}**.
10233
+ `Recorded a disagreement about **${oneLine(entry.memory.title, WIDTH.title)}**.
9848
10234
  `
9849
10235
  );
9850
- printHuman(`\`${stripControl(entry.path)}\`
10236
+ printHuman(`\`${oneLine(entry.path, WIDTH.path)}\`
9851
10237
  `);
9852
10238
  printHuman(
9853
10239
  c.gray(
@@ -9855,12 +10241,12 @@ To see what is there now: \`memgineering open ` + ref + "`"
9855
10241
  )
9856
10242
  );
9857
10243
  } else {
9858
- printHuman(`Revised **${stripControl(entry.memory.title)}**.
10244
+ printHuman(`Revised **${oneLine(entry.memory.title, WIDTH.title)}**.
9859
10245
  `);
9860
- printHuman(`\`${stripControl(entry.path)}\`
10246
+ printHuman(`\`${oneLine(entry.path, WIDTH.path)}\`
9861
10247
  `);
9862
10248
  }
9863
- printHuman(stripControl(renderDiff(preview.before, preview.after)));
10249
+ printBlock(renderDiff(preview.before, preview.after));
9864
10250
  if (drift) {
9865
10251
  printHuman(
9866
10252
  c.yellow(`
@@ -9927,18 +10313,18 @@ async function reviseHosted(where, ref, opts) {
9927
10313
  json: { brain: where.name, ...result },
9928
10314
  human: () => {
9929
10315
  if (result.dry_run) {
9930
- printHuman(`## revise (dry run) \u2014 ${stripControl(where.name)}
10316
+ printHuman(`## revise (dry run) \u2014 ${oneLine(where.name, WIDTH.title)}
9931
10317
  `);
9932
- printHuman(`\`${stripControl(result.target)}\`
10318
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
9933
10319
  `);
9934
- printHuman(stripControl(renderDiff(result.before, result.after)));
10320
+ printBlock(renderDiff(result.before, result.after));
9935
10321
  printHuman(c.gray("\nNothing was written. Drop --dry-run to apply."));
9936
10322
  return;
9937
10323
  }
9938
10324
  if (opts.action === "conflict") {
9939
- printHuman(`Recorded a disagreement in **${stripControl(where.name)}**.
10325
+ printHuman(`Recorded a disagreement in **${oneLine(where.name, WIDTH.title)}**.
9940
10326
  `);
9941
- printHuman(`\`${stripControl(result.target)}\`
10327
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
9942
10328
  `);
9943
10329
  printHuman(
9944
10330
  c.gray(
@@ -9946,16 +10332,16 @@ async function reviseHosted(where, ref, opts) {
9946
10332
  )
9947
10333
  );
9948
10334
  } else {
9949
- printHuman(`Revised in **${stripControl(where.name)}**.
10335
+ printHuman(`Revised in **${oneLine(where.name, WIDTH.title)}**.
9950
10336
  `);
9951
- printHuman(`\`${stripControl(result.target)}\`
10337
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
9952
10338
  `);
9953
10339
  }
9954
- printHuman(stripControl(renderDiff(result.before, result.after)));
10340
+ printBlock(renderDiff(result.before, result.after));
9955
10341
  if (result.body_drift) {
9956
10342
  printHuman(
9957
10343
  c.yellow(`
9958
- \u26A0 the body still reads "${stripControl(result.body_drift)}"`) + c.gray(
10344
+ \u26A0 the body still reads "${oneLine(result.body_drift, WIDTH.text)}"`) + c.gray(
9959
10345
  "\n \u2014 the frontmatter now concludes differently. The prose is yours and was not\n touched; edit it yourself if it should agree."
9960
10346
  )
9961
10347
  );
@@ -9979,7 +10365,7 @@ To put it back: \`memgineering revise ${ref} --claim ${JSON.stringify(
9979
10365
  );
9980
10366
  } else if (result.reverse?.hint) {
9981
10367
  printHuman(c.gray(`
9982
- ${stripControl(result.reverse.hint)}`));
10368
+ ${oneLine(result.reverse.hint, WIDTH.text)}`));
9983
10369
  }
9984
10370
  }
9985
10371
  });
@@ -10023,7 +10409,7 @@ function nothingToChange(path, ref, verb = "revise") {
10023
10409
  memgineering revise ${ref} --action reinforce --claim "<the conclusion>" --reason "<what confirms it>"` : `It is already ${verb === "retire" ? "retired" : "current"}. \`memgineering ${verb === "retire" ? "unretire" : "retire"} ${ref}\` is what changes that.`;
10024
10410
  return memgError(
10025
10411
  "nothing_to_change",
10026
- `${stripControl(path)} already says exactly this`,
10412
+ `${oneLine(path, WIDTH.path)} already says exactly this`,
10027
10413
  `Nothing was written. ${way}
10028
10414
  A \`--reason\` does not make it a change either \u2014 it would record an operation that changed nothing, and one whose \`undo\` would reverse nothing while saying it had.
10029
10415
  To see what the note says now: \`memgineering open ${ref}\``
@@ -10066,11 +10452,17 @@ function proposalsCommand() {
10066
10452
  opts.all === true ? { all: true } : {}
10067
10453
  ) : await listLocally(where.brain.root, opts.all === true);
10068
10454
  const shown = listed.proposals;
10455
+ const unavailable = shown.filter(
10456
+ (p) => p.claim === void 0 && !carriesNoClaim(p.action)
10457
+ );
10458
+ const rows = shown.map(
10459
+ (p) => p.claim === void 0 && !carriesNoClaim(p.action) ? { ...p, claim: null, claim_unavailable: true } : p
10460
+ );
10069
10461
  printDual({
10070
10462
  json: {
10071
- proposals: shown,
10072
- count: shown.length,
10073
- hint: hintForList(shown.length, listed.total, opts.all === true)
10463
+ proposals: rows,
10464
+ count: rows.length,
10465
+ 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)
10074
10466
  },
10075
10467
  human: () => {
10076
10468
  if (shown.length === 0) {
@@ -10111,6 +10503,7 @@ function summarize(p) {
10111
10503
  action: p.action,
10112
10504
  memory_id: p.memory_id,
10113
10505
  target: p.target.path,
10506
+ claim: p.claim?.text ?? null,
10114
10507
  reason: p.reason,
10115
10508
  proposed_kind: p.proposed_kind,
10116
10509
  supersedes: p.relations.supersedes,
@@ -10118,14 +10511,28 @@ function summarize(p) {
10118
10511
  expires_at: p.expiresAt
10119
10512
  };
10120
10513
  }
10514
+ var CLAIM_WIDTH = 160;
10121
10515
  function renderSummary(p) {
10516
+ const claimless = carriesNoClaim(p.action);
10517
+ const claim = p.claim === void 0 || p.claim === null ? null : oneLine(p.claim, CLAIM_WIDTH);
10518
+ const unavailable = p.claim === void 0 && !claimless;
10122
10519
  const lines2 = [
10123
- `- **${stripControl(p.action)}**${p.proposed_kind ? ` \u2192 ${p.proposed_kind}` : ""} \`${stripControl(p.target)}\``,
10124
- ` ${p.id} ${p.state}`,
10125
- quoteBlock(p.reason, " > ")
10520
+ `- **${oneLine(p.action, WIDTH.title)}**${p.proposed_kind ? ` \u2192 ${p.proposed_kind}` : ""} \`${oneLine(p.target, WIDTH.path)}\``,
10521
+ ` ${p.id} ${p.state}`
10126
10522
  ];
10523
+ if (claim !== null && claim !== "") lines2.push(` \u2192 ${claim}`);
10524
+ else if (unavailable) {
10525
+ lines2.push(
10526
+ c.gray(
10527
+ ` \u2192 (claim unavailable: this brain's server is older than the CLI. \`memgineering open ${oneLine(p.memory_id ?? p.target, WIDTH.handle)}\` shows what the note says NOW; upgrading the server restores the claim.)`
10528
+ )
10529
+ );
10530
+ }
10531
+ lines2.push(quoteBlock(p.reason, " > "));
10127
10532
  if (p.supersedes.length > 0) {
10128
- lines2.push(c.gray(` replaces: ${p.supersedes.map(stripControl).join(", ")}`));
10533
+ lines2.push(
10534
+ c.gray(` replaces: ${p.supersedes.map((id) => oneLine(id, WIDTH.handle)).join(", ")}`)
10535
+ );
10129
10536
  }
10130
10537
  return lines2.join("\n");
10131
10538
  }
@@ -10241,10 +10648,10 @@ function renderApproval(applied, vault) {
10241
10648
  },
10242
10649
  human: () => {
10243
10650
  printHuman(
10244
- `Applied **${stripControl(applied.action)}** to \`${stripControl(applied.target)}\`.
10651
+ `Applied **${oneLine(applied.action, WIDTH.title)}** to \`${oneLine(applied.target, WIDTH.path)}\`.
10245
10652
  `
10246
10653
  );
10247
- printHuman(stripControl(renderDiff(applied.before, applied.after)));
10654
+ printBlock(renderDiff(applied.before, applied.after));
10248
10655
  printHuman(c.gray(`
10249
10656
  Undo: \`${undoHint(applied.opId, vault)}\``));
10250
10657
  }
@@ -10254,7 +10661,7 @@ function renderRejection(id, state, target) {
10254
10661
  printDual({
10255
10662
  json: { proposal_id: id, state, target },
10256
10663
  human: () => {
10257
- printHuman(`Declined \`${stripControl(target)}\`.
10664
+ printHuman(`Declined \`${oneLine(target, WIDTH.path)}\`.
10258
10665
  `);
10259
10666
  printHuman(c.gray("The note was not touched."));
10260
10667
  }
@@ -10312,9 +10719,9 @@ function renderRebase(id, state, target, before, after) {
10312
10719
  hint: `This is the change as it applies to the note TODAY, and it has not been approved. Read the diff, then \`memgineering proposals approve ${id}\` \u2014 or reject it if the note moving on made it wrong.`
10313
10720
  },
10314
10721
  human: () => {
10315
- printHuman(`Recomputed against \`${stripControl(target)}\`.
10722
+ printHuman(`Recomputed against \`${oneLine(target, WIDTH.path)}\`.
10316
10723
  `);
10317
- printHuman(stripControl(renderDiff(before, after)));
10724
+ printBlock(renderDiff(before, after));
10318
10725
  printHuman(
10319
10726
  c.gray(
10320
10727
  `
@@ -10471,14 +10878,25 @@ function consolidateCommand() {
10471
10878
  });
10472
10879
  });
10473
10880
  }
10881
+ var TITLE_WIDTH = 72;
10882
+ function noteLines(id, title, path) {
10883
+ const shown = oneLine(title, TITLE_WIDTH);
10884
+ return [
10885
+ shown === "" ? ` ${oneLine(id, WIDTH.handle)}` : ` ${oneLine(id, WIDTH.handle)} ${shown}`,
10886
+ // Backticked, as it was before the title moved onto the row above it. This
10887
+ // output is read by agents as markdown, and a path outside code formatting
10888
+ // is a path they have to guess the extent of.
10889
+ c.gray(` \`${oneLine(path, WIDTH.path)}\``)
10890
+ ];
10891
+ }
10474
10892
  function renderCandidate(candidate) {
10475
10893
  const lines2 = [
10476
- `- **${stripControl(candidate.rule)}** \u2014 ${stripControl(candidate.evidence)}`,
10477
- ` ${stripControl(candidate.memory_id)} \`${stripControl(candidate.path)}\``
10894
+ `- **${oneLine(candidate.rule, WIDTH.title)}** \u2014 ${oneLine(candidate.evidence, WIDTH.text)}`,
10895
+ ...noteLines(candidate.memory_id, candidate.title, candidate.path)
10478
10896
  ];
10479
10897
  if (candidate.other_id !== null) {
10480
10898
  lines2.push(
10481
- ` ${stripControl(candidate.other_id)} \`${stripControl(candidate.other_path ?? "")}\``
10899
+ ...noteLines(candidate.other_id, candidate.other_title ?? "", candidate.other_path ?? "")
10482
10900
  );
10483
10901
  }
10484
10902
  lines2.push(
@@ -10515,7 +10933,7 @@ function proposeCommand() {
10515
10933
  },
10516
10934
  human: () => {
10517
10935
  printHuman(
10518
- `Proposed **${stripControl(filed.action)}** on \`${stripControl(filed.target)}\`.
10936
+ `Proposed **${oneLine(filed.action, WIDTH.title)}** on \`${oneLine(filed.target, WIDTH.path)}\`.
10519
10937
  `
10520
10938
  );
10521
10939
  printHuman(c.gray(` ${filed.id}`));
@@ -10950,14 +11368,14 @@ function lifecycleCommand(action) {
10950
11368
  json: { action, memory_id: entry.memory.id, target: entry.path, op_id: record.op_id },
10951
11369
  human: () => {
10952
11370
  printHuman(
10953
- action === "retire" ? `Retired **${stripControl(entry.memory.title)}**.
10954
- ` : `**${stripControl(entry.memory.title)}** is current again.
11371
+ action === "retire" ? `Retired **${oneLine(entry.memory.title, WIDTH.title)}**.
11372
+ ` : `**${oneLine(entry.memory.title, WIDTH.title)}** is current again.
10955
11373
  `
10956
11374
  );
10957
11375
  if (action === "retire") {
10958
11376
  printHuman(
10959
11377
  c.gray(
10960
- "It still appears in recall \u2014 ranked last and labelled. Nothing was removed.\nTo stop reading the note entirely: `memgineering exclude " + stripControl(entry.path) + "`"
11378
+ "It still appears in recall \u2014 ranked last and labelled. Nothing was removed.\nTo stop reading the note entirely: `memgineering exclude " + oneLine(entry.path, WIDTH.path) + "`"
10961
11379
  )
10962
11380
  );
10963
11381
  }
@@ -11015,8 +11433,10 @@ function ignoreCommand(mode) {
11015
11433
  mode === "exclude" ? (
11016
11434
  // The path is argv here, but argv came from a listing of the
11017
11435
  // user's own notes — and a filename is as authored as a title.
11018
- c.gray(`\`${stripControl(target)}\` was already excluded \u2014 nothing changed.`)
11019
- ) : c.gray(`\`${stripControl(target)}\` was not excluded \u2014 nothing changed.`)
11436
+ c.gray(
11437
+ `\`${oneLine(target, WIDTH.path)}\` was already excluded \u2014 nothing changed.`
11438
+ )
11439
+ ) : c.gray(`\`${oneLine(target, WIDTH.path)}\` was not excluded \u2014 nothing changed.`)
11020
11440
  );
11021
11441
  return;
11022
11442
  }
@@ -11046,13 +11466,13 @@ async function lifecycleHosted(where, action, ref, reason) {
11046
11466
  json: { brain: where.name, ...result },
11047
11467
  human: () => {
11048
11468
  printHuman(
11049
- action === "retire" ? `Retired in **${stripControl(where.name)}**.
11050
- ` : `Current again in **${stripControl(where.name)}**.
11469
+ action === "retire" ? `Retired in **${oneLine(where.name, WIDTH.title)}**.
11470
+ ` : `Current again in **${oneLine(where.name, WIDTH.title)}**.
11051
11471
  `
11052
11472
  );
11053
- printHuman(`\`${stripControl(result.target)}\`
11473
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
11054
11474
  `);
11055
- printHuman(c.gray(stripControl(result.note)));
11475
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
11056
11476
  printHuman(
11057
11477
  c.gray(
11058
11478
  `
@@ -11072,15 +11492,15 @@ async function ignoreHosted(where, mode, path, reason) {
11072
11492
  json: { brain: where.name, ...result },
11073
11493
  human: () => {
11074
11494
  if (!result.changed) {
11075
- printHuman(c.gray(stripControl(result.note)));
11495
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
11076
11496
  return;
11077
11497
  }
11078
11498
  printHuman(
11079
- mode === "exclude" ? `No longer reading \`${stripControl(result.path)}\` in **${stripControl(where.name)}**.
11080
- ` : `Reading \`${stripControl(result.path)}\` again in **${stripControl(where.name)}**.
11499
+ mode === "exclude" ? `No longer reading \`${oneLine(result.path, WIDTH.path)}\` in **${oneLine(where.name, WIDTH.title)}**.
11500
+ ` : `Reading \`${oneLine(result.path, WIDTH.path)}\` again in **${oneLine(where.name, WIDTH.title)}**.
11081
11501
  `
11082
11502
  );
11083
- printHuman(c.gray(stripControl(result.note)));
11503
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
11084
11504
  if (result.op_id) printHuman(c.gray(`
11085
11505
  Undo: \`memgineering ${opposite(mode)} ${path}\``));
11086
11506
  }
@@ -11144,10 +11564,12 @@ function evidenceCommand() {
11144
11564
  });
11145
11565
  }
11146
11566
  function renderEvidence(report, opts) {
11147
- printHuman(`## evidence: ${stripControl(report.title)}
11148
- `);
11149
- printHuman(`\`id: ${stripControl(report.id)}\` \xB7 \`${stripControl(report.path)}\`
11567
+ printHuman(`## evidence: ${oneLine(report.title, WIDTH.title)}
11150
11568
  `);
11569
+ printHuman(
11570
+ `\`id: ${oneLine(report.id, WIDTH.handle)}\` \xB7 \`${oneLine(report.path, WIDTH.path)}\`
11571
+ `
11572
+ );
11151
11573
  if (report.critical) {
11152
11574
  printHuman(c.yellow("\u26A0 critical \u2014 this note defines how the agent behaves.") + "\n");
11153
11575
  }
@@ -11197,11 +11619,12 @@ function renderEvidence(report, opts) {
11197
11619
  ]);
11198
11620
  }
11199
11621
  const pad = rows.reduce((w, [label]) => Math.max(w, label.length), 0) + 2;
11200
- for (const [label, value] of rows) printHuman(stripControl(` ${label.padEnd(pad)}${value}`));
11622
+ for (const [label, value] of rows)
11623
+ printHuman(` ${label.padEnd(pad)}${oneLine(value, WIDTH.text)}`);
11201
11624
  if (report.standing) {
11202
11625
  const means = STANDING_MEANS[report.standing];
11203
11626
  printHuman(`
11204
- **${stripControl(report.standing)}**${means ? ` \u2014 ${means}` : ""}`);
11627
+ **${oneLine(report.standing, WIDTH.title)}**${means ? ` \u2014 ${means}` : ""}`);
11205
11628
  }
11206
11629
  if (report.contradicts > 0) {
11207
11630
  printHuman(
@@ -11222,7 +11645,7 @@ function renderEvidence(report, opts) {
11222
11645
  report.last_reason.verb,
11223
11646
  report.last_reason.ts === null ? null : report.last_reason.ts.slice(0, 10)
11224
11647
  ].filter((part) => part !== null && part !== "").join(", ");
11225
- if (provenance !== "") printHuman(c.gray(` ${stripControl(provenance)}`));
11648
+ if (provenance !== "") printHuman(c.gray(` ${oneLine(provenance, WIDTH.text)}`));
11226
11649
  } else if (report.changed > 0) {
11227
11650
  printHuman(
11228
11651
  c.gray(
@@ -11358,6 +11781,7 @@ import { Command as Command11 } from "commander";
11358
11781
  // src/lib/rules.ts
11359
11782
  init_src();
11360
11783
  init_index_build();
11784
+ init_ui();
11361
11785
  init_vault();
11362
11786
  var RULE_LIMIT = 8;
11363
11787
  async function collectRules(vault, contextDir, now = /* @__PURE__ */ new Date()) {
@@ -11386,11 +11810,12 @@ async function collectRules(vault, contextDir, now = /* @__PURE__ */ new Date())
11386
11810
  };
11387
11811
  }
11388
11812
  function renderRuleLines(rules) {
11389
- return rules.map(({ entry }) => `- ${entry.memory.summary ?? entry.memory.title}`);
11813
+ return rules.map(({ entry }) => `- ${oneLine(entry.memory.summary ?? entry.memory.title, 200)}`);
11390
11814
  }
11391
11815
 
11392
11816
  // src/commands/guard.ts
11393
11817
  init_deadline();
11818
+ init_ui();
11394
11819
  function guardCommand() {
11395
11820
  return new Command11("guard").description("hook entry \u2014 show the user\u2019s rules before the first edit of a session").option("--context-dir <path>", "where the work is happening, when stdin does not say").option("--at <event>", "pre-tool-use | stop", "pre-tool-use").action(async (opts) => {
11396
11821
  try {
@@ -11430,7 +11855,7 @@ async function runGuard(payload, contextDirFlag) {
11430
11855
  return JSON.stringify({
11431
11856
  hookSpecificOutput: {
11432
11857
  hookEventName: "PreToolUse",
11433
- additionalContext: contextFor(claimed.rules, claimed.total)
11858
+ additionalContext: contextFor(claimed.lines, claimed.total)
11434
11859
  }
11435
11860
  });
11436
11861
  }
@@ -11444,16 +11869,22 @@ async function runStopGuard(payload, contextDirFlag) {
11444
11869
  return JSON.stringify({
11445
11870
  hookSpecificOutput: {
11446
11871
  hookEventName: "Stop",
11447
- additionalContext: stopContextFor(claimed.rules, claimed.total)
11872
+ additionalContext: stopContextFor(claimed.lines, claimed.total)
11448
11873
  }
11449
11874
  });
11450
11875
  }
11451
11876
  async function claimRules(sessionId, contextDir) {
11877
+ const hosted = await hostedRuleLines().catch(() => null);
11878
+ if (hosted !== null) {
11879
+ if (hosted.lines.length === 0) return null;
11880
+ if (!await claimSession(sessionId, hosted.brainId)) return null;
11881
+ return { lines: hosted.lines, total: hosted.total };
11882
+ }
11452
11883
  const collected = await collectRules(void 0, contextDir).catch(() => null);
11453
11884
  if (collected === null) return null;
11454
11885
  const { brainRoot, rules, total } = collected;
11455
11886
  if (rules.length === 0) return null;
11456
- if (!await claimSession(sessionId)) return null;
11887
+ if (!await claimSession(sessionId, brainRoot)) return null;
11457
11888
  await recordEvent(brainRoot, {
11458
11889
  verb: "guard",
11459
11890
  contextDir,
@@ -11461,22 +11892,20 @@ async function claimRules(sessionId, contextDir) {
11461
11892
  limit: RULE_LIMIT,
11462
11893
  returnedIds: rules.map((r) => r.entry.memory.id)
11463
11894
  });
11464
- return { rules, total };
11895
+ return { lines: renderRuleLines(rules), total };
11465
11896
  }
11466
- function contextFor(rules, total) {
11467
- const lines2 = renderRuleLines(rules);
11468
- const more = total > rules.length ? `
11469
- (${total - rules.length} more \u2014 \`memgineering rules\` for all of them.)` : "";
11897
+ function contextFor(lines2, total) {
11898
+ const more = total > lines2.length ? `
11899
+ (${total - lines2.length} more \u2014 \`memgineering rules\` for all of them.)` : "";
11470
11900
  return `Decisions this user has already made. They are theirs, not suggestions, and they hold whether or not this task mentions them:
11471
11901
 
11472
11902
  ${lines2.join("\n")}${more}
11473
11903
 
11474
11904
  Follow them. If what you are about to do goes against one, say so in a sentence instead of doing it quietly.`;
11475
11905
  }
11476
- function stopContextFor(rules, total) {
11477
- const lines2 = renderRuleLines(rules);
11478
- const more = total > rules.length ? `
11479
- (${total - rules.length} more \u2014 \`memgineering rules\` for all of them.)` : "";
11906
+ function stopContextFor(lines2, total) {
11907
+ const more = total > lines2.length ? `
11908
+ (${total - lines2.length} more \u2014 \`memgineering rules\` for all of them.)` : "";
11480
11909
  return `Decisions this user has already made. They are theirs, not suggestions, and they hold whether or not this task mentions them:
11481
11910
 
11482
11911
  ${lines2.join("\n")}${more}
@@ -11507,10 +11936,10 @@ async function readPayload() {
11507
11936
  }
11508
11937
  }
11509
11938
  var guardDir = () => join16(brandHome(), "guard");
11510
- async function claimSession(sessionId) {
11939
+ async function claimSession(sessionId, brain) {
11511
11940
  const marker = join16(
11512
11941
  guardDir(),
11513
- `${createHash7("sha256").update(sessionId).digest("hex").slice(0, 32)}`
11942
+ `${createHash7("sha256").update(`${sessionId}\0${brain}`).digest("hex").slice(0, 32)}`
11514
11943
  );
11515
11944
  try {
11516
11945
  await mkdir11(guardDir(), { recursive: true, mode: 448 });
@@ -11535,6 +11964,17 @@ async function pruneOldMarkers() {
11535
11964
  } catch {
11536
11965
  }
11537
11966
  }
11967
+ var RULE_WIDTH = 200;
11968
+ async function hostedRuleLines() {
11969
+ const where = await resolveTarget({});
11970
+ if (where.kind !== "cloud") return null;
11971
+ const { rules, total } = await cloudRules(await openCloudBrain(where));
11972
+ return {
11973
+ lines: rules.map((r) => `- ${oneLine(r.summary ?? r.title, RULE_WIDTH)}`),
11974
+ total,
11975
+ brainId: where.brainId
11976
+ };
11977
+ }
11538
11978
 
11539
11979
  // src/program.ts
11540
11980
  init_init();
@@ -11577,7 +12017,7 @@ function logCommand() {
11577
12017
  human: () => {
11578
12018
  const diverged2 = Array.isArray(history["divergence"]) ? history["divergence"] : [];
11579
12019
  const total = typeof history["total"] === "number" ? history["total"] : rows.length;
11580
- printHuman(`## log \u2014 ${stripControl(target.name)} (${total} operations)
12020
+ printHuman(`## log \u2014 ${oneLine(target.name, WIDTH.title)} (${total} operations)
11581
12021
  `);
11582
12022
  if (rows.length === 0) {
11583
12023
  printHuman("_This brain has no recorded operations._\n");
@@ -11591,7 +12031,8 @@ function logCommand() {
11591
12031
  );
11592
12032
  }
11593
12033
  const hint = history["hint"];
11594
- if (typeof hint === "string" && hint !== "") printHuman(c.gray(stripControl(hint)));
12034
+ if (typeof hint === "string" && hint !== "")
12035
+ printHuman(c.gray(oneLine(hint, WIDTH.text)));
11595
12036
  }
11596
12037
  });
11597
12038
  return;
@@ -11713,15 +12154,19 @@ function renderOps(ops, opts) {
11713
12154
  op.diverged && !op.undone ? "note has changed since" : null
11714
12155
  ].filter(Boolean);
11715
12156
  printHuman(
11716
- `- ${stripControl(op.verb)} \`${stripControl(op.target)}\`${marks.length ? c.yellow(` (${marks.join(", ")})`) : ""}`
12157
+ `- ${oneLine(op.verb, WIDTH.title)} \`${oneLine(op.target, WIDTH.path)}\`${marks.length ? c.yellow(` (${marks.join(", ")})`) : ""}`
11717
12158
  );
11718
12159
  printHuman(
11719
- c.gray(` ${stripControl(op.ts)}${op.actor ? ` \xB7 ${stripControl(op.actor)}` : ""}`) + ` ${c.gray(undoable(op, opts.hosted) ? "undo:" : "op:")} ${stripControl(op.op_id)}`
12160
+ c.gray(
12161
+ ` ${oneLine(op.ts, WIDTH.handle)}${op.actor ? ` \xB7 ${oneLine(op.actor, WIDTH.title)}` : ""}`
12162
+ ) + ` ${c.gray(undoable(op, opts.hosted) ? "undo:" : "op:")} ${oneLine(op.op_id, WIDTH.handle)}`
11720
12163
  );
11721
12164
  if (op.rationale) printHuman(c.gray(quoteBlock(op.rationale, " > ")));
11722
12165
  if (opts.verbose) {
11723
12166
  printHuman(
11724
- c.gray(` ${stripControl(op.hash_before ?? "\u2205")} \u2192 ${stripControl(op.hash_after ?? "\u2205")}`)
12167
+ c.gray(
12168
+ ` ${oneLine(op.hash_before ?? "\u2205", WIDTH.handle)} \u2192 ${oneLine(op.hash_after ?? "\u2205", WIDTH.handle)}`
12169
+ )
11725
12170
  );
11726
12171
  }
11727
12172
  }
@@ -11777,7 +12222,7 @@ init_pending_login();
11777
12222
  init_ui();
11778
12223
  import { Command as Command14 } from "commander";
11779
12224
  function logoutCommand() {
11780
- return new Command14("logout").description("sign out of a hosted brain").option("--all", "revoke every token on the account, not just this machine\u2019s").option(
12225
+ return new Command14("logout").description("sign out of your account \u2014 the pointer and your notes stay").option("--all", "revoke every token on the account, not just this machine\u2019s").option(
11781
12226
  "--local",
11782
12227
  "forget the token here without telling the server \u2014 only for a server you cannot reach"
11783
12228
  ).action(async (opts) => {
@@ -11860,6 +12305,11 @@ function logoutCommand() {
11860
12305
  opts.all ? " Every token on the account is dead. `memgineering login` starts a new one." : " This machine\u2019s token is revoked. Other machines are unaffected."
11861
12306
  )
11862
12307
  );
12308
+ printHuman(
12309
+ c.gray(
12310
+ " Your notes are untouched: signing out takes a token, not a brain.\n This machine still remembers which brain it was reading, so `memgineering\n login` picks up where you left off. `memgineering unlink --brain` forgets it."
12311
+ )
12312
+ );
11863
12313
  }
11864
12314
  });
11865
12315
  });
@@ -11943,12 +12393,12 @@ init_ui();
11943
12393
  function pullCommand() {
11944
12394
  return new Command16("pull").description("download your hosted brain to a folder on this machine").argument("[destination]", "folder to write into (created if missing)").option("--brain <name>", "which hosted brain to download").option("--dry-run", "say what would be written and write nothing").action(async (destination, opts) => {
11945
12395
  const { brain: source, others } = await resolveSource(opts.brain);
11946
- const target = resolve8(destination ?? `./${stripControl(source.name)}`);
12396
+ const target = resolve8(destination ?? `./${oneLine(source.name, WIDTH.title)}`);
11947
12397
  const first = await cloudListNotes(source, { limit: 1 });
11948
12398
  if (first.total === 0) {
11949
12399
  throw memgError(
11950
12400
  "invalid_input",
11951
- `\u201C${stripControl(source.name)}\u201D has no notes to download`,
12401
+ `\u201C${oneLine(source.name, WIDTH.title)}\u201D has no notes to download`,
11952
12402
  "Nothing was written. `memgineering push` carries a brain on this disk up to the hosted one, and `remember` writes a first note into it."
11953
12403
  );
11954
12404
  }
@@ -11969,13 +12419,13 @@ function pullCommand() {
11969
12419
  printHuman(`## pull (dry run)
11970
12420
  `);
11971
12421
  printHuman(
11972
- `${first.total} note(s) from \u201C${stripControl(source.name)}\u201D into \`${target}\`
12422
+ `${first.total} note(s) from \u201C${oneLine(source.name, WIDTH.title)}\u201D into \`${target}\`
11973
12423
  `
11974
12424
  );
11975
12425
  for (const note of preview.slice(0, 20)) {
11976
12426
  printHuman(
11977
12427
  c.gray(
11978
- ` ${stripControl(note.path)}${note.excluded === true ? " (excluded)" : ""}`
12428
+ ` ${oneLine(note.path, WIDTH.path)}${note.excluded === true ? " (excluded)" : ""}`
11979
12429
  )
11980
12430
  );
11981
12431
  }
@@ -11990,7 +12440,7 @@ function pullCommand() {
11990
12440
  });
11991
12441
  return;
11992
12442
  }
11993
- progress(`downloading ${first.total} note(s) from \u201C${stripControl(source.name)}\u201D\u2026`);
12443
+ progress(`downloading ${first.total} note(s) from \u201C${oneLine(source.name, WIDTH.title)}\u201D\u2026`);
11994
12444
  const written = [];
11995
12445
  const already = [];
11996
12446
  const excluded = [];
@@ -12032,7 +12482,7 @@ function pullCommand() {
12032
12482
  human: () => {
12033
12483
  printHuman(
12034
12484
  c.green(
12035
- `\u2713 ${written.length} note(s) from \u201C${stripControl(source.name)}\u201D written to \`${target}\``
12485
+ `\u2713 ${written.length} note(s) from \u201C${oneLine(source.name, WIDTH.title)}\u201D written to \`${target}\``
12036
12486
  )
12037
12487
  );
12038
12488
  if (already.length > 0) {
@@ -12084,8 +12534,8 @@ async function resolveSource(named) {
12084
12534
  if (!found) {
12085
12535
  throw memgError(
12086
12536
  "invalid_input",
12087
- named ? `no hosted brain called \u201C${stripControl(named)}\u201D` : "more than one hosted brain \u2014 say which",
12088
- `Pass one of: ${brains.map((b) => stripControl(b.name)).join(", ")} (e.g. \`memgineering pull --brain ${stripControl(brains[0]?.name ?? "")}\`)`
12537
+ named ? `no hosted brain called \u201C${oneLine(named, WIDTH.title)}\u201D` : "more than one hosted brain \u2014 say which",
12538
+ `Pass one of: ${brains.map((b) => oneLine(b.name, WIDTH.title)).join(", ")} (e.g. \`memgineering pull --brain ${oneLine(brains[0]?.name ?? "", WIDTH.title)}\`)`
12089
12539
  );
12090
12540
  }
12091
12541
  return {
@@ -12102,7 +12552,7 @@ async function resolveSource(named) {
12102
12552
  }
12103
12553
  function sayWhatIsElsewhere(others, into) {
12104
12554
  if (others.length === 0) return;
12105
- const names = others.map(stripControl);
12555
+ const names = others.map((name) => oneLine(name, WIDTH.title));
12106
12556
  printHuman(
12107
12557
  c.yellow(
12108
12558
  `
@@ -12147,6 +12597,11 @@ init_api_client();
12147
12597
  import { Command as Command17 } from "commander";
12148
12598
  init_vault();
12149
12599
  init_ui();
12600
+ var HELD_BACK_WORDING = {
12601
+ where: "the hosted brain",
12602
+ retry: "push again"
12603
+ };
12604
+ var printHeldBack2 = (held) => printHeldBack(held, HELD_BACK_WORDING);
12150
12605
  function pushCommand() {
12151
12606
  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) => {
12152
12607
  const { brain: local } = await resolveBrain(opts.vault);
@@ -12155,11 +12610,16 @@ function pushCommand() {
12155
12610
  const paths = scan.files.filter(isNote);
12156
12611
  const held = heldBack(scan);
12157
12612
  if (paths.length === 0) {
12613
+ const sawFiles = scan.files.length > 0;
12614
+ const excluded = describeHeldBack(held).slice(0, 10).map((row) => ` ${row}`).join("\n");
12158
12615
  throw memgError(
12159
12616
  "invalid_input",
12160
12617
  `there is nothing to upload in ${local.root}`,
12161
- held.total === 0 ? "This brain has no markdown notes in it at all. Check the path \u2014 `--vault` takes the folder your notes live in." : `This brain has ${held.total} markdown note(s) and the rules on this machine hold back every one:
12162
- ` + describeHeldBack(held).slice(0, 10).map((row) => ` ${row}`).join("\n") + "\nRename one, or list it in .memgallow, if the name is a false alarm."
12618
+ held.total > 0 ? `This brain has ${held.total} markdown note(s) and the rules on this machine hold back every one:
12619
+ ${excluded}
12620
+ 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:
12621
+ ${excluded}
12622
+ 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."
12163
12623
  );
12164
12624
  }
12165
12625
  if (opts.dryRun) {
@@ -12179,7 +12639,7 @@ function pushCommand() {
12179
12639
  `);
12180
12640
  for (const path of paths.slice(0, 20)) printHuman(c.gray(` ${path}`));
12181
12641
  if (paths.length > 20) printHuman(c.gray(` \u2026 and ${paths.length - 20} more`));
12182
- printHeldBack(held);
12642
+ printHeldBack2(held);
12183
12643
  printHuman(
12184
12644
  c.gray(
12185
12645
  "\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."
@@ -12190,7 +12650,7 @@ function pushCommand() {
12190
12650
  return;
12191
12651
  }
12192
12652
  const destination = await resolveDestination(opts.brain, local.root);
12193
- progress(`uploading ${paths.length} note(s) to \u201C${stripControl(destination.name)}\u201D\u2026`);
12653
+ progress(`uploading ${paths.length} note(s) to \u201C${oneLine(destination.name, WIDTH.title)}\u201D\u2026`);
12194
12654
  const uploaded = [];
12195
12655
  const already = [];
12196
12656
  const refused = [];
@@ -12260,7 +12720,9 @@ function pushCommand() {
12260
12720
  },
12261
12721
  human: () => {
12262
12722
  printHuman(
12263
- c.green(`\u2713 ${uploaded.length} note(s) uploaded to \u201C${stripControl(destination.name)}\u201D`)
12723
+ c.green(
12724
+ `\u2713 ${uploaded.length} note(s) uploaded to \u201C${oneLine(destination.name, WIDTH.title)}\u201D`
12725
+ )
12264
12726
  );
12265
12727
  if (already.length > 0) {
12266
12728
  printHuman(c.gray(` ${already.length} were already there \u2014 left as they are`));
@@ -12278,13 +12740,15 @@ function pushCommand() {
12278
12740
  \u26A0 ${refused.length} note(s) were NOT uploaded:`) + c.gray("\n they are untouched on this machine and can be fixed and pushed again")
12279
12741
  );
12280
12742
  for (const r of refused.slice(0, 10)) {
12281
- printHuman(c.gray(` ${stripControl(r.path)} (${stripControl(r.code)})`));
12743
+ printHuman(
12744
+ c.gray(` ${oneLine(r.path, WIDTH.path)} (${oneLine(r.code, WIDTH.title)})`)
12745
+ );
12282
12746
  }
12283
12747
  if (refused.length > 10) {
12284
12748
  printHuman(c.gray(` \u2026 and ${refused.length - 10} more`));
12285
12749
  }
12286
12750
  }
12287
- printHeldBack(held);
12751
+ printHeldBack2(held);
12288
12752
  printRepoint(repointed, destination);
12289
12753
  printHuman(
12290
12754
  c.gray(
@@ -12300,14 +12764,14 @@ Nothing was deleted from \`${local.root}\` \u2014
12300
12764
  function printRepoint(repoint, destination) {
12301
12765
  if (repoint === null) {
12302
12766
  printHuman(c.gray(`
12303
- This machine still reads \u201C${stripControl(destination.name)}\u201D.`));
12767
+ This machine still reads \u201C${oneLine(destination.name, WIDTH.title)}\u201D.`));
12304
12768
  return;
12305
12769
  }
12306
12770
  if (repoint.from === null) {
12307
12771
  printHuman(
12308
12772
  c.gray(
12309
12773
  `
12310
- This machine now reads \u201C${stripControl(destination.name)}\u201D \u2014 it was not reading a hosted
12774
+ This machine now reads \u201C${oneLine(destination.name, WIDTH.title)}\u201D \u2014 it was not reading a hosted
12311
12775
  brain before. \`memgineering recall --local\` still reads the folder.`
12312
12776
  )
12313
12777
  );
@@ -12316,85 +12780,9 @@ brain before. \`memgineering recall --local\` still reads the folder.`
12316
12780
  printHuman(
12317
12781
  c.yellow(
12318
12782
  `
12319
- \u26A0 THIS MACHINE NOW READS \u201C${stripControl(destination.name)}\u201D \u2014 it was reading \u201C${stripControl(repoint.from.name)}\u201D.`
12783
+ \u26A0 THIS MACHINE NOW READS \u201C${oneLine(destination.name, WIDTH.title)}\u201D \u2014 it was reading \u201C${oneLine(repoint.from.name, WIDTH.title)}\u201D.`
12320
12784
  ) + c.gray(
12321
- "\n That is a side effect of `--brain`, and it outlives this command: `recall`,\n `remember` and the memories an agent session starts with all come from the\n brain named here. Nothing in \u201C" + stripControl(repoint.from.name) + "\u201D was changed or deleted.\n There is no verb that points back without uploading \u2014 `memgineering push\n --brain " + stripControl(repoint.from.name) + "` would move the pointer AND send this folder into it."
12322
- )
12323
- );
12324
- }
12325
- var isNote = (p) => /\.(md|markdown)$/i.test(p);
12326
- function heldBack(scan) {
12327
- const denied = scan.denied ?? [];
12328
- const byName = denied.filter((d) => d.kind === "name" && isNote(d.path)).map((d) => ({ path: d.path, term: d.term }));
12329
- const explicitly = denied.filter((d) => d.kind === "explicit" && isNote(d.path)).map((d) => d.path);
12330
- const byRule = scan.excluded.filter(isNote);
12331
- const symlinked = scan.skippedSymlinks.filter(isNote);
12332
- return {
12333
- total: byName.length + explicitly.length + byRule.length + symlinked.length,
12334
- byName,
12335
- explicitly,
12336
- byRule,
12337
- symlinked
12338
- };
12339
- }
12340
- function heldBackJson(held) {
12341
- return {
12342
- not_sent: held.total,
12343
- denied_by_name: held.byName,
12344
- denied_explicitly: held.explicitly,
12345
- excluded_by_rule: held.byRule,
12346
- skipped_symlinks: held.symlinked
12347
- };
12348
- }
12349
- function heldBackGroups(held) {
12350
- return [
12351
- {
12352
- label: "name looks private",
12353
- rows: held.byName.map((d) => ({ path: d.path, detail: `matched "${d.term ?? "?"}"` }))
12354
- },
12355
- { label: "listed in .memgdeny", rows: held.explicitly.map((p) => ({ path: p, detail: null })) },
12356
- { label: "excluded by a rule", rows: held.byRule.map((p) => ({ path: p, detail: null })) },
12357
- {
12358
- label: "a symlink, which is never followed",
12359
- rows: held.symlinked.map((p) => ({ path: p, detail: null }))
12360
- }
12361
- ];
12362
- }
12363
- function describeHeldBack(held) {
12364
- return heldBackGroups(held).flatMap(
12365
- ({ label, rows }) => rows.map(
12366
- (r) => `${stripControl(r.path)} \u2014 ${label}${r.detail === null ? "" : ` (${stripControl(r.detail)})`}`
12367
- )
12368
- );
12369
- }
12370
- function printHeldBack(held) {
12371
- if (held.total === 0) return;
12372
- printHuman(
12373
- c.yellow(`
12374
- \u26A0 ${held.total} note(s) held back on this machine \u2014 they never reach the brain:`)
12375
- );
12376
- for (const { label, rows } of heldBackGroups(held)) {
12377
- if (rows.length === 0) continue;
12378
- printHuman(c.gray(` ${rows.length} \u2014 ${label}:`));
12379
- for (const r of rows.slice(0, 10)) {
12380
- printHuman(
12381
- c.gray(
12382
- ` ${stripControl(r.path)}${r.detail === null ? "" : ` (${stripControl(r.detail)})`}`
12383
- )
12384
- );
12385
- }
12386
- if (rows.length > 10) printHuman(c.gray(` \u2026 and ${rows.length - 10} more`));
12387
- }
12388
- if (held.byName.length > 0) {
12389
- printHuman(
12390
- c.gray(
12391
- " 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."
12392
- )
12393
- );
12394
- }
12395
- printHuman(
12396
- c.gray(
12397
- " Counted per file. A rule that excludes a whole FOLDER stops the walk there,\n so notes inside one are not listed above."
12785
+ "\n That is a side effect of `--brain`, and it outlives this command: `recall`,\n `remember` and the memories an agent session starts with all come from the\n brain named here. Nothing in \u201C" + oneLine(repoint.from.name, WIDTH.title) + "\u201D was changed or deleted.\n To point back without uploading: `memgineering use --brain " + oneLine(repoint.from.name, 72) + "`."
12398
12786
  )
12399
12787
  );
12400
12788
  }
@@ -12601,7 +12989,7 @@ function recallCommand() {
12601
12989
  if (semantic.state !== "skipped" || semantic.hint === void 0) return;
12602
12990
  printHuman(
12603
12991
  c.yellow("\u26A0 Meaning was not searched \u2014 this answer matched words only.") + c.gray(`
12604
- ${stripControl(semantic.hint)}
12992
+ ${oneLine(semantic.hint, WIDTH.text)}
12605
12993
  `)
12606
12994
  );
12607
12995
  };
@@ -12649,13 +13037,11 @@ function recallCommand() {
12649
13037
  for (const id of conflict.between) {
12650
13038
  const reason = reasons.get(id);
12651
13039
  if (!reason) {
12652
- printHuman(c.gray(` ${stripControl(id)} \u2014 no reason was recorded.`));
13040
+ printHuman(c.gray(` ${oneLine(id, WIDTH.handle)} \u2014 no reason was recorded.`));
12653
13041
  continue;
12654
13042
  }
12655
13043
  printHuman(
12656
- stripControl(
12657
- ` ${id} \u2014 ${reason.verb === "remember" ? "why it was recorded" : "why it last changed"} (${reason.ts.slice(0, 10)}):`
12658
- )
13044
+ ` ${oneLine(id, WIDTH.handle)} \u2014 ${reason.verb === "remember" ? "why it was recorded" : "why it last changed"} (${reason.ts.slice(0, 10)}):`
12659
13045
  );
12660
13046
  printHuman(quoteBlock(reason.text, " > "));
12661
13047
  }
@@ -12671,8 +13057,10 @@ function recallCommand() {
12671
13057
  );
12672
13058
  }
12673
13059
  if (result.related.length > 0) {
12674
- printHuman(`_related:_ ${result.related.map(stripControl).join(", ")}
12675
- `);
13060
+ printHuman(
13061
+ `_related:_ ${result.related.map((id) => oneLine(id, WIDTH.handle)).join(", ")}
13062
+ `
13063
+ );
12676
13064
  }
12677
13065
  if (result.truncated) {
12678
13066
  const { withheld, estimated_tokens, budget_tokens } = result.truncated;
@@ -12726,9 +13114,8 @@ function asksInSentences(query) {
12726
13114
  return query.trim().split(/\s+/).filter(Boolean).length >= 4;
12727
13115
  }
12728
13116
  function side(id, status, validFrom2) {
12729
- return stripControl(
12730
- `${id} (${status}${validFrom2 === null ? "" : ` \xB7 ${validFrom2.slice(0, 10)}`})`
12731
- );
13117
+ const date = validFrom2 === null ? "" : ` \xB7 ${oneLine(validFrom2.slice(0, 10), WIDTH.handle)}`;
13118
+ return `${oneLine(id, WIDTH.handle)} (${oneLine(status, WIDTH.title)}${date})`;
12732
13119
  }
12733
13120
  async function conflictReasons(vaultRoot, conflicts, byId) {
12734
13121
  const { ops } = await readOps(vaultRoot);
@@ -12747,7 +13134,7 @@ function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffold
12747
13134
  const out = [];
12748
13135
  const standing = card.freshness === "current" ? "" : ` \xB7 ${card.freshness}`;
12749
13136
  const age = Number.isFinite(card.age_days) ? ` \xB7 ${ago2(Math.floor(card.age_days))}` : "";
12750
- out.push(`### ${n}. ${stripControl(card.title)}`);
13137
+ out.push(`### ${n}. ${oneLine(card.title, WIDTH.title)}`);
12751
13138
  if (scaffolding) {
12752
13139
  out.push(c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote"));
12753
13140
  } else {
@@ -12755,14 +13142,14 @@ function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffold
12755
13142
  card.binding ? c.bold("a standing decision") : null,
12756
13143
  `${card.confidence}${standing}${age}`
12757
13144
  ].filter(Boolean);
12758
- out.push(stripControl(marks.join(c.gray(" \xB7 "))));
13145
+ out.push(oneLine(marks.join(c.gray(" \xB7 ")), WIDTH.text));
12759
13146
  }
12760
13147
  if (glossHere && !scaffolding) {
12761
13148
  out.push(c.gray("stated = these fields came from the note, not from inference"));
12762
13149
  }
12763
13150
  if (detail !== "title") {
12764
13151
  if (card.summary) out.push("", quoteBlock(card.summary));
12765
- out.push("", `\`open: ${stripControl(handle2)}\``);
13152
+ out.push("", `\`open: ${oneLine(handle2, WIDTH.handle)}\``);
12766
13153
  }
12767
13154
  if (stage.outline && stage.outline.length > 0) {
12768
13155
  out.push("", ...stage.outline.map((h) => `- ${h}`));
@@ -12893,10 +13280,10 @@ function rememberCommand() {
12893
13280
  ...kindLanded ? {} : { kind_dropped: kind }
12894
13281
  },
12895
13282
  human: () => {
12896
- printHuman(`Remembered in ${stripControl(target.name)}.
13283
+ printHuman(`Remembered in ${oneLine(target.name, WIDTH.title)}.
12897
13284
  `);
12898
13285
  printHuman(
12899
- `note: \`${stripControl(written.id)}\` \xB7 \`${stripControl(written.path)}\``
13286
+ `note: \`${oneLine(written.id, WIDTH.handle)}\` \xB7 \`${oneLine(written.path, WIDTH.path)}\``
12900
13287
  );
12901
13288
  if (opts.rule === true) {
12902
13289
  printHuman("rule: shown before you write a file here, once a session");
@@ -12908,7 +13295,9 @@ function rememberCommand() {
12908
13295
  )
12909
13296
  );
12910
13297
  }
12911
- printHuman(c.gray(`undo: \`memgineering undo ${stripControl(written.op_id)}\``));
13298
+ printHuman(
13299
+ c.gray(`undo: \`memgineering undo ${oneLine(written.op_id, WIDTH.handle)}\``)
13300
+ );
12912
13301
  }
12913
13302
  });
12914
13303
  return;
@@ -13109,9 +13498,7 @@ function resurfaceCommand() {
13109
13498
  `);
13110
13499
  if (unreachableDefault) {
13111
13500
  printHuman(
13112
- c.yellow(
13113
- `Your default brain is not on disk right now: ${unreachableDefault}`
13114
- )
13501
+ c.yellow(`Your default brain is not on disk right now: ${unreachableDefault}`)
13115
13502
  );
13116
13503
  printHuman(
13117
13504
  c.gray(
@@ -13125,7 +13512,7 @@ function resurfaceCommand() {
13125
13512
  );
13126
13513
  for (const [i, { entry, why }] of rankedShown.entries()) {
13127
13514
  const blank = untouched.has(entry.path);
13128
- printHuman(`### ${i + 1}. ${stripControl(entry.memory.title)}`);
13515
+ printHuman(`### ${i + 1}. ${oneLine(entry.memory.title, WIDTH.title)}`);
13129
13516
  printHuman(blank ? c.yellow("not filled in yet") : why.join(" \xB7 "));
13130
13517
  if (blank) {
13131
13518
  const hasWritten = rankedShown.some((r) => !untouched.has(r.entry.path));
@@ -13167,31 +13554,31 @@ async function resurfaceHosted(where, limit) {
13167
13554
  },
13168
13555
  human: () => {
13169
13556
  if (result.cards.length === 0) {
13170
- printHuman(`## resurface \u2014 ${stripControl(where.name)}
13557
+ printHuman(`## resurface \u2014 ${oneLine(where.name, WIDTH.title)}
13171
13558
  `);
13172
13559
  printHuman("_Nothing to bring up yet._\n");
13173
- if (result.hint) printHuman(c.gray(stripControl(result.hint)));
13560
+ if (result.hint) printHuman(c.gray(oneLine(result.hint, WIDTH.text)));
13174
13561
  return;
13175
13562
  }
13176
- printHuman(`## resurface \u2014 ${stripControl(where.name)} (${result.cards.length} cards)
13177
- `);
13563
+ printHuman(
13564
+ `## resurface \u2014 ${oneLine(where.name, WIDTH.title)} (${result.cards.length} cards)
13565
+ `
13566
+ );
13178
13567
  for (const [i, card] of result.cards.entries()) {
13179
- printHuman(`### ${i + 1}. ${stripControl(card.title)}`);
13568
+ printHuman(`### ${i + 1}. ${oneLine(card.title, WIDTH.title)}`);
13180
13569
  if (card.untouched === true) printHuman(c.yellow("not filled in yet"));
13181
- else if (card.why.length > 0) printHuman(stripControl(card.why.join(" \xB7 ")));
13570
+ else if (card.why.length > 0) printHuman(oneLine(card.why.join(" \xB7 "), WIDTH.text));
13182
13571
  if (card.summary) printHuman(`
13183
13572
  ${quoteBlock(card.summary)}`);
13184
13573
  printHuman(`
13185
- \`open: ${stripControl(card.handle)}\`
13574
+ \`open: ${oneLine(card.handle, WIDTH.handle)}\`
13186
13575
  `);
13187
13576
  }
13188
- printHuman(
13577
+ printBlock(
13189
13578
  c.gray(
13190
- stripControl(
13191
- `Ranked by ${result.ranked_by.join(", ")}. A hosted brain does not record where a
13579
+ `Ranked by ${oneLine(result.ranked_by.join(", "), WIDTH.text)}. A hosted brain does not record where a
13192
13580
  question was asked from, so "in this folder" is not one of them \u2014 that signal
13193
13581
  exists only for a brain on this machine.`
13194
- )
13195
13582
  )
13196
13583
  );
13197
13584
  }
@@ -13212,7 +13599,7 @@ function rulesCommand() {
13212
13599
  printDual({
13213
13600
  json: { brain: where.name, hosted: true, rules: rules2, total: total2 },
13214
13601
  human: () => {
13215
- printHuman(`## rules \u2014 ${stripControl(where.name)} (${total2})
13602
+ printHuman(`## rules \u2014 ${oneLine(where.name, WIDTH.title)} (${total2})
13216
13603
  `);
13217
13604
  if (rules2.length === 0) {
13218
13605
  printHuman("_Nothing is marked as a rule in this brain._\n");
@@ -13224,8 +13611,8 @@ function rulesCommand() {
13224
13611
  return;
13225
13612
  }
13226
13613
  for (const rule of rules2) {
13227
- printHuman(`- ${stripControl(rule.summary ?? rule.title)}`);
13228
- printHuman(c.gray(` \`open: ${stripControl(rule.handle)}\``));
13614
+ printHuman(`- ${oneLine(rule.summary ?? rule.title, WIDTH.text)}`);
13615
+ printHuman(c.gray(` \`open: ${oneLine(rule.handle, WIDTH.handle)}\``));
13229
13616
  }
13230
13617
  if (total2 > rules2.length) {
13231
13618
  printHuman(c.gray(`
@@ -13277,10 +13664,10 @@ This answered from \`${brainRoot}\` instead, so rules you wrote in the missing o
13277
13664
  printHuman(`## rules (${rules.length}${total > rules.length ? ` of ${total}` : ""})
13278
13665
  `);
13279
13666
  for (const { entry } of rules) {
13280
- printHuman(`- ${stripControl(entry.memory.summary ?? entry.memory.title)}`);
13667
+ printHuman(`- ${oneLine(entry.memory.summary ?? entry.memory.title, WIDTH.text)}`);
13281
13668
  printHuman(
13282
13669
  c.gray(
13283
- ` \`open: ${displayHandle(entry.memory.id, allIds)}\`${entry.memory.scope ? ` \xB7 ${stripControl(entry.memory.scope)}` : ""}`
13670
+ ` \`open: ${displayHandle(entry.memory.id, allIds)}\`${entry.memory.scope ? ` \xB7 ${oneLine(entry.memory.scope, WIDTH.title)}` : ""}`
13284
13671
  )
13285
13672
  );
13286
13673
  }
@@ -13642,7 +14029,7 @@ async function reportPreviousAttempt() {
13642
14029
  function reportNotice(text) {
13643
14030
  if (!text) return;
13644
14031
  printHuman(c.gray(`
13645
- note from memgineering.com: "${stripControl(text)}"`));
14032
+ note from memgineering.com: "${oneLine(text, WIDTH.text)}"`));
13646
14033
  }
13647
14034
  function reportBelowMinimum(below) {
13648
14035
  if (!below) return;
@@ -13680,11 +14067,11 @@ function undoCommand() {
13680
14067
  json: { ...undone, brain: { hosted: true, name: where.name } },
13681
14068
  human: () => {
13682
14069
  printHuman(
13683
- `Took back \`${stripControl(undone.undone)}\` \u2014 a ${stripControl(undone.verb)}.
14070
+ `Took back \`${oneLine(undone.undone, WIDTH.handle)}\` \u2014 a ${oneLine(undone.verb, WIDTH.title)}.
13684
14071
  `
13685
14072
  );
13686
- printHuman(c.gray(` ${stripControl(undone.target)}`));
13687
- printHuman(c.gray(` recorded as \`${stripControl(undone.op_id)}\``));
14073
+ printHuman(c.gray(` ${oneLine(undone.target, WIDTH.path)}`));
14074
+ printHuman(c.gray(` recorded as \`${oneLine(undone.op_id, WIDTH.handle)}\``));
13688
14075
  }
13689
14076
  });
13690
14077
  return;
@@ -13770,7 +14157,7 @@ ${matches.slice(0, 8).map((id) => ` ${id}`).join("\n")}${matches.length > 8 ? "
13770
14157
  },
13771
14158
  human: () => {
13772
14159
  printHuman(
13773
- `Undone: ${stripControl(target.verb)} on \`${stripControl(target.target)}\`
14160
+ `Undone: ${oneLine(target.verb, WIDTH.title)} on \`${oneLine(target.target, WIDTH.path)}\`
13774
14161
  `
13775
14162
  );
13776
14163
  if (opId === void 0) {
@@ -13861,13 +14248,91 @@ Read what it says now, then decide what should stay.`;
13861
14248
  The text from before that change is still on this machine: ${join23(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
13862
14249
  }
13863
14250
 
14251
+ // src/commands/brains.ts
14252
+ init_errors();
14253
+ init_api_client();
14254
+ import { Command as Command25 } from "commander";
14255
+ init_config();
14256
+ init_ui();
14257
+ var NAME_WIDTH = 48;
14258
+ function brainsCommand() {
14259
+ return new Command25("brains").description("list the hosted brains on your account, and which one this machine reads").addHelpText(
14260
+ "after",
14261
+ "\nReads only. `memgineering use --brain <name>` points this machine at one of\nthem; `memgineering push --brain <name>` is what creates a new one."
14262
+ ).action(async () => {
14263
+ const base = apiUrl();
14264
+ const credentials = await requireCredentials(base);
14265
+ const brains = await listCloudBrains(credentials.token, base);
14266
+ const pointer = (await loadConfig()).brain.cloud;
14267
+ const currentId = pointer?.api_url === base ? pointer.id : null;
14268
+ if (brains.length === 0) {
14269
+ throw memgError(
14270
+ "invalid_input",
14271
+ "this account has no hosted brains",
14272
+ "Nothing is wrong \u2014 none has been made yet. `memgineering push --brain <name>` creates one and fills it from a folder on this machine."
14273
+ );
14274
+ }
14275
+ const rows = brains.map((b) => ({
14276
+ id: b.id,
14277
+ name: b.name,
14278
+ // Passed through rather than defaulted to 0. A server that does not send
14279
+ // a count and a brain that holds no notes are different facts, and the
14280
+ // second is alarming.
14281
+ note_count: b.note_count ?? null,
14282
+ created_at: b.created_at ?? null,
14283
+ current: b.id === currentId
14284
+ }));
14285
+ printDual({
14286
+ json: {
14287
+ // WHOSE ACCOUNT, on the command that lists what the account holds.
14288
+ //
14289
+ // "What is this computer connected to?" took three commands to answer:
14290
+ // `whoami` for the identity, this for the brains, `use` for the folder
14291
+ // — and `whoami`, the one named after the account, was the only one
14292
+ // that answered none of the rest. Two round-test agents ran all three.
14293
+ // The name is free here; it came back with the token.
14294
+ account: { id: credentials.account_id, name: credentials.display_name },
14295
+ server: base,
14296
+ brains: rows,
14297
+ count: rows.length,
14298
+ // Named at the top level too, so a caller does not have to scan the
14299
+ // array to answer the question it most often has.
14300
+ current: currentId,
14301
+ hint: currentId === null ? "This machine is not pointed at any of them. `memgineering use --brain <name>` picks one \u2014 it moves the pointer and uploads nothing." : "Switch with `memgineering use --brain <name>`, which moves the pointer and uploads nothing. `push --brain <name>` is what creates a new brain."
14302
+ },
14303
+ human: () => {
14304
+ printHuman(
14305
+ `## ${rows.length} brain${rows.length === 1 ? "" : "s"} on ${credentials.display_name === null ? "this account" : `${oneLine(credentials.display_name, NAME_WIDTH)}\u2019s account`}
14306
+ `
14307
+ );
14308
+ for (const row of rows) {
14309
+ const name = oneLine(row.name, NAME_WIDTH).padEnd(NAME_WIDTH);
14310
+ const notes = row.note_count === null ? "" : `${String(row.note_count).padStart(6)} note(s)`;
14311
+ const made = row.created_at === null ? "" : ` made ${row.created_at.slice(0, 10)}`;
14312
+ printHuman(
14313
+ ` ${name}${c.gray(notes)}${c.gray(made)}` + (row.current ? c.yellow(" \u2190 this machine reads this one") : "")
14314
+ );
14315
+ }
14316
+ printHuman("");
14317
+ printHuman(
14318
+ c.gray(
14319
+ currentId === null ? " This machine is not pointed at any of them.\n `memgineering use --brain <name>` picks one \u2014 nothing is uploaded." : " `memgineering use --brain <name>` switches \u2014 nothing is uploaded.\n `memgineering push --brain <name>` is what creates a new one."
14320
+ )
14321
+ );
14322
+ }
14323
+ });
14324
+ });
14325
+ }
14326
+
13864
14327
  // src/commands/use.ts
13865
14328
  init_atomic();
13866
14329
  init_brand();
13867
14330
  init_errors();
13868
- init_config();
14331
+ init_api_client();
14332
+ init_credentials();
13869
14333
  import { isAbsolute as isAbsolute5, relative as relative3, resolve as resolve11 } from "path";
13870
- import { Command as Command25 } from "commander";
14334
+ import { Command as Command26 } from "commander";
14335
+ init_config();
13871
14336
  init_vault();
13872
14337
  init_ui();
13873
14338
  function lostDefaultLines(missing) {
@@ -13886,16 +14351,24 @@ var VIA_MEANS = {
13886
14351
  default: "because it is your default brain (`memgineering use --default <path>` changes it)"
13887
14352
  };
13888
14353
  function useCommand() {
13889
- return new Command25("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").option(
14354
+ return new Command26("use").description(
14355
+ "say which brain answers here \u2014 `--brain <name>` for the whole machine, or a path to bind this folder"
14356
+ ).argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--brain <name>", "point this machine at a hosted brain, by its name or id").option("--here <dir>", "write the pointer in this directory instead of the cwd").option(
13890
14357
  "--default",
13891
- "make this the brain used anywhere no pointer applies, instead of binding one folder"
14358
+ "for a FOLDER on this machine: make it the one used wherever no pointer applies. Not for hosted brains \u2014 `--brain <name>` is that"
13892
14359
  ).action(async (brainPath, opts) => {
14360
+ if (opts.brain !== void 0) {
14361
+ await pointAtHosted(opts.brain, opts, brainPath);
14362
+ return;
14363
+ }
13893
14364
  const where = await canonicalize(resolve11(opts.here ?? process.cwd()));
13894
14365
  if (!brainPath) {
13895
14366
  const pointer = await readPointer(where);
13896
14367
  const resolved = await resolveBrain(void 0, where).catch((err) => err);
13897
14368
  const stuck = resolved instanceof Error;
13898
14369
  const hosted = await hostedPointer();
14370
+ const stored = await readCredentials().catch(() => null);
14371
+ const signedIn = stored !== null && stored.api_url === apiUrl();
13899
14372
  printDual({
13900
14373
  json: {
13901
14374
  // The brain that answers `recall` and takes `remember` — the same
@@ -13930,14 +14403,24 @@ function useCommand() {
13930
14403
  brain: stuck ? null : resolved.brain.root,
13931
14404
  via: stuck ? null : resolved.via,
13932
14405
  pointer: pointer ? pointer.from : null,
13933
- ...stuck ? {
14406
+ // ONLY WHEN THE LOCAL FOLDER IS THE ANSWER. `stuck` describes the
14407
+ // FOLDER resolver, and on a machine reading a hosted brain that
14408
+ // resolver having nothing is the expected state, not a problem —
14409
+ // but it was reported unqualified beside `destination: "hosted"`,
14410
+ // so a successful switch produced `problem: "no_vault_linked"` and
14411
+ // `message: "no brain is linked yet"` in the same document that
14412
+ // named the brain it had just moved to. Two agents read `problem`
14413
+ // first and would have told the user the switch failed.
14414
+ //
14415
+ // `--local` is how somebody asks what the folder side would do.
14416
+ ...stuck && hosted?.signedIn !== true ? {
13934
14417
  problem: resolved.code ?? "unresolved",
13935
14418
  message: resolved.message
13936
14419
  } : {}
13937
14420
  },
13938
14421
  human: () => {
13939
14422
  if (hosted?.signedIn) {
13940
- printHuman(`Using the hosted brain \`${stripControl(hosted.name)}\`
14423
+ printHuman(`Using the hosted brain \`${oneLine(hosted.name, NAME_WIDTH2)}\`
13941
14424
  `);
13942
14425
  printHuman(
13943
14426
  c.gray(
@@ -13958,7 +14441,7 @@ function useCommand() {
13958
14441
  );
13959
14442
  printHuman(
13960
14443
  c.yellow(
13961
- ` A sign-in to \`${stripControl(hosted.name)}\` is waiting to be approved.`
14444
+ ` A sign-in to \`${oneLine(hosted.name, NAME_WIDTH2)}\` is waiting to be approved.`
13962
14445
  )
13963
14446
  );
13964
14447
  printHuman(
@@ -13975,7 +14458,9 @@ function useCommand() {
13975
14458
  printHuman(`No brain resolves here.
13976
14459
  `);
13977
14460
  printHuman(
13978
- c.yellow(` Pointed at the hosted brain \`${stripControl(hosted.name)}\`,`) + c.gray(" with no sign-in on this machine that matches it,\n") + c.gray(" and no brain linked here either. `memgineering login` connects it.")
14461
+ c.yellow(
14462
+ ` Pointed at the hosted brain \`${oneLine(hosted.name, NAME_WIDTH2)}\`,`
14463
+ ) + c.gray(" with no sign-in on this machine that matches it,\n") + c.gray(" and no brain linked here either. `memgineering login` connects it.")
13979
14464
  );
13980
14465
  return;
13981
14466
  }
@@ -13984,7 +14469,7 @@ function useCommand() {
13984
14469
  printHuman(c.gray(` ${VIA_MEANS[resolved.via]}`));
13985
14470
  printHuman(
13986
14471
  c.yellow(
13987
- ` This machine is pointed at the hosted brain \`${stripControl(hosted.name)}\` and is not signed in to it,`
14472
+ ` This machine is pointed at the hosted brain \`${oneLine(hosted.name, NAME_WIDTH2)}\` and is not signed in to it,`
13988
14473
  ) + c.gray("\n so reads and writes are landing here instead.")
13989
14474
  );
13990
14475
  printHuman(
@@ -13995,6 +14480,14 @@ function useCommand() {
13995
14480
  if (stuck) {
13996
14481
  printHuman(`No brain resolves here.
13997
14482
  `);
14483
+ if (signedIn) {
14484
+ printHuman(
14485
+ c.yellow(" This machine is signed in, and pointed at no brain.") + c.gray(
14486
+ "\n Your notes may be on the account rather than missing.\n `memgineering brains` lists them; `memgineering use --brain <name>` points here.\n `init` and `link` make a NEW empty brain on this machine \u2014 not what you want\n if the notes are already on the account."
14487
+ )
14488
+ );
14489
+ return;
14490
+ }
13998
14491
  printHuman(c.gray(` ${resolved.message}`));
13999
14492
  const hint = resolved.hint;
14000
14493
  if (hint) printHuman(c.gray(`
@@ -14061,13 +14554,114 @@ A pointer to an unlinked folder would fail on every command instead of at this o
14061
14554
  });
14062
14555
  });
14063
14556
  }
14557
+ var NAME_WIDTH2 = 72;
14558
+ function describeSize(count) {
14559
+ if (count === void 0) return "";
14560
+ if (count === 0)
14561
+ return " \u2014 which is EMPTY. Nothing here has been lost; that brain has no notes yet";
14562
+ return ` \u2014 ${count} note(s)`;
14563
+ }
14564
+ async function pointAtHosted(named, opts, path) {
14565
+ if (named.startsWith("--")) {
14566
+ throw memgError(
14567
+ "invalid_input",
14568
+ `\`--brain\` was given the flag \`${oneLine(named, 32)}\` as its name`,
14569
+ "Nothing was changed. `--brain` takes the name or id of a hosted brain, and the\nflag written after it was read as that name. Put the name first \u2014 `memgineering\nuse --brain <name>` \u2014 and run the other flag as its own command."
14570
+ );
14571
+ }
14572
+ const clash = path !== void 0 ? "a folder path" : opts.default === true ? "`--default`" : opts.here !== void 0 ? "`--here`" : null;
14573
+ if (clash !== null) {
14574
+ throw memgError(
14575
+ "invalid_input",
14576
+ `\`--brain\` cannot be combined with ${clash}`,
14577
+ "They point different things. `--brain <name>` moves the one hosted brain this machine reads, which is a single setting for the whole machine; a path, `--default` and `--here` all bind a FOLDER on this disk. Run them separately."
14578
+ );
14579
+ }
14580
+ const base = apiUrl();
14581
+ const credentials = await requireCredentials(base).catch((err) => {
14582
+ if (err instanceof MemgError && err.code === "not_signed_in") {
14583
+ throw memgError(
14584
+ "not_signed_in",
14585
+ "this machine is not signed in to any account",
14586
+ "Nothing was changed. `--brain` names a brain on an ACCOUNT, so there has to be one:\n\n `memgineering login --emit-only` then show the user the code, and re-run this\n\nA brain in a folder on this machine is a different thing and `use <path>` binds one."
14587
+ );
14588
+ }
14589
+ throw err;
14590
+ });
14591
+ const brains = await listCloudBrains(credentials.token, base);
14592
+ if (brains.length === 0) {
14593
+ throw memgError(
14594
+ "invalid_input",
14595
+ "this account has no hosted brains",
14596
+ "Nothing was changed. `memgineering push --brain <name>` creates one and fills it from a folder on this machine."
14597
+ );
14598
+ }
14599
+ const found = brains.find((b) => b.id === named) ?? brains.find((b) => b.name === named);
14600
+ if (!found) {
14601
+ throw memgError(
14602
+ "invalid_input",
14603
+ `no hosted brain called "${oneLine(named, NAME_WIDTH2)}"`,
14604
+ "Nothing was changed. This account has:\n" + brains.map((b) => ` ${oneLine(b.name, NAME_WIDTH2)} (${oneLine(b.id, NAME_WIDTH2)})`).join("\n") + "\nIf one of those is the one they meant, use it. If they genuinely want a NEW\nnotebook, ASK THEM FIRST and say what it costs: `memgineering push --brain\n<name>` creates one by UPLOADING a folder into it, so a new brain arrives\nholding whatever that folder held \u2014 which on a fresh one is five empty\ntemplate files, not their notes. Creating one is not a way to find\nnotes that already exist."
14605
+ );
14606
+ }
14607
+ const config = await loadConfig();
14608
+ const previous = config.brain.cloud ?? null;
14609
+ const unchanged = previous?.id === found.id && previous.api_url === base && previous.name === found.name;
14610
+ if (!unchanged) {
14611
+ await saveConfig({
14612
+ ...config,
14613
+ brain: { ...config.brain, cloud: { id: found.id, name: found.name, api_url: base } }
14614
+ });
14615
+ }
14616
+ printDual({
14617
+ json: {
14618
+ brain: {
14619
+ id: found.id,
14620
+ name: found.name,
14621
+ api_url: base,
14622
+ // Carried because the human line says it. A caller that has to ask a
14623
+ // second time to learn the destination is empty is a caller that will
14624
+ // not ask.
14625
+ ...found.note_count === void 0 ? {} : { note_count: found.note_count }
14626
+ },
14627
+ previous: previous ? { id: previous.id, name: previous.name } : null,
14628
+ // Present even when nothing moved, so a caller can tell "already there"
14629
+ // from "switched" without comparing two names itself.
14630
+ changed: !unchanged
14631
+ },
14632
+ human: () => {
14633
+ if (unchanged) {
14634
+ printHuman(`Already using \`${oneLine(found.name, NAME_WIDTH2)}\`.
14635
+ `);
14636
+ printHuman(c.gray(" Nothing changed."));
14637
+ return;
14638
+ }
14639
+ printHuman(
14640
+ `Now using the hosted brain \`${oneLine(found.name, NAME_WIDTH2)}\`${describeSize(found.note_count)}.
14641
+ `
14642
+ );
14643
+ if (previous) {
14644
+ printHuman(
14645
+ c.yellow(
14646
+ ` This machine was reading \`${oneLine(previous.name, NAME_WIDTH2)}\` and now is not.`
14647
+ )
14648
+ );
14649
+ }
14650
+ printHuman(
14651
+ c.gray(
14652
+ " Reads and writes go there from now on. Nothing was uploaded and nothing\n was downloaded \u2014 `push` and `pull` are what move notes."
14653
+ )
14654
+ );
14655
+ }
14656
+ });
14657
+ }
14064
14658
 
14065
14659
  // src/commands/whoami.ts
14066
14660
  init_api_client();
14067
14661
  init_ui();
14068
- import { Command as Command26 } from "commander";
14662
+ import { Command as Command27 } from "commander";
14069
14663
  function whoamiCommand() {
14070
- return new Command26("whoami").description("show which account this machine is signed in as").action(async () => {
14664
+ return new Command27("whoami").description("show which account this machine is signed in as").action(async () => {
14071
14665
  const base = apiUrl();
14072
14666
  const credentials = await requireCredentials(base);
14073
14667
  const res = await apiRequest({
@@ -14093,9 +14687,11 @@ function whoamiCommand() {
14093
14687
  },
14094
14688
  human: () => {
14095
14689
  printHuman(
14096
- c.bold(stripControl(account.display_name ?? account.id ?? credentials.account_id))
14690
+ c.bold(
14691
+ oneLine(account.display_name ?? account.id ?? credentials.account_id, WIDTH.title)
14692
+ )
14097
14693
  );
14098
- if (account.email) printHuman(c.gray(` ${stripControl(account.email)}`));
14694
+ if (account.email) printHuman(c.gray(` ${oneLine(account.email, WIDTH.title)}`));
14099
14695
  printHuman(c.gray(` on ${base}`));
14100
14696
  if (live !== null) {
14101
14697
  printHuman(
@@ -14110,6 +14706,8 @@ function whoamiCommand() {
14110
14706
  }
14111
14707
 
14112
14708
  // src/lib/next-step.ts
14709
+ init_api_client();
14710
+ init_credentials();
14113
14711
  init_config();
14114
14712
  function isSetUp(cfg) {
14115
14713
  return Object.keys(cfg.system.installed).length > 0;
@@ -14163,7 +14761,16 @@ Then run \`memgineering onboard\`: it prints what to ask so the brain knows whos
14163
14761
  it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
14164
14762
 
14165
14763
  Read \`memgineering-setup\` for accounts and which of their tools this reaches.`;
14166
- function nextStepFor(cfg) {
14764
+ var POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
14765
+
14766
+ \`memgineering brains\` lists what is on the account.
14767
+ \`memgineering use --brain <name>\` points this machine at one.
14768
+ `;
14769
+ function nextStepFor(cfg, signedIn = false) {
14770
+ if (signedIn && cfg.brain.cloud === void 0) {
14771
+ return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
14772
+ }
14773
+ if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
14167
14774
  if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
14168
14775
  if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
14169
14776
  return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
@@ -14172,7 +14779,8 @@ function nextStepFor(cfg) {
14172
14779
  }
14173
14780
  async function currentNextStep() {
14174
14781
  try {
14175
- return nextStepFor(await loadConfig());
14782
+ const stored = await readCredentials().catch(() => null);
14783
+ return nextStepFor(await loadConfig(), stored !== null && stored.api_url === apiUrl());
14176
14784
  } catch {
14177
14785
  return null;
14178
14786
  }
@@ -14182,7 +14790,7 @@ async function currentNextStep() {
14182
14790
  init_ui();
14183
14791
  init_brand();
14184
14792
  function buildProgram() {
14185
- const program = new Command27("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", async (thisCommand, actionCommand) => {
14793
+ const program = new Command28("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", async (thisCommand, actionCommand) => {
14186
14794
  const opts = thisCommand.optsWithGlobals();
14187
14795
  if (opts.json || process.env[BRAND.jsonEnvVar] === "1") setJsonMode(true);
14188
14796
  await announceNextStep(actionCommand.name());
@@ -14203,6 +14811,7 @@ function buildProgram() {
14203
14811
  program.addCommand(initCommand());
14204
14812
  program.addCommand(linkCommand());
14205
14813
  program.addCommand(useCommand());
14814
+ program.addCommand(brainsCommand());
14206
14815
  program.addCommand(unlinkCommand());
14207
14816
  program.addCommand(reindexCommand());
14208
14817
  program.addCommand(syncRulesCommand());
@@ -14242,8 +14851,15 @@ function makeParseErrorsActionable(program) {
14242
14851
  outputError: (str, write) => {
14243
14852
  write(str);
14244
14853
  if (!/unknown option|unknown command|too many arguments/i.test(str)) return;
14245
- const flags = cmd.options.map((o) => o.long ?? o.short).filter(Boolean);
14246
14854
  const path = cmd.parent === null ? cmd.name() : `${cmd.parent.name()} ${cmd.name()}`;
14855
+ if (/unknown command/i.test(str) && cmd.commands.length > 0) {
14856
+ write(
14857
+ `hint: ${path} has ${cmd.commands.map((c2) => c2.name()).sort().join(" ")}
14858
+ `
14859
+ );
14860
+ return;
14861
+ }
14862
+ const flags = cmd.options.map((o) => o.long ?? o.short).filter(Boolean);
14247
14863
  write(
14248
14864
  flags.length > 0 ? `hint: ${path} takes ${flags.join(" ")}
14249
14865
  ` : `hint: \`${path} --help\` lists what this takes.
@@ -14271,7 +14887,7 @@ async function handleError(err) {
14271
14887
  if (isJsonMode()) {
14272
14888
  printJson({ error: { code: err.code, message: err.message, hint: err.hint } });
14273
14889
  } else {
14274
- printHuman(c.red(`\u2717 ${stripControl(err.message)}`));
14890
+ printHuman(c.red(`\u2717 ${oneLine(err.message, WIDTH.text)}`));
14275
14891
  for (const line of stripControl(err.hint).split("\n")) printHuman(c.gray(` \u2192 ${line}`));
14276
14892
  }
14277
14893
  process.exit(1);
@@ -14288,7 +14904,7 @@ async function handleError(err) {
14288
14904
  }
14289
14905
  });
14290
14906
  } else {
14291
- printHuman(c.red(`\u2717 ${stripControl(err.message)}`));
14907
+ printHuman(c.red(`\u2717 ${oneLine(err.message, WIDTH.text)}`));
14292
14908
  if (hintStr) {
14293
14909
  for (const line of stripControl(hintStr).split("\n")) printHuman(c.gray(` \u2192 ${line}`));
14294
14910
  }
@@ -14296,7 +14912,7 @@ async function handleError(err) {
14296
14912
  process.exit(1);
14297
14913
  }
14298
14914
  if (isJsonMode()) printJson({ error: { code: "generic_failure", message: String(err) } });
14299
- else printHuman(c.red(`\u2717 ${stripControl(String(err))}`));
14915
+ else printHuman(c.red(`\u2717 ${oneLine(String(err), WIDTH.text)}`));
14300
14916
  process.exit(1);
14301
14917
  }
14302
14918