memgineering 0.7.5 → 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
  /**
@@ -173,6 +186,9 @@ function oneLine(text, max) {
173
186
  if (chars.length <= max) return flat;
174
187
  return `${chars.slice(0, Math.max(1, max - 1)).join("").trimEnd()}\u2026`;
175
188
  }
189
+ function printBlock(text) {
190
+ printHuman(stripControl(text));
191
+ }
176
192
  function progress(text) {
177
193
  if (isInteractive()) process.stderr.write(kleur.gray(` ${text}
178
194
  `));
@@ -187,13 +203,23 @@ function undoHint(opId, vault) {
187
203
  function quoteArg(value) {
188
204
  return /^[A-Za-z0-9._\/-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
189
205
  }
190
- var jsonMode, CONTROL_INTRODUCER, c;
206
+ var jsonMode, CONTROL_INTRODUCER, WIDTH, c;
191
207
  var init_ui = __esm({
192
208
  "src/ui.ts"() {
193
209
  "use strict";
194
210
  init_brand();
195
211
  jsonMode = false;
196
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
+ };
197
223
  c = kleur;
198
224
  }
199
225
  });
@@ -4430,9 +4456,11 @@ function linkCommand() {
4430
4456
  Nothing was indexed. This is what WOULD be stored:
4431
4457
  `));
4432
4458
  for (const r of rows) {
4433
- printHuman(` ${c.bold(stripControl(r.title))}`);
4434
- printHuman(c.gray(` ${stripControl(r.summary ?? "(no summary \u2014 title only)")}`));
4435
- 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)}`));
4436
4464
  }
4437
4465
  printHuman(
4438
4466
  c.gray(`
@@ -4705,8 +4733,8 @@ Brain: ${root}`));
4705
4733
  if (samples.length > 0) {
4706
4734
  printHuman(c.gray("\n For example, these exact lines would be stored:"));
4707
4735
  for (const r of samples) {
4708
- printHuman(c.gray(` ${stripControl(r.title)}`));
4709
- 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)}`));
4710
4738
  }
4711
4739
  if (notes.length > samples.length) {
4712
4740
  printHuman(
@@ -4826,7 +4854,7 @@ function loginCommand() {
4826
4854
  },
4827
4855
  human: () => {
4828
4856
  printHuman(
4829
- `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))}` : ""}.`
4830
4858
  );
4831
4859
  printHuman(c.gray(" `memgineering logout` first if you want a different account."));
4832
4860
  }
@@ -4899,13 +4927,13 @@ function emit(pending, ctx) {
4899
4927
  },
4900
4928
  human: () => {
4901
4929
  printHuman("Show this code to whoever is signing in:\n");
4902
- printHuman(` ${c.bold().yellow(stripControl(pending.user_code))}
4930
+ printHuman(` ${c.bold().yellow(oneLine(pending.user_code, WIDTH.handle))}
4903
4931
  `);
4904
4932
  if (ctx.opened) {
4905
4933
  printHuman("Opened the approval page in your browser.");
4906
4934
  } else {
4907
4935
  printHuman("Open this to approve:\n");
4908
- printHuman(` ${c.cyan(stripControl(pending.verification_url))}`);
4936
+ printHuman(` ${c.cyan(oneLine(pending.verification_url, WIDTH.path))}`);
4909
4937
  }
4910
4938
  printHuman(
4911
4939
  c.gray(
@@ -4913,7 +4941,7 @@ function emit(pending, ctx) {
4913
4941
  )
4914
4942
  );
4915
4943
  if (ctx.opened && ctx.showUrl) {
4916
- printHuman(c.gray(` ${stripControl(pending.verification_url)}`));
4944
+ printHuman(c.gray(` ${oneLine(pending.verification_url, WIDTH.path)}`));
4917
4945
  }
4918
4946
  printHuman(
4919
4947
  c.gray(
@@ -4955,7 +4983,7 @@ async function waitForApproval(pending, base) {
4955
4983
  human: () => {
4956
4984
  printHuman(
4957
4985
  `
4958
- 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))}` : ""}.`
4959
4987
  );
4960
4988
  printHuman(c.gray(" The token is stored on this machine only, readable by you alone."));
4961
4989
  }
@@ -5056,17 +5084,19 @@ function printSignInStep(step) {
5056
5084
  switch (step.status) {
5057
5085
  case "signed-in":
5058
5086
  printHuman(
5059
- c.gray(` signed in${step.displayName ? ` as ${stripControl(step.displayName)}` : ""}
5060
- `)
5087
+ c.gray(
5088
+ ` signed in${step.displayName ? ` as ${oneLine(step.displayName, WIDTH.title)}` : ""}
5089
+ `
5090
+ )
5061
5091
  );
5062
5092
  return;
5063
5093
  case "started":
5064
5094
  printHuman("Show this code to whoever is signing in:\n");
5065
- printHuman(` ${c.bold().yellow(stripControl(step.userCode))}
5095
+ printHuman(` ${c.bold().yellow(oneLine(step.userCode, WIDTH.handle))}
5066
5096
  `);
5067
5097
  printHuman(
5068
5098
  step.opened ? c.gray(" The approval page is open in the browser.") : `Open this to approve:
5069
- ${c.cyan(stripControl(step.verificationUrl))}`
5099
+ ${c.cyan(oneLine(step.verificationUrl, WIDTH.path))}`
5070
5100
  );
5071
5101
  printHuman(
5072
5102
  c.gray(" Not waiting \u2014 setup carries on, and the next command collects the token.\n")
@@ -5074,12 +5104,14 @@ function printSignInStep(step) {
5074
5104
  return;
5075
5105
  case "pending":
5076
5106
  printHuman(
5077
- c.gray(` a sign-in is waiting for approval \u2014 code ${stripControl(step.userCode)}
5078
- `)
5107
+ c.gray(
5108
+ ` a sign-in is waiting for approval \u2014 code ${oneLine(step.userCode, WIDTH.handle)}
5109
+ `
5110
+ )
5079
5111
  );
5080
5112
  return;
5081
5113
  case "unavailable":
5082
- 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)}`));
5083
5115
  printHuman(c.gray(" Setup continues \u2014 an account is not needed for any of it.\n"));
5084
5116
  return;
5085
5117
  case "would-start":
@@ -7387,11 +7419,11 @@ var init_setup_web = __esm({
7387
7419
  // src/commands/setup.ts
7388
7420
  import { createHash as createHash8 } from "crypto";
7389
7421
  import { access, mkdir as mkdir13, readFile as readFile19 } from "fs/promises";
7390
- import { homedir as homedir4 } from "os";
7391
7422
  import { dirname as dirname5, join as join19 } from "path";
7392
7423
  import { fileURLToPath } from "url";
7393
7424
  import { Command as Command22 } from "commander";
7394
7425
  import prompts3 from "prompts";
7426
+ import { homedir as homedir4 } from "os";
7395
7427
  function setupCommand() {
7396
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(
7397
7429
  "--tools <list>",
@@ -7463,6 +7495,18 @@ function setupCommand() {
7463
7495
  ...signInStepJson(signIn),
7464
7496
  defaults_used: defaultsTaken,
7465
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() } : {},
7466
7510
  tools: choice.tools.map((t) => t.id),
7467
7511
  auto_update: choice.autoUpdate,
7468
7512
  hook: choice.hook,
@@ -7486,9 +7530,27 @@ function setupCommand() {
7486
7530
  if (opts.silent && changed.length === 0) return;
7487
7531
  if (opts.dryRun) {
7488
7532
  printSignInStep(signIn);
7489
- 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):
7490
7546
  `);
7491
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
+ );
7492
7554
  return;
7493
7555
  }
7494
7556
  printSignInStep(signIn);
@@ -7589,7 +7651,7 @@ async function applySetup(choice, dryRun) {
7589
7651
  const hub = await loadAsset(BRAND.hubFileName);
7590
7652
  const installed = { ...(await loadConfig()).system.installed };
7591
7653
  const mark = (path, hash) => {
7592
- const key2 = path.startsWith(homedir4()) ? path.slice(homedir4().length + 1) : path;
7654
+ const key2 = path.startsWith(agentHome()) ? path.slice(agentHome().length + 1) : path;
7593
7655
  installed[key2] = hash;
7594
7656
  };
7595
7657
  const persist = async () => {
@@ -7601,11 +7663,11 @@ async function applySetup(choice, dryRun) {
7601
7663
  });
7602
7664
  };
7603
7665
  const wasEdited = (path, onDisk) => {
7604
- const key2 = path.startsWith(homedir4()) ? path.slice(homedir4().length + 1) : path;
7666
+ const key2 = path.startsWith(agentHome()) ? path.slice(agentHome().length + 1) : path;
7605
7667
  const known = installed[key2];
7606
7668
  return known !== void 0 && onDisk !== null && fingerprint(onDisk) !== known;
7607
7669
  };
7608
- 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;
7609
7671
  try {
7610
7672
  return await write();
7611
7673
  } finally {
@@ -7613,7 +7675,7 @@ async function applySetup(choice, dryRun) {
7613
7675
  }
7614
7676
  async function write() {
7615
7677
  for (const tool of choice.tools) {
7616
- const hubPath = join19(homedir4(), tool.hubFile);
7678
+ const hubPath = join19(agentHome(), tool.hubFile);
7617
7679
  const written = await injectBlock(
7618
7680
  hubPath,
7619
7681
  hub + tailFor(tool),
@@ -7629,7 +7691,7 @@ async function applySetup(choice, dryRun) {
7629
7691
  if (!dryRun && written.after !== null) mark(hubPath, written.after);
7630
7692
  for (const skill of BRAND.subSkills) {
7631
7693
  const content = await loadAsset(join19(skill, "SKILL.md"));
7632
- const path = join19(homedir4(), tool.skillsDir, skill, "SKILL.md");
7694
+ const path = join19(agentHome(), tool.skillsDir, skill, "SKILL.md");
7633
7695
  const out = await writeIfDifferent(path, content, dryRun);
7634
7696
  results.push({
7635
7697
  tool: tool.label,
@@ -7699,7 +7761,7 @@ async function resolveMode(opts) {
7699
7761
  return { kind: mode ?? "agent", chosen: mode !== void 0 };
7700
7762
  }
7701
7763
  async function detectTools() {
7702
- const home = homedir4();
7764
+ const home = agentHome();
7703
7765
  const found = [];
7704
7766
  for (const tool of TOOLS) {
7705
7767
  if (await exists2(join19(home, tool.dir))) found.push(tool);
@@ -7832,7 +7894,7 @@ async function writeIfDifferent(filePath, content, dryRun) {
7832
7894
  return { outcome: existing === null ? "injected" : "updated", before: existing, after };
7833
7895
  }
7834
7896
  async function installHook(spec, tool, dryRun) {
7835
- const settingsPath = join19(homedir4(), ...(tool.hooksFile ?? "").split("/"));
7897
+ const settingsPath = join19(agentHome(), ...(tool.hooksFile ?? "").split("/"));
7836
7898
  const raw = await readFile19(settingsPath, "utf8").catch(() => null);
7837
7899
  let settings = {};
7838
7900
  if (raw !== null) {
@@ -7882,7 +7944,7 @@ async function installHook(spec, tool, dryRun) {
7882
7944
  return raw === null ? "injected" : "updated";
7883
7945
  }
7884
7946
  async function removeHook(spec, tool, dryRun) {
7885
- const settingsPath = join19(homedir4(), ...(tool.hooksFile ?? "").split("/"));
7947
+ const settingsPath = join19(agentHome(), ...(tool.hooksFile ?? "").split("/"));
7886
7948
  const raw = await readFile19(settingsPath, "utf8").catch(() => null);
7887
7949
  if (raw === null) return "unchanged";
7888
7950
  let parsed;
@@ -8093,7 +8155,7 @@ For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around t
8093
8155
  });
8094
8156
 
8095
8157
  // src/program.ts
8096
- import { Command as Command27 } from "commander";
8158
+ import { Command as Command28 } from "commander";
8097
8159
 
8098
8160
  // src/help.ts
8099
8161
  init_ui();
@@ -8117,7 +8179,7 @@ var COMMAND_GROUPS = [
8117
8179
  {
8118
8180
  title: "ACCOUNT",
8119
8181
  blurb: "optional \u2014 connect a hosted brain; everything above works without it",
8120
- commands: ["login", "logout", "whoami", "push", "pull"]
8182
+ commands: ["login", "logout", "whoami", "brains", "push", "pull"]
8121
8183
  },
8122
8184
  {
8123
8185
  title: "SYSTEM",
@@ -8130,7 +8192,12 @@ var ENV_ROWS = [
8130
8192
  ["MEMGINEERING_HOME=<dir>", "state + derived index (default: ~/.memgineering)"],
8131
8193
  ["MEMGINEERING_JSON=1", "force --json framing without the flag"],
8132
8194
  ["MEMGINEERING_NO_UPDATE=1", "skip the update check for this run"],
8133
- ["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)"]
8134
8201
  ];
8135
8202
  function commandLabel(sub) {
8136
8203
  const args = sub.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
@@ -8283,12 +8350,13 @@ var cloudRules = (brain, args = {}) => call(brain, "POST", "/rules", {
8283
8350
  var cloudReindex = (brain, args = {}) => call(brain, "POST", "/reindex", {
8284
8351
  ...args.rebuild === true ? { rebuild: true } : {}
8285
8352
  });
8286
- async function listCloudBrains(token, baseUrl) {
8353
+ async function listCloudBrains(token, baseUrl, opts = {}) {
8287
8354
  const res = await apiRequest({
8288
8355
  method: "GET",
8289
8356
  path: "/v1/brains",
8290
8357
  token,
8291
- baseUrl
8358
+ baseUrl,
8359
+ ...opts.retries === void 0 ? {} : { retries: opts.retries }
8292
8360
  });
8293
8361
  return res.body?.brains ?? [];
8294
8362
  }
@@ -8360,11 +8428,11 @@ function describeHeldBack(held) {
8360
8428
  return [
8361
8429
  ...heldBackGroups(held).flatMap(
8362
8430
  ({ label, rows }) => rows.map(
8363
- (r) => `${stripControl(r.path)} \u2014 ${label}${r.detail === null ? "" : ` (${stripControl(r.detail)})`}`
8431
+ (r) => `${oneLine(r.path, WIDTH.path)} \u2014 ${label}${r.detail === null ? "" : ` (${oneLine(r.detail, WIDTH.text)})`}`
8364
8432
  )
8365
8433
  ),
8366
8434
  ...held.folders.map(
8367
- (dir) => `${stripControl(dir)}/ \u2014 an excluded folder, whose contents were never walked`
8435
+ (dir) => `${oneLine(dir, WIDTH.path)}/ \u2014 an excluded folder, whose contents were never walked`
8368
8436
  )
8369
8437
  ];
8370
8438
  }
@@ -8384,7 +8452,7 @@ ${chosenCount} note(s) are not in ${wording.where}, because your rules say so:`
8384
8452
  for (const { label, rows } of chosen) {
8385
8453
  if (rows.length === 0) continue;
8386
8454
  printHuman(c.gray(` ${rows.length} \u2014 ${label}:`));
8387
- for (const r of rows.slice(0, 10)) printHuman(c.gray(` ${stripControl(r.path)}`));
8455
+ for (const r of rows.slice(0, 10)) printHuman(c.gray(` ${oneLine(r.path, WIDTH.path)}`));
8388
8456
  if (rows.length > 10) printHuman(c.gray(` \u2026 and ${rows.length - 10} more`));
8389
8457
  }
8390
8458
  if (held.folders.length > 0) {
@@ -8394,7 +8462,8 @@ ${chosenCount} note(s) are not in ${wording.where}, because your rules say so:`
8394
8462
  inside them are not counted above:`
8395
8463
  )
8396
8464
  );
8397
- for (const dir of held.folders.slice(0, 10)) printHuman(c.gray(` ${stripControl(dir)}/`));
8465
+ for (const dir of held.folders.slice(0, 10))
8466
+ printHuman(c.gray(` ${oneLine(dir, WIDTH.path)}/`));
8398
8467
  if (held.folders.length > 10) {
8399
8468
  printHuman(c.gray(` \u2026 and ${held.folders.length - 10} more`));
8400
8469
  }
@@ -8408,7 +8477,11 @@ ${chosenCount} note(s) are not in ${wording.where}, because your rules say so:`
8408
8477
  )
8409
8478
  );
8410
8479
  for (const d of guessed.slice(0, 10)) {
8411
- printHuman(c.gray(` ${stripControl(d.path)} (matched "${stripControl(d.term ?? "?")}")`));
8480
+ printHuman(
8481
+ c.gray(
8482
+ ` ${oneLine(d.path, WIDTH.path)} (matched "${oneLine(d.term ?? "?", WIDTH.title)}")`
8483
+ )
8484
+ );
8412
8485
  }
8413
8486
  if (guessed.length > 10) printHuman(c.gray(` \u2026 and ${guessed.length - 10} more`));
8414
8487
  printHuman(
@@ -8456,13 +8529,13 @@ function isOpaqueId(id) {
8456
8529
  return id.length === OPAQUE_ID_LENGTH && /^[0-9ABCDEFGHJKMNPQRSTVWXYZ]+$/.test(id);
8457
8530
  }
8458
8531
  function displayHandle(id, allIds) {
8459
- if (!isOpaqueId(id)) return stripControl(id);
8532
+ if (!isOpaqueId(id)) return oneLine(id, WIDTH.handle);
8460
8533
  for (let len = MIN_HANDLE_LENGTH; len < id.length; len++) {
8461
8534
  const prefix = id.slice(0, len);
8462
8535
  const matches = allIds.filter((other) => other.startsWith(prefix));
8463
8536
  if (matches.length === 1) return prefix;
8464
8537
  }
8465
- return stripControl(id);
8538
+ return oneLine(id, WIDTH.handle);
8466
8539
  }
8467
8540
  function matchIds(ref, allIds) {
8468
8541
  const exact = allIds.filter((id) => id === ref);
@@ -8689,14 +8762,83 @@ init_api_client();
8689
8762
  init_credentials();
8690
8763
  init_pending_login();
8691
8764
  init_config();
8765
+ init_ui();
8692
8766
  init_vault();
8693
8767
  async function resolveTarget(opts = {}) {
8694
8768
  if (opts.local !== true) {
8695
8769
  const cloud = await cloudTarget();
8696
- 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;
8697
8841
  }
8698
- const { brain, via } = await resolveBrain(opts.vault);
8699
- return { kind: "local", brain, via };
8700
8842
  }
8701
8843
  async function hostedPointer() {
8702
8844
  const base = apiUrl();
@@ -8788,7 +8930,9 @@ function reindexCommand() {
8788
8930
  )
8789
8931
  );
8790
8932
  for (const d of byContent.slice(0, 10)) {
8791
- printHuman(c.gray(` ${stripControl(d.path)} (${stripControl(d.detail)})`));
8933
+ printHuman(
8934
+ c.gray(` ${oneLine(d.path, WIDTH.path)} (${oneLine(d.detail, WIDTH.text)})`)
8935
+ );
8792
8936
  }
8793
8937
  if (byContent.length > 10) {
8794
8938
  printHuman(c.gray(` \u2026 and ${byContent.length - 10} more`));
@@ -8809,7 +8953,7 @@ async function reindexHosted(target, rebuild) {
8809
8953
  // route documents, spelled the way the route spells it.
8810
8954
  json: { brain: { hosted: true, name: target.name }, ...answered },
8811
8955
  human: () => {
8812
- printHuman(`Reindexed ${stripControl(target.name)}.
8956
+ printHuman(`Reindexed ${oneLine(target.name, WIDTH.title)}.
8813
8957
  `);
8814
8958
  if (lexical === void 0) {
8815
8959
  printHuman(c.yellow("\u26A0 this brain\u2019s server did not report the card index"));
@@ -8847,7 +8991,7 @@ async function reindexHosted(target, rebuild) {
8847
8991
  if (answered.usable === false) {
8848
8992
  printHuman(
8849
8993
  c.yellow(
8850
- ` \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)})`
8851
8995
  )
8852
8996
  );
8853
8997
  }
@@ -8866,7 +9010,7 @@ function printFailures(failed, failures) {
8866
9010
  for (const failure of (failures ?? []).slice(0, 3)) {
8867
9011
  printHuman(
8868
9012
  c.gray(
8869
- ` ${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)})`
8870
9014
  )
8871
9015
  );
8872
9016
  }
@@ -8883,18 +9027,39 @@ function nextStep(stopReason) {
8883
9027
  case "unusable_index":
8884
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.";
8885
9029
  default:
8886
- 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.`;
8887
9031
  }
8888
9032
  }
8889
9033
  function unlinkCommand() {
8890
- 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) => {
8891
- 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
+ });
8892
9052
  const cfg = await loadConfig();
8893
9053
  const remaining = cfg.brain.brains.filter((b) => b.root !== brain.root);
8894
9054
  const nextDefault = cfg.brain.default === brain.root ? remaining[0]?.root : cfg.brain.default;
9055
+ const { default: _dropped, ...restOfBrain } = cfg.brain;
8895
9056
  await saveConfig({
8896
9057
  ...cfg,
8897
- brain: { brains: remaining, ...nextDefault ? { default: nextDefault } : {} }
9058
+ brain: {
9059
+ ...restOfBrain,
9060
+ brains: remaining,
9061
+ ...nextDefault === void 0 ? {} : { default: nextDefault }
9062
+ }
8898
9063
  });
8899
9064
  if (opts.purge) {
8900
9065
  await clearCache(cacheDirFor(brain.root, brandHome()));
@@ -8982,6 +9147,54 @@ async function readLines(root, name) {
8982
9147
  const raw = await readFile10(join10(root, name), "utf8").catch(() => "");
8983
9148
  return raw.split("\n").map((l) => l.replace(/\r$/, "").trim()).filter((l) => l !== "" && !l.startsWith("#"));
8984
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
+ }
8985
9198
 
8986
9199
  // src/commands/consolidate.ts
8987
9200
  init_src();
@@ -9279,17 +9492,23 @@ function openCommand() {
9279
9492
  brain: { hosted: true, name: target.name }
9280
9493
  },
9281
9494
  human: () => {
9282
- printHuman(`## ${stripControl(String(found.memory["title"] ?? found.path))}
9283
- `);
9495
+ printHuman(
9496
+ `## ${oneLine(String(found.memory["title"] ?? found.path), WIDTH.title)}
9497
+ `
9498
+ );
9284
9499
  const summary = found.memory["summary"];
9285
9500
  if (typeof summary === "string" && summary !== "") {
9286
- printHuman(`> ${stripControl(summary)}
9501
+ printHuman(`> ${oneLine(summary, WIDTH.text)}
9287
9502
  `);
9288
9503
  }
9289
- 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
+ );
9290
9509
  if (detail !== "card" && found.body !== "") {
9291
- printHuman(`
9292
- ${stripControl(found.body)}`);
9510
+ printHuman("\n");
9511
+ printBlock(found.body);
9293
9512
  }
9294
9513
  }
9295
9514
  });
@@ -9326,7 +9545,7 @@ ${stripControl(found.body)}`);
9326
9545
  ...stage
9327
9546
  },
9328
9547
  human: () => {
9329
- printHuman(`## ${stripControl(entry.memory.title)}
9548
+ printHuman(`## ${oneLine(entry.memory.title, WIDTH.title)}
9330
9549
  `);
9331
9550
  if (scaffolding) {
9332
9551
  printHuman(
@@ -9339,13 +9558,17 @@ ${stripControl(found.body)}`);
9339
9558
  }
9340
9559
  printHuman(
9341
9560
  `
9342
- \`id: ${stripControl(entry.memory.id)}\` \xB7 \`${stripControl(entry.path)}\`
9561
+ \`id: ${oneLine(entry.memory.id, WIDTH.handle)}\` \xB7 \`${oneLine(entry.path, WIDTH.path)}\`
9343
9562
  `
9344
9563
  );
9345
9564
  if (entry.memory.summary) printHuman(quoteBlock(entry.memory.summary) + "\n");
9346
9565
  for (const claim of claims) {
9347
- printHuman(`- ${stripControl(claim.text)}`);
9348
- 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
+ );
9349
9572
  }
9350
9573
  if (claims.length === 0 && scaffolding) {
9351
9574
  printHuman(
@@ -9376,13 +9599,14 @@ makes the two surface together as a conflict.`
9376
9599
  }
9377
9600
  if (stage.outline && stage.outline.length > 0) {
9378
9601
  printHuman("\n### sections\n");
9379
- for (const heading of stage.outline) printHuman(`- ${stripControl(heading)}`);
9602
+ for (const heading of stage.outline)
9603
+ printHuman(`- ${oneLine(heading, WIDTH.title)}`);
9380
9604
  printHuman("");
9381
9605
  }
9382
9606
  if (stage.sections) {
9383
9607
  for (const section of stage.sections) {
9384
9608
  printHuman(`
9385
- ### ${stripControl(section.label)}
9609
+ ### ${oneLine(section.label, WIDTH.title)}
9386
9610
  `);
9387
9611
  printHuman(quoteBlock(section.body));
9388
9612
  }
@@ -9942,11 +10166,11 @@ To see what is there now: \`memgineering open ` + ref + "`"
9942
10166
  after: preview.after
9943
10167
  },
9944
10168
  human: () => {
9945
- printHuman(`## revise (dry run): ${stripControl(entry.memory.title)}
10169
+ printHuman(`## revise (dry run): ${oneLine(entry.memory.title, WIDTH.title)}
9946
10170
  `);
9947
- printHuman(`\`${stripControl(entry.path)}\`
10171
+ printHuman(`\`${oneLine(entry.path, WIDTH.path)}\`
9948
10172
  `);
9949
- printHuman(stripControl(renderDiff(preview.before, preview.after)));
10173
+ printBlock(renderDiff(preview.before, preview.after));
9950
10174
  printHuman(c.gray("\nNothing was written. Drop --dry-run to apply."));
9951
10175
  }
9952
10176
  });
@@ -10006,10 +10230,10 @@ To see what is there now: \`memgineering open ` + ref + "`"
10006
10230
  human: () => {
10007
10231
  if (proposal.action === "conflict") {
10008
10232
  printHuman(
10009
- `Recorded a disagreement about **${stripControl(entry.memory.title)}**.
10233
+ `Recorded a disagreement about **${oneLine(entry.memory.title, WIDTH.title)}**.
10010
10234
  `
10011
10235
  );
10012
- printHuman(`\`${stripControl(entry.path)}\`
10236
+ printHuman(`\`${oneLine(entry.path, WIDTH.path)}\`
10013
10237
  `);
10014
10238
  printHuman(
10015
10239
  c.gray(
@@ -10017,12 +10241,12 @@ To see what is there now: \`memgineering open ` + ref + "`"
10017
10241
  )
10018
10242
  );
10019
10243
  } else {
10020
- printHuman(`Revised **${stripControl(entry.memory.title)}**.
10244
+ printHuman(`Revised **${oneLine(entry.memory.title, WIDTH.title)}**.
10021
10245
  `);
10022
- printHuman(`\`${stripControl(entry.path)}\`
10246
+ printHuman(`\`${oneLine(entry.path, WIDTH.path)}\`
10023
10247
  `);
10024
10248
  }
10025
- printHuman(stripControl(renderDiff(preview.before, preview.after)));
10249
+ printBlock(renderDiff(preview.before, preview.after));
10026
10250
  if (drift) {
10027
10251
  printHuman(
10028
10252
  c.yellow(`
@@ -10089,18 +10313,18 @@ async function reviseHosted(where, ref, opts) {
10089
10313
  json: { brain: where.name, ...result },
10090
10314
  human: () => {
10091
10315
  if (result.dry_run) {
10092
- printHuman(`## revise (dry run) \u2014 ${stripControl(where.name)}
10316
+ printHuman(`## revise (dry run) \u2014 ${oneLine(where.name, WIDTH.title)}
10093
10317
  `);
10094
- printHuman(`\`${stripControl(result.target)}\`
10318
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
10095
10319
  `);
10096
- printHuman(stripControl(renderDiff(result.before, result.after)));
10320
+ printBlock(renderDiff(result.before, result.after));
10097
10321
  printHuman(c.gray("\nNothing was written. Drop --dry-run to apply."));
10098
10322
  return;
10099
10323
  }
10100
10324
  if (opts.action === "conflict") {
10101
- printHuman(`Recorded a disagreement in **${stripControl(where.name)}**.
10325
+ printHuman(`Recorded a disagreement in **${oneLine(where.name, WIDTH.title)}**.
10102
10326
  `);
10103
- printHuman(`\`${stripControl(result.target)}\`
10327
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
10104
10328
  `);
10105
10329
  printHuman(
10106
10330
  c.gray(
@@ -10108,16 +10332,16 @@ async function reviseHosted(where, ref, opts) {
10108
10332
  )
10109
10333
  );
10110
10334
  } else {
10111
- printHuman(`Revised in **${stripControl(where.name)}**.
10335
+ printHuman(`Revised in **${oneLine(where.name, WIDTH.title)}**.
10112
10336
  `);
10113
- printHuman(`\`${stripControl(result.target)}\`
10337
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
10114
10338
  `);
10115
10339
  }
10116
- printHuman(stripControl(renderDiff(result.before, result.after)));
10340
+ printBlock(renderDiff(result.before, result.after));
10117
10341
  if (result.body_drift) {
10118
10342
  printHuman(
10119
10343
  c.yellow(`
10120
- \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(
10121
10345
  "\n \u2014 the frontmatter now concludes differently. The prose is yours and was not\n touched; edit it yourself if it should agree."
10122
10346
  )
10123
10347
  );
@@ -10141,7 +10365,7 @@ To put it back: \`memgineering revise ${ref} --claim ${JSON.stringify(
10141
10365
  );
10142
10366
  } else if (result.reverse?.hint) {
10143
10367
  printHuman(c.gray(`
10144
- ${stripControl(result.reverse.hint)}`));
10368
+ ${oneLine(result.reverse.hint, WIDTH.text)}`));
10145
10369
  }
10146
10370
  }
10147
10371
  });
@@ -10185,7 +10409,7 @@ function nothingToChange(path, ref, verb = "revise") {
10185
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.`;
10186
10410
  return memgError(
10187
10411
  "nothing_to_change",
10188
- `${stripControl(path)} already says exactly this`,
10412
+ `${oneLine(path, WIDTH.path)} already says exactly this`,
10189
10413
  `Nothing was written. ${way}
10190
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.
10191
10415
  To see what the note says now: \`memgineering open ${ref}\``
@@ -10293,20 +10517,22 @@ function renderSummary(p) {
10293
10517
  const claim = p.claim === void 0 || p.claim === null ? null : oneLine(p.claim, CLAIM_WIDTH);
10294
10518
  const unavailable = p.claim === void 0 && !claimless;
10295
10519
  const lines2 = [
10296
- `- **${stripControl(p.action)}**${p.proposed_kind ? ` \u2192 ${p.proposed_kind}` : ""} \`${stripControl(p.target)}\``,
10520
+ `- **${oneLine(p.action, WIDTH.title)}**${p.proposed_kind ? ` \u2192 ${p.proposed_kind}` : ""} \`${oneLine(p.target, WIDTH.path)}\``,
10297
10521
  ` ${p.id} ${p.state}`
10298
10522
  ];
10299
10523
  if (claim !== null && claim !== "") lines2.push(` \u2192 ${claim}`);
10300
10524
  else if (unavailable) {
10301
10525
  lines2.push(
10302
10526
  c.gray(
10303
- ` \u2192 (claim unavailable: this brain's server is older than the CLI. \`memgineering open ${stripControl(p.memory_id ?? p.target)}\` shows what the note says NOW; upgrading the server restores the claim.)`
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.)`
10304
10528
  )
10305
10529
  );
10306
10530
  }
10307
10531
  lines2.push(quoteBlock(p.reason, " > "));
10308
10532
  if (p.supersedes.length > 0) {
10309
- 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
+ );
10310
10536
  }
10311
10537
  return lines2.join("\n");
10312
10538
  }
@@ -10422,10 +10648,10 @@ function renderApproval(applied, vault) {
10422
10648
  },
10423
10649
  human: () => {
10424
10650
  printHuman(
10425
- `Applied **${stripControl(applied.action)}** to \`${stripControl(applied.target)}\`.
10651
+ `Applied **${oneLine(applied.action, WIDTH.title)}** to \`${oneLine(applied.target, WIDTH.path)}\`.
10426
10652
  `
10427
10653
  );
10428
- printHuman(stripControl(renderDiff(applied.before, applied.after)));
10654
+ printBlock(renderDiff(applied.before, applied.after));
10429
10655
  printHuman(c.gray(`
10430
10656
  Undo: \`${undoHint(applied.opId, vault)}\``));
10431
10657
  }
@@ -10435,7 +10661,7 @@ function renderRejection(id, state, target) {
10435
10661
  printDual({
10436
10662
  json: { proposal_id: id, state, target },
10437
10663
  human: () => {
10438
- printHuman(`Declined \`${stripControl(target)}\`.
10664
+ printHuman(`Declined \`${oneLine(target, WIDTH.path)}\`.
10439
10665
  `);
10440
10666
  printHuman(c.gray("The note was not touched."));
10441
10667
  }
@@ -10493,9 +10719,9 @@ function renderRebase(id, state, target, before, after) {
10493
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.`
10494
10720
  },
10495
10721
  human: () => {
10496
- printHuman(`Recomputed against \`${stripControl(target)}\`.
10722
+ printHuman(`Recomputed against \`${oneLine(target, WIDTH.path)}\`.
10497
10723
  `);
10498
- printHuman(stripControl(renderDiff(before, after)));
10724
+ printBlock(renderDiff(before, after));
10499
10725
  printHuman(
10500
10726
  c.gray(
10501
10727
  `
@@ -10656,16 +10882,16 @@ var TITLE_WIDTH = 72;
10656
10882
  function noteLines(id, title, path) {
10657
10883
  const shown = oneLine(title, TITLE_WIDTH);
10658
10884
  return [
10659
- shown === "" ? ` ${stripControl(id)}` : ` ${stripControl(id)} ${shown}`,
10885
+ shown === "" ? ` ${oneLine(id, WIDTH.handle)}` : ` ${oneLine(id, WIDTH.handle)} ${shown}`,
10660
10886
  // Backticked, as it was before the title moved onto the row above it. This
10661
10887
  // output is read by agents as markdown, and a path outside code formatting
10662
10888
  // is a path they have to guess the extent of.
10663
- c.gray(` \`${stripControl(path)}\``)
10889
+ c.gray(` \`${oneLine(path, WIDTH.path)}\``)
10664
10890
  ];
10665
10891
  }
10666
10892
  function renderCandidate(candidate) {
10667
10893
  const lines2 = [
10668
- `- **${stripControl(candidate.rule)}** \u2014 ${stripControl(candidate.evidence)}`,
10894
+ `- **${oneLine(candidate.rule, WIDTH.title)}** \u2014 ${oneLine(candidate.evidence, WIDTH.text)}`,
10669
10895
  ...noteLines(candidate.memory_id, candidate.title, candidate.path)
10670
10896
  ];
10671
10897
  if (candidate.other_id !== null) {
@@ -10707,7 +10933,7 @@ function proposeCommand() {
10707
10933
  },
10708
10934
  human: () => {
10709
10935
  printHuman(
10710
- `Proposed **${stripControl(filed.action)}** on \`${stripControl(filed.target)}\`.
10936
+ `Proposed **${oneLine(filed.action, WIDTH.title)}** on \`${oneLine(filed.target, WIDTH.path)}\`.
10711
10937
  `
10712
10938
  );
10713
10939
  printHuman(c.gray(` ${filed.id}`));
@@ -11142,14 +11368,14 @@ function lifecycleCommand(action) {
11142
11368
  json: { action, memory_id: entry.memory.id, target: entry.path, op_id: record.op_id },
11143
11369
  human: () => {
11144
11370
  printHuman(
11145
- action === "retire" ? `Retired **${stripControl(entry.memory.title)}**.
11146
- ` : `**${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.
11147
11373
  `
11148
11374
  );
11149
11375
  if (action === "retire") {
11150
11376
  printHuman(
11151
11377
  c.gray(
11152
- "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) + "`"
11153
11379
  )
11154
11380
  );
11155
11381
  }
@@ -11207,8 +11433,10 @@ function ignoreCommand(mode) {
11207
11433
  mode === "exclude" ? (
11208
11434
  // The path is argv here, but argv came from a listing of the
11209
11435
  // user's own notes — and a filename is as authored as a title.
11210
- c.gray(`\`${stripControl(target)}\` was already excluded \u2014 nothing changed.`)
11211
- ) : 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.`)
11212
11440
  );
11213
11441
  return;
11214
11442
  }
@@ -11238,13 +11466,13 @@ async function lifecycleHosted(where, action, ref, reason) {
11238
11466
  json: { brain: where.name, ...result },
11239
11467
  human: () => {
11240
11468
  printHuman(
11241
- action === "retire" ? `Retired in **${stripControl(where.name)}**.
11242
- ` : `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)}**.
11243
11471
  `
11244
11472
  );
11245
- printHuman(`\`${stripControl(result.target)}\`
11473
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
11246
11474
  `);
11247
- printHuman(c.gray(stripControl(result.note)));
11475
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
11248
11476
  printHuman(
11249
11477
  c.gray(
11250
11478
  `
@@ -11264,15 +11492,15 @@ async function ignoreHosted(where, mode, path, reason) {
11264
11492
  json: { brain: where.name, ...result },
11265
11493
  human: () => {
11266
11494
  if (!result.changed) {
11267
- printHuman(c.gray(stripControl(result.note)));
11495
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
11268
11496
  return;
11269
11497
  }
11270
11498
  printHuman(
11271
- mode === "exclude" ? `No longer reading \`${stripControl(result.path)}\` in **${stripControl(where.name)}**.
11272
- ` : `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)}**.
11273
11501
  `
11274
11502
  );
11275
- printHuman(c.gray(stripControl(result.note)));
11503
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
11276
11504
  if (result.op_id) printHuman(c.gray(`
11277
11505
  Undo: \`memgineering ${opposite(mode)} ${path}\``));
11278
11506
  }
@@ -11336,10 +11564,12 @@ function evidenceCommand() {
11336
11564
  });
11337
11565
  }
11338
11566
  function renderEvidence(report, opts) {
11339
- printHuman(`## evidence: ${stripControl(report.title)}
11340
- `);
11341
- printHuman(`\`id: ${stripControl(report.id)}\` \xB7 \`${stripControl(report.path)}\`
11567
+ printHuman(`## evidence: ${oneLine(report.title, WIDTH.title)}
11342
11568
  `);
11569
+ printHuman(
11570
+ `\`id: ${oneLine(report.id, WIDTH.handle)}\` \xB7 \`${oneLine(report.path, WIDTH.path)}\`
11571
+ `
11572
+ );
11343
11573
  if (report.critical) {
11344
11574
  printHuman(c.yellow("\u26A0 critical \u2014 this note defines how the agent behaves.") + "\n");
11345
11575
  }
@@ -11389,11 +11619,12 @@ function renderEvidence(report, opts) {
11389
11619
  ]);
11390
11620
  }
11391
11621
  const pad = rows.reduce((w, [label]) => Math.max(w, label.length), 0) + 2;
11392
- 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)}`);
11393
11624
  if (report.standing) {
11394
11625
  const means = STANDING_MEANS[report.standing];
11395
11626
  printHuman(`
11396
- **${stripControl(report.standing)}**${means ? ` \u2014 ${means}` : ""}`);
11627
+ **${oneLine(report.standing, WIDTH.title)}**${means ? ` \u2014 ${means}` : ""}`);
11397
11628
  }
11398
11629
  if (report.contradicts > 0) {
11399
11630
  printHuman(
@@ -11414,7 +11645,7 @@ function renderEvidence(report, opts) {
11414
11645
  report.last_reason.verb,
11415
11646
  report.last_reason.ts === null ? null : report.last_reason.ts.slice(0, 10)
11416
11647
  ].filter((part) => part !== null && part !== "").join(", ");
11417
- if (provenance !== "") printHuman(c.gray(` ${stripControl(provenance)}`));
11648
+ if (provenance !== "") printHuman(c.gray(` ${oneLine(provenance, WIDTH.text)}`));
11418
11649
  } else if (report.changed > 0) {
11419
11650
  printHuman(
11420
11651
  c.gray(
@@ -11550,6 +11781,7 @@ import { Command as Command11 } from "commander";
11550
11781
  // src/lib/rules.ts
11551
11782
  init_src();
11552
11783
  init_index_build();
11784
+ init_ui();
11553
11785
  init_vault();
11554
11786
  var RULE_LIMIT = 8;
11555
11787
  async function collectRules(vault, contextDir, now = /* @__PURE__ */ new Date()) {
@@ -11578,11 +11810,12 @@ async function collectRules(vault, contextDir, now = /* @__PURE__ */ new Date())
11578
11810
  };
11579
11811
  }
11580
11812
  function renderRuleLines(rules) {
11581
- return rules.map(({ entry }) => `- ${entry.memory.summary ?? entry.memory.title}`);
11813
+ return rules.map(({ entry }) => `- ${oneLine(entry.memory.summary ?? entry.memory.title, 200)}`);
11582
11814
  }
11583
11815
 
11584
11816
  // src/commands/guard.ts
11585
11817
  init_deadline();
11818
+ init_ui();
11586
11819
  function guardCommand() {
11587
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) => {
11588
11821
  try {
@@ -11622,7 +11855,7 @@ async function runGuard(payload, contextDirFlag) {
11622
11855
  return JSON.stringify({
11623
11856
  hookSpecificOutput: {
11624
11857
  hookEventName: "PreToolUse",
11625
- additionalContext: contextFor(claimed.rules, claimed.total)
11858
+ additionalContext: contextFor(claimed.lines, claimed.total)
11626
11859
  }
11627
11860
  });
11628
11861
  }
@@ -11636,16 +11869,22 @@ async function runStopGuard(payload, contextDirFlag) {
11636
11869
  return JSON.stringify({
11637
11870
  hookSpecificOutput: {
11638
11871
  hookEventName: "Stop",
11639
- additionalContext: stopContextFor(claimed.rules, claimed.total)
11872
+ additionalContext: stopContextFor(claimed.lines, claimed.total)
11640
11873
  }
11641
11874
  });
11642
11875
  }
11643
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
+ }
11644
11883
  const collected = await collectRules(void 0, contextDir).catch(() => null);
11645
11884
  if (collected === null) return null;
11646
11885
  const { brainRoot, rules, total } = collected;
11647
11886
  if (rules.length === 0) return null;
11648
- if (!await claimSession(sessionId)) return null;
11887
+ if (!await claimSession(sessionId, brainRoot)) return null;
11649
11888
  await recordEvent(brainRoot, {
11650
11889
  verb: "guard",
11651
11890
  contextDir,
@@ -11653,22 +11892,20 @@ async function claimRules(sessionId, contextDir) {
11653
11892
  limit: RULE_LIMIT,
11654
11893
  returnedIds: rules.map((r) => r.entry.memory.id)
11655
11894
  });
11656
- return { rules, total };
11895
+ return { lines: renderRuleLines(rules), total };
11657
11896
  }
11658
- function contextFor(rules, total) {
11659
- const lines2 = renderRuleLines(rules);
11660
- const more = total > rules.length ? `
11661
- (${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.)` : "";
11662
11900
  return `Decisions this user has already made. They are theirs, not suggestions, and they hold whether or not this task mentions them:
11663
11901
 
11664
11902
  ${lines2.join("\n")}${more}
11665
11903
 
11666
11904
  Follow them. If what you are about to do goes against one, say so in a sentence instead of doing it quietly.`;
11667
11905
  }
11668
- function stopContextFor(rules, total) {
11669
- const lines2 = renderRuleLines(rules);
11670
- const more = total > rules.length ? `
11671
- (${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.)` : "";
11672
11909
  return `Decisions this user has already made. They are theirs, not suggestions, and they hold whether or not this task mentions them:
11673
11910
 
11674
11911
  ${lines2.join("\n")}${more}
@@ -11699,10 +11936,10 @@ async function readPayload() {
11699
11936
  }
11700
11937
  }
11701
11938
  var guardDir = () => join16(brandHome(), "guard");
11702
- async function claimSession(sessionId) {
11939
+ async function claimSession(sessionId, brain) {
11703
11940
  const marker = join16(
11704
11941
  guardDir(),
11705
- `${createHash7("sha256").update(sessionId).digest("hex").slice(0, 32)}`
11942
+ `${createHash7("sha256").update(`${sessionId}\0${brain}`).digest("hex").slice(0, 32)}`
11706
11943
  );
11707
11944
  try {
11708
11945
  await mkdir11(guardDir(), { recursive: true, mode: 448 });
@@ -11727,6 +11964,17 @@ async function pruneOldMarkers() {
11727
11964
  } catch {
11728
11965
  }
11729
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
+ }
11730
11978
 
11731
11979
  // src/program.ts
11732
11980
  init_init();
@@ -11769,7 +12017,7 @@ function logCommand() {
11769
12017
  human: () => {
11770
12018
  const diverged2 = Array.isArray(history["divergence"]) ? history["divergence"] : [];
11771
12019
  const total = typeof history["total"] === "number" ? history["total"] : rows.length;
11772
- printHuman(`## log \u2014 ${stripControl(target.name)} (${total} operations)
12020
+ printHuman(`## log \u2014 ${oneLine(target.name, WIDTH.title)} (${total} operations)
11773
12021
  `);
11774
12022
  if (rows.length === 0) {
11775
12023
  printHuman("_This brain has no recorded operations._\n");
@@ -11783,7 +12031,8 @@ function logCommand() {
11783
12031
  );
11784
12032
  }
11785
12033
  const hint = history["hint"];
11786
- if (typeof hint === "string" && hint !== "") printHuman(c.gray(stripControl(hint)));
12034
+ if (typeof hint === "string" && hint !== "")
12035
+ printHuman(c.gray(oneLine(hint, WIDTH.text)));
11787
12036
  }
11788
12037
  });
11789
12038
  return;
@@ -11905,15 +12154,19 @@ function renderOps(ops, opts) {
11905
12154
  op.diverged && !op.undone ? "note has changed since" : null
11906
12155
  ].filter(Boolean);
11907
12156
  printHuman(
11908
- `- ${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(", ")})`) : ""}`
11909
12158
  );
11910
12159
  printHuman(
11911
- 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)}`
11912
12163
  );
11913
12164
  if (op.rationale) printHuman(c.gray(quoteBlock(op.rationale, " > ")));
11914
12165
  if (opts.verbose) {
11915
12166
  printHuman(
11916
- 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
+ )
11917
12170
  );
11918
12171
  }
11919
12172
  }
@@ -11969,7 +12222,7 @@ init_pending_login();
11969
12222
  init_ui();
11970
12223
  import { Command as Command14 } from "commander";
11971
12224
  function logoutCommand() {
11972
- 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(
11973
12226
  "--local",
11974
12227
  "forget the token here without telling the server \u2014 only for a server you cannot reach"
11975
12228
  ).action(async (opts) => {
@@ -12052,6 +12305,11 @@ function logoutCommand() {
12052
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."
12053
12306
  )
12054
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
+ );
12055
12313
  }
12056
12314
  });
12057
12315
  });
@@ -12135,12 +12393,12 @@ init_ui();
12135
12393
  function pullCommand() {
12136
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) => {
12137
12395
  const { brain: source, others } = await resolveSource(opts.brain);
12138
- const target = resolve8(destination ?? `./${stripControl(source.name)}`);
12396
+ const target = resolve8(destination ?? `./${oneLine(source.name, WIDTH.title)}`);
12139
12397
  const first = await cloudListNotes(source, { limit: 1 });
12140
12398
  if (first.total === 0) {
12141
12399
  throw memgError(
12142
12400
  "invalid_input",
12143
- `\u201C${stripControl(source.name)}\u201D has no notes to download`,
12401
+ `\u201C${oneLine(source.name, WIDTH.title)}\u201D has no notes to download`,
12144
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."
12145
12403
  );
12146
12404
  }
@@ -12161,13 +12419,13 @@ function pullCommand() {
12161
12419
  printHuman(`## pull (dry run)
12162
12420
  `);
12163
12421
  printHuman(
12164
- `${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}\`
12165
12423
  `
12166
12424
  );
12167
12425
  for (const note of preview.slice(0, 20)) {
12168
12426
  printHuman(
12169
12427
  c.gray(
12170
- ` ${stripControl(note.path)}${note.excluded === true ? " (excluded)" : ""}`
12428
+ ` ${oneLine(note.path, WIDTH.path)}${note.excluded === true ? " (excluded)" : ""}`
12171
12429
  )
12172
12430
  );
12173
12431
  }
@@ -12182,7 +12440,7 @@ function pullCommand() {
12182
12440
  });
12183
12441
  return;
12184
12442
  }
12185
- 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`);
12186
12444
  const written = [];
12187
12445
  const already = [];
12188
12446
  const excluded = [];
@@ -12224,7 +12482,7 @@ function pullCommand() {
12224
12482
  human: () => {
12225
12483
  printHuman(
12226
12484
  c.green(
12227
- `\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}\``
12228
12486
  )
12229
12487
  );
12230
12488
  if (already.length > 0) {
@@ -12276,8 +12534,8 @@ async function resolveSource(named) {
12276
12534
  if (!found) {
12277
12535
  throw memgError(
12278
12536
  "invalid_input",
12279
- named ? `no hosted brain called \u201C${stripControl(named)}\u201D` : "more than one hosted brain \u2014 say which",
12280
- `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)}\`)`
12281
12539
  );
12282
12540
  }
12283
12541
  return {
@@ -12294,7 +12552,7 @@ async function resolveSource(named) {
12294
12552
  }
12295
12553
  function sayWhatIsElsewhere(others, into) {
12296
12554
  if (others.length === 0) return;
12297
- const names = others.map(stripControl);
12555
+ const names = others.map((name) => oneLine(name, WIDTH.title));
12298
12556
  printHuman(
12299
12557
  c.yellow(
12300
12558
  `
@@ -12392,7 +12650,7 @@ If your notes are in one of those, that rule is why \u2014 nothing here can see
12392
12650
  return;
12393
12651
  }
12394
12652
  const destination = await resolveDestination(opts.brain, local.root);
12395
- 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`);
12396
12654
  const uploaded = [];
12397
12655
  const already = [];
12398
12656
  const refused = [];
@@ -12462,7 +12720,9 @@ If your notes are in one of those, that rule is why \u2014 nothing here can see
12462
12720
  },
12463
12721
  human: () => {
12464
12722
  printHuman(
12465
- 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
+ )
12466
12726
  );
12467
12727
  if (already.length > 0) {
12468
12728
  printHuman(c.gray(` ${already.length} were already there \u2014 left as they are`));
@@ -12480,7 +12740,9 @@ If your notes are in one of those, that rule is why \u2014 nothing here can see
12480
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")
12481
12741
  );
12482
12742
  for (const r of refused.slice(0, 10)) {
12483
- 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
+ );
12484
12746
  }
12485
12747
  if (refused.length > 10) {
12486
12748
  printHuman(c.gray(` \u2026 and ${refused.length - 10} more`));
@@ -12502,14 +12764,14 @@ Nothing was deleted from \`${local.root}\` \u2014
12502
12764
  function printRepoint(repoint, destination) {
12503
12765
  if (repoint === null) {
12504
12766
  printHuman(c.gray(`
12505
- This machine still reads \u201C${stripControl(destination.name)}\u201D.`));
12767
+ This machine still reads \u201C${oneLine(destination.name, WIDTH.title)}\u201D.`));
12506
12768
  return;
12507
12769
  }
12508
12770
  if (repoint.from === null) {
12509
12771
  printHuman(
12510
12772
  c.gray(
12511
12773
  `
12512
- 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
12513
12775
  brain before. \`memgineering recall --local\` still reads the folder.`
12514
12776
  )
12515
12777
  );
@@ -12518,9 +12780,9 @@ brain before. \`memgineering recall --local\` still reads the folder.`
12518
12780
  printHuman(
12519
12781
  c.yellow(
12520
12782
  `
12521
- \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.`
12522
12784
  ) + c.gray(
12523
- "\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."
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) + "`."
12524
12786
  )
12525
12787
  );
12526
12788
  }
@@ -12727,7 +12989,7 @@ function recallCommand() {
12727
12989
  if (semantic.state !== "skipped" || semantic.hint === void 0) return;
12728
12990
  printHuman(
12729
12991
  c.yellow("\u26A0 Meaning was not searched \u2014 this answer matched words only.") + c.gray(`
12730
- ${stripControl(semantic.hint)}
12992
+ ${oneLine(semantic.hint, WIDTH.text)}
12731
12993
  `)
12732
12994
  );
12733
12995
  };
@@ -12775,13 +13037,11 @@ function recallCommand() {
12775
13037
  for (const id of conflict.between) {
12776
13038
  const reason = reasons.get(id);
12777
13039
  if (!reason) {
12778
- printHuman(c.gray(` ${stripControl(id)} \u2014 no reason was recorded.`));
13040
+ printHuman(c.gray(` ${oneLine(id, WIDTH.handle)} \u2014 no reason was recorded.`));
12779
13041
  continue;
12780
13042
  }
12781
13043
  printHuman(
12782
- stripControl(
12783
- ` ${id} \u2014 ${reason.verb === "remember" ? "why it was recorded" : "why it last changed"} (${reason.ts.slice(0, 10)}):`
12784
- )
13044
+ ` ${oneLine(id, WIDTH.handle)} \u2014 ${reason.verb === "remember" ? "why it was recorded" : "why it last changed"} (${reason.ts.slice(0, 10)}):`
12785
13045
  );
12786
13046
  printHuman(quoteBlock(reason.text, " > "));
12787
13047
  }
@@ -12797,8 +13057,10 @@ function recallCommand() {
12797
13057
  );
12798
13058
  }
12799
13059
  if (result.related.length > 0) {
12800
- printHuman(`_related:_ ${result.related.map(stripControl).join(", ")}
12801
- `);
13060
+ printHuman(
13061
+ `_related:_ ${result.related.map((id) => oneLine(id, WIDTH.handle)).join(", ")}
13062
+ `
13063
+ );
12802
13064
  }
12803
13065
  if (result.truncated) {
12804
13066
  const { withheld, estimated_tokens, budget_tokens } = result.truncated;
@@ -12852,9 +13114,8 @@ function asksInSentences(query) {
12852
13114
  return query.trim().split(/\s+/).filter(Boolean).length >= 4;
12853
13115
  }
12854
13116
  function side(id, status, validFrom2) {
12855
- return stripControl(
12856
- `${id} (${status}${validFrom2 === null ? "" : ` \xB7 ${validFrom2.slice(0, 10)}`})`
12857
- );
13117
+ const date = validFrom2 === null ? "" : ` \xB7 ${oneLine(validFrom2.slice(0, 10), WIDTH.handle)}`;
13118
+ return `${oneLine(id, WIDTH.handle)} (${oneLine(status, WIDTH.title)}${date})`;
12858
13119
  }
12859
13120
  async function conflictReasons(vaultRoot, conflicts, byId) {
12860
13121
  const { ops } = await readOps(vaultRoot);
@@ -12873,7 +13134,7 @@ function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffold
12873
13134
  const out = [];
12874
13135
  const standing = card.freshness === "current" ? "" : ` \xB7 ${card.freshness}`;
12875
13136
  const age = Number.isFinite(card.age_days) ? ` \xB7 ${ago2(Math.floor(card.age_days))}` : "";
12876
- out.push(`### ${n}. ${stripControl(card.title)}`);
13137
+ out.push(`### ${n}. ${oneLine(card.title, WIDTH.title)}`);
12877
13138
  if (scaffolding) {
12878
13139
  out.push(c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote"));
12879
13140
  } else {
@@ -12881,14 +13142,14 @@ function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffold
12881
13142
  card.binding ? c.bold("a standing decision") : null,
12882
13143
  `${card.confidence}${standing}${age}`
12883
13144
  ].filter(Boolean);
12884
- out.push(stripControl(marks.join(c.gray(" \xB7 "))));
13145
+ out.push(oneLine(marks.join(c.gray(" \xB7 ")), WIDTH.text));
12885
13146
  }
12886
13147
  if (glossHere && !scaffolding) {
12887
13148
  out.push(c.gray("stated = these fields came from the note, not from inference"));
12888
13149
  }
12889
13150
  if (detail !== "title") {
12890
13151
  if (card.summary) out.push("", quoteBlock(card.summary));
12891
- out.push("", `\`open: ${stripControl(handle2)}\``);
13152
+ out.push("", `\`open: ${oneLine(handle2, WIDTH.handle)}\``);
12892
13153
  }
12893
13154
  if (stage.outline && stage.outline.length > 0) {
12894
13155
  out.push("", ...stage.outline.map((h) => `- ${h}`));
@@ -13019,10 +13280,10 @@ function rememberCommand() {
13019
13280
  ...kindLanded ? {} : { kind_dropped: kind }
13020
13281
  },
13021
13282
  human: () => {
13022
- printHuman(`Remembered in ${stripControl(target.name)}.
13283
+ printHuman(`Remembered in ${oneLine(target.name, WIDTH.title)}.
13023
13284
  `);
13024
13285
  printHuman(
13025
- `note: \`${stripControl(written.id)}\` \xB7 \`${stripControl(written.path)}\``
13286
+ `note: \`${oneLine(written.id, WIDTH.handle)}\` \xB7 \`${oneLine(written.path, WIDTH.path)}\``
13026
13287
  );
13027
13288
  if (opts.rule === true) {
13028
13289
  printHuman("rule: shown before you write a file here, once a session");
@@ -13034,7 +13295,9 @@ function rememberCommand() {
13034
13295
  )
13035
13296
  );
13036
13297
  }
13037
- 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
+ );
13038
13301
  }
13039
13302
  });
13040
13303
  return;
@@ -13235,9 +13498,7 @@ function resurfaceCommand() {
13235
13498
  `);
13236
13499
  if (unreachableDefault) {
13237
13500
  printHuman(
13238
- c.yellow(
13239
- `Your default brain is not on disk right now: ${unreachableDefault}`
13240
- )
13501
+ c.yellow(`Your default brain is not on disk right now: ${unreachableDefault}`)
13241
13502
  );
13242
13503
  printHuman(
13243
13504
  c.gray(
@@ -13251,7 +13512,7 @@ function resurfaceCommand() {
13251
13512
  );
13252
13513
  for (const [i, { entry, why }] of rankedShown.entries()) {
13253
13514
  const blank = untouched.has(entry.path);
13254
- printHuman(`### ${i + 1}. ${stripControl(entry.memory.title)}`);
13515
+ printHuman(`### ${i + 1}. ${oneLine(entry.memory.title, WIDTH.title)}`);
13255
13516
  printHuman(blank ? c.yellow("not filled in yet") : why.join(" \xB7 "));
13256
13517
  if (blank) {
13257
13518
  const hasWritten = rankedShown.some((r) => !untouched.has(r.entry.path));
@@ -13293,31 +13554,31 @@ async function resurfaceHosted(where, limit) {
13293
13554
  },
13294
13555
  human: () => {
13295
13556
  if (result.cards.length === 0) {
13296
- printHuman(`## resurface \u2014 ${stripControl(where.name)}
13557
+ printHuman(`## resurface \u2014 ${oneLine(where.name, WIDTH.title)}
13297
13558
  `);
13298
13559
  printHuman("_Nothing to bring up yet._\n");
13299
- if (result.hint) printHuman(c.gray(stripControl(result.hint)));
13560
+ if (result.hint) printHuman(c.gray(oneLine(result.hint, WIDTH.text)));
13300
13561
  return;
13301
13562
  }
13302
- printHuman(`## resurface \u2014 ${stripControl(where.name)} (${result.cards.length} cards)
13303
- `);
13563
+ printHuman(
13564
+ `## resurface \u2014 ${oneLine(where.name, WIDTH.title)} (${result.cards.length} cards)
13565
+ `
13566
+ );
13304
13567
  for (const [i, card] of result.cards.entries()) {
13305
- printHuman(`### ${i + 1}. ${stripControl(card.title)}`);
13568
+ printHuman(`### ${i + 1}. ${oneLine(card.title, WIDTH.title)}`);
13306
13569
  if (card.untouched === true) printHuman(c.yellow("not filled in yet"));
13307
- 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));
13308
13571
  if (card.summary) printHuman(`
13309
13572
  ${quoteBlock(card.summary)}`);
13310
13573
  printHuman(`
13311
- \`open: ${stripControl(card.handle)}\`
13574
+ \`open: ${oneLine(card.handle, WIDTH.handle)}\`
13312
13575
  `);
13313
13576
  }
13314
- printHuman(
13577
+ printBlock(
13315
13578
  c.gray(
13316
- stripControl(
13317
- `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
13318
13580
  question was asked from, so "in this folder" is not one of them \u2014 that signal
13319
13581
  exists only for a brain on this machine.`
13320
- )
13321
13582
  )
13322
13583
  );
13323
13584
  }
@@ -13338,7 +13599,7 @@ function rulesCommand() {
13338
13599
  printDual({
13339
13600
  json: { brain: where.name, hosted: true, rules: rules2, total: total2 },
13340
13601
  human: () => {
13341
- printHuman(`## rules \u2014 ${stripControl(where.name)} (${total2})
13602
+ printHuman(`## rules \u2014 ${oneLine(where.name, WIDTH.title)} (${total2})
13342
13603
  `);
13343
13604
  if (rules2.length === 0) {
13344
13605
  printHuman("_Nothing is marked as a rule in this brain._\n");
@@ -13350,8 +13611,8 @@ function rulesCommand() {
13350
13611
  return;
13351
13612
  }
13352
13613
  for (const rule of rules2) {
13353
- printHuman(`- ${stripControl(rule.summary ?? rule.title)}`);
13354
- 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)}\``));
13355
13616
  }
13356
13617
  if (total2 > rules2.length) {
13357
13618
  printHuman(c.gray(`
@@ -13403,10 +13664,10 @@ This answered from \`${brainRoot}\` instead, so rules you wrote in the missing o
13403
13664
  printHuman(`## rules (${rules.length}${total > rules.length ? ` of ${total}` : ""})
13404
13665
  `);
13405
13666
  for (const { entry } of rules) {
13406
- printHuman(`- ${stripControl(entry.memory.summary ?? entry.memory.title)}`);
13667
+ printHuman(`- ${oneLine(entry.memory.summary ?? entry.memory.title, WIDTH.text)}`);
13407
13668
  printHuman(
13408
13669
  c.gray(
13409
- ` \`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)}` : ""}`
13410
13671
  )
13411
13672
  );
13412
13673
  }
@@ -13768,7 +14029,7 @@ async function reportPreviousAttempt() {
13768
14029
  function reportNotice(text) {
13769
14030
  if (!text) return;
13770
14031
  printHuman(c.gray(`
13771
- note from memgineering.com: "${stripControl(text)}"`));
14032
+ note from memgineering.com: "${oneLine(text, WIDTH.text)}"`));
13772
14033
  }
13773
14034
  function reportBelowMinimum(below) {
13774
14035
  if (!below) return;
@@ -13806,11 +14067,11 @@ function undoCommand() {
13806
14067
  json: { ...undone, brain: { hosted: true, name: where.name } },
13807
14068
  human: () => {
13808
14069
  printHuman(
13809
- `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)}.
13810
14071
  `
13811
14072
  );
13812
- printHuman(c.gray(` ${stripControl(undone.target)}`));
13813
- 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)}\``));
13814
14075
  }
13815
14076
  });
13816
14077
  return;
@@ -13896,7 +14157,7 @@ ${matches.slice(0, 8).map((id) => ` ${id}`).join("\n")}${matches.length > 8 ? "
13896
14157
  },
13897
14158
  human: () => {
13898
14159
  printHuman(
13899
- `Undone: ${stripControl(target.verb)} on \`${stripControl(target.target)}\`
14160
+ `Undone: ${oneLine(target.verb, WIDTH.title)} on \`${oneLine(target.target, WIDTH.path)}\`
13900
14161
  `
13901
14162
  );
13902
14163
  if (opId === void 0) {
@@ -13987,13 +14248,91 @@ Read what it says now, then decide what should stay.`;
13987
14248
  The text from before that change is still on this machine: ${join23(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
13988
14249
  }
13989
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
+
13990
14327
  // src/commands/use.ts
13991
14328
  init_atomic();
13992
14329
  init_brand();
13993
14330
  init_errors();
13994
- init_config();
14331
+ init_api_client();
14332
+ init_credentials();
13995
14333
  import { isAbsolute as isAbsolute5, relative as relative3, resolve as resolve11 } from "path";
13996
- import { Command as Command25 } from "commander";
14334
+ import { Command as Command26 } from "commander";
14335
+ init_config();
13997
14336
  init_vault();
13998
14337
  init_ui();
13999
14338
  function lostDefaultLines(missing) {
@@ -14012,16 +14351,24 @@ var VIA_MEANS = {
14012
14351
  default: "because it is your default brain (`memgineering use --default <path>` changes it)"
14013
14352
  };
14014
14353
  function useCommand() {
14015
- 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(
14016
14357
  "--default",
14017
- "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"
14018
14359
  ).action(async (brainPath, opts) => {
14360
+ if (opts.brain !== void 0) {
14361
+ await pointAtHosted(opts.brain, opts, brainPath);
14362
+ return;
14363
+ }
14019
14364
  const where = await canonicalize(resolve11(opts.here ?? process.cwd()));
14020
14365
  if (!brainPath) {
14021
14366
  const pointer = await readPointer(where);
14022
14367
  const resolved = await resolveBrain(void 0, where).catch((err) => err);
14023
14368
  const stuck = resolved instanceof Error;
14024
14369
  const hosted = await hostedPointer();
14370
+ const stored = await readCredentials().catch(() => null);
14371
+ const signedIn = stored !== null && stored.api_url === apiUrl();
14025
14372
  printDual({
14026
14373
  json: {
14027
14374
  // The brain that answers `recall` and takes `remember` — the same
@@ -14056,14 +14403,24 @@ function useCommand() {
14056
14403
  brain: stuck ? null : resolved.brain.root,
14057
14404
  via: stuck ? null : resolved.via,
14058
14405
  pointer: pointer ? pointer.from : null,
14059
- ...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 ? {
14060
14417
  problem: resolved.code ?? "unresolved",
14061
14418
  message: resolved.message
14062
14419
  } : {}
14063
14420
  },
14064
14421
  human: () => {
14065
14422
  if (hosted?.signedIn) {
14066
- printHuman(`Using the hosted brain \`${stripControl(hosted.name)}\`
14423
+ printHuman(`Using the hosted brain \`${oneLine(hosted.name, NAME_WIDTH2)}\`
14067
14424
  `);
14068
14425
  printHuman(
14069
14426
  c.gray(
@@ -14084,7 +14441,7 @@ function useCommand() {
14084
14441
  );
14085
14442
  printHuman(
14086
14443
  c.yellow(
14087
- ` 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.`
14088
14445
  )
14089
14446
  );
14090
14447
  printHuman(
@@ -14101,7 +14458,9 @@ function useCommand() {
14101
14458
  printHuman(`No brain resolves here.
14102
14459
  `);
14103
14460
  printHuman(
14104
- 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.")
14105
14464
  );
14106
14465
  return;
14107
14466
  }
@@ -14110,7 +14469,7 @@ function useCommand() {
14110
14469
  printHuman(c.gray(` ${VIA_MEANS[resolved.via]}`));
14111
14470
  printHuman(
14112
14471
  c.yellow(
14113
- ` 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,`
14114
14473
  ) + c.gray("\n so reads and writes are landing here instead.")
14115
14474
  );
14116
14475
  printHuman(
@@ -14121,6 +14480,14 @@ function useCommand() {
14121
14480
  if (stuck) {
14122
14481
  printHuman(`No brain resolves here.
14123
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
+ }
14124
14491
  printHuman(c.gray(` ${resolved.message}`));
14125
14492
  const hint = resolved.hint;
14126
14493
  if (hint) printHuman(c.gray(`
@@ -14187,13 +14554,114 @@ A pointer to an unlinked folder would fail on every command instead of at this o
14187
14554
  });
14188
14555
  });
14189
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
+ }
14190
14658
 
14191
14659
  // src/commands/whoami.ts
14192
14660
  init_api_client();
14193
14661
  init_ui();
14194
- import { Command as Command26 } from "commander";
14662
+ import { Command as Command27 } from "commander";
14195
14663
  function whoamiCommand() {
14196
- 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 () => {
14197
14665
  const base = apiUrl();
14198
14666
  const credentials = await requireCredentials(base);
14199
14667
  const res = await apiRequest({
@@ -14219,9 +14687,11 @@ function whoamiCommand() {
14219
14687
  },
14220
14688
  human: () => {
14221
14689
  printHuman(
14222
- 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
+ )
14223
14693
  );
14224
- if (account.email) printHuman(c.gray(` ${stripControl(account.email)}`));
14694
+ if (account.email) printHuman(c.gray(` ${oneLine(account.email, WIDTH.title)}`));
14225
14695
  printHuman(c.gray(` on ${base}`));
14226
14696
  if (live !== null) {
14227
14697
  printHuman(
@@ -14236,6 +14706,8 @@ function whoamiCommand() {
14236
14706
  }
14237
14707
 
14238
14708
  // src/lib/next-step.ts
14709
+ init_api_client();
14710
+ init_credentials();
14239
14711
  init_config();
14240
14712
  function isSetUp(cfg) {
14241
14713
  return Object.keys(cfg.system.installed).length > 0;
@@ -14289,7 +14761,16 @@ Then run \`memgineering onboard\`: it prints what to ask so the brain knows whos
14289
14761
  it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
14290
14762
 
14291
14763
  Read \`memgineering-setup\` for accounts and which of their tools this reaches.`;
14292
- 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;
14293
14774
  if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
14294
14775
  if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
14295
14776
  return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
@@ -14298,7 +14779,8 @@ function nextStepFor(cfg) {
14298
14779
  }
14299
14780
  async function currentNextStep() {
14300
14781
  try {
14301
- return nextStepFor(await loadConfig());
14782
+ const stored = await readCredentials().catch(() => null);
14783
+ return nextStepFor(await loadConfig(), stored !== null && stored.api_url === apiUrl());
14302
14784
  } catch {
14303
14785
  return null;
14304
14786
  }
@@ -14308,7 +14790,7 @@ async function currentNextStep() {
14308
14790
  init_ui();
14309
14791
  init_brand();
14310
14792
  function buildProgram() {
14311
- 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) => {
14312
14794
  const opts = thisCommand.optsWithGlobals();
14313
14795
  if (opts.json || process.env[BRAND.jsonEnvVar] === "1") setJsonMode(true);
14314
14796
  await announceNextStep(actionCommand.name());
@@ -14329,6 +14811,7 @@ function buildProgram() {
14329
14811
  program.addCommand(initCommand());
14330
14812
  program.addCommand(linkCommand());
14331
14813
  program.addCommand(useCommand());
14814
+ program.addCommand(brainsCommand());
14332
14815
  program.addCommand(unlinkCommand());
14333
14816
  program.addCommand(reindexCommand());
14334
14817
  program.addCommand(syncRulesCommand());
@@ -14368,8 +14851,15 @@ function makeParseErrorsActionable(program) {
14368
14851
  outputError: (str, write) => {
14369
14852
  write(str);
14370
14853
  if (!/unknown option|unknown command|too many arguments/i.test(str)) return;
14371
- const flags = cmd.options.map((o) => o.long ?? o.short).filter(Boolean);
14372
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);
14373
14863
  write(
14374
14864
  flags.length > 0 ? `hint: ${path} takes ${flags.join(" ")}
14375
14865
  ` : `hint: \`${path} --help\` lists what this takes.
@@ -14397,7 +14887,7 @@ async function handleError(err) {
14397
14887
  if (isJsonMode()) {
14398
14888
  printJson({ error: { code: err.code, message: err.message, hint: err.hint } });
14399
14889
  } else {
14400
- printHuman(c.red(`\u2717 ${stripControl(err.message)}`));
14890
+ printHuman(c.red(`\u2717 ${oneLine(err.message, WIDTH.text)}`));
14401
14891
  for (const line of stripControl(err.hint).split("\n")) printHuman(c.gray(` \u2192 ${line}`));
14402
14892
  }
14403
14893
  process.exit(1);
@@ -14414,7 +14904,7 @@ async function handleError(err) {
14414
14904
  }
14415
14905
  });
14416
14906
  } else {
14417
- printHuman(c.red(`\u2717 ${stripControl(err.message)}`));
14907
+ printHuman(c.red(`\u2717 ${oneLine(err.message, WIDTH.text)}`));
14418
14908
  if (hintStr) {
14419
14909
  for (const line of stripControl(hintStr).split("\n")) printHuman(c.gray(` \u2192 ${line}`));
14420
14910
  }
@@ -14422,7 +14912,7 @@ async function handleError(err) {
14422
14912
  process.exit(1);
14423
14913
  }
14424
14914
  if (isJsonMode()) printJson({ error: { code: "generic_failure", message: String(err) } });
14425
- else printHuman(c.red(`\u2717 ${stripControl(String(err))}`));
14915
+ else printHuman(c.red(`\u2717 ${oneLine(String(err), WIDTH.text)}`));
14426
14916
  process.exit(1);
14427
14917
  }
14428
14918