memgineering 0.7.5 → 0.9.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
  /**
@@ -168,10 +181,32 @@ function quoteBlock(text, prefix = "> ") {
168
181
  return stripControl(text).split("\n").map((line) => prefix + line).join("\n");
169
182
  }
170
183
  function oneLine(text, max) {
171
- const flat = stripControl(text).replace(/\s+/g, " ").trim();
184
+ const flat = oneLineWhole(text);
172
185
  const chars = [...flat];
173
186
  if (chars.length <= max) return flat;
174
- return `${chars.slice(0, Math.max(1, max - 1)).join("").trimEnd()}\u2026`;
187
+ const budget = Math.max(1, max - 1);
188
+ let shown = chars.slice(0, budget).join("").trimEnd();
189
+ const next = chars[budget];
190
+ const cutsWord = next !== void 0 && /\S/.test(next) && /\S/.test(shown.at(-1) ?? "");
191
+ const lastSpace = shown.lastIndexOf(" ");
192
+ if (cutsWord && lastSpace >= Math.floor(budget * 0.6))
193
+ shown = shown.slice(0, lastSpace).trimEnd();
194
+ return `${shown}\u2026`;
195
+ }
196
+ function oneLineWhole(text) {
197
+ return stripControl(text).replace(/\s+/g, " ").trim();
198
+ }
199
+ function middleLine(text, max) {
200
+ const chars = [...oneLineWhole(text)];
201
+ if (chars.length <= max) return chars.join("");
202
+ if (max <= 1) return "\u2026";
203
+ const budget = max - 1;
204
+ const headLength = Math.max(1, Math.floor(budget * 0.4));
205
+ const tailLength = Math.max(1, budget - headLength);
206
+ return `${chars.slice(0, headLength).join("")}\u2026${chars.slice(-tailLength).join("")}`;
207
+ }
208
+ function printBlock(text) {
209
+ printHuman(stripControl(text));
175
210
  }
176
211
  function progress(text) {
177
212
  if (isInteractive()) process.stderr.write(kleur.gray(` ${text}
@@ -187,13 +222,23 @@ function undoHint(opId, vault) {
187
222
  function quoteArg(value) {
188
223
  return /^[A-Za-z0-9._\/-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
189
224
  }
190
- var jsonMode, CONTROL_INTRODUCER, c;
225
+ var jsonMode, CONTROL_INTRODUCER, WIDTH, c;
191
226
  var init_ui = __esm({
192
227
  "src/ui.ts"() {
193
228
  "use strict";
194
229
  init_brand();
195
230
  jsonMode = false;
196
231
  CONTROL_INTRODUCER = /[\u001b\u009b\u009d]/;
232
+ WIDTH = {
233
+ /** A brain name, a note title — a heading-shaped thing. */
234
+ title: 72,
235
+ /** A vault path. Longer, because the tail is the part that identifies it. */
236
+ path: 120,
237
+ /** An id or an `open:` handle. */
238
+ handle: 128,
239
+ /** A claim, a summary, a server hint — a sentence rather than a label. */
240
+ text: 200
241
+ };
197
242
  c = kleur;
198
243
  }
199
244
  });
@@ -221,11 +266,12 @@ var init_errors = __esm({
221
266
 
222
267
  // ../../packages/memory-engine/src/schema.ts
223
268
  import { z } from "zod";
224
- var MappingModeSchema, ProvenanceSchema, MemoryStatusSchema, ClaimConfidenceSchema, MemoryKindSchema, ClaimSchema, MemorySchema, RelationTypeSchema, RelationSourceSchema, RelationSchema;
269
+ var MappingModeSchema, ScopeSourceSchema, ProvenanceSchema, MemoryStatusSchema, ClaimConfidenceSchema, MemoryKindSchema, ClaimSchema, MemorySchema, RelationTypeSchema, RelationSourceSchema, RelationSchema;
225
270
  var init_schema = __esm({
226
271
  "../../packages/memory-engine/src/schema.ts"() {
227
272
  "use strict";
228
273
  MappingModeSchema = z.enum(["explicit", "heuristic"]);
274
+ ScopeSourceSchema = z.enum(["explicit", "path", "none"]);
229
275
  ProvenanceSchema = z.object({
230
276
  /** Vault-relative path of the note this was mapped from. */
231
277
  sourcePath: z.string().min(1),
@@ -238,7 +284,9 @@ var init_schema = __esm({
238
284
  * here would be the engine learning one backend's internals.
239
285
  */
240
286
  sourceRevision: z.unknown(),
241
- mapping: MappingModeSchema
287
+ mapping: MappingModeSchema,
288
+ /** Field-level provenance needed before scope may lower a semantic threshold. */
289
+ scopeSource: ScopeSourceSchema.optional()
242
290
  });
243
291
  MemoryStatusSchema = z.enum(["active", "superseded", "retired"]);
244
292
  ClaimConfidenceSchema = z.enum(["stated", "observed", "inferred"]);
@@ -618,6 +666,8 @@ function mapNoteToMemory(input) {
618
666
  const ns = explicit ? nsRaw : {};
619
667
  const id = asString(ns["id"]) ?? path;
620
668
  const title = asString(ns["title"]) ?? asString(frontmatter?.["title"]) ?? h1Of(outline.intro) ?? titleFromPath(path);
669
+ const declaredScope = asString(ns["scope"]);
670
+ const scope = declaredScope ?? scopeFromPath(path);
621
671
  const summary = asString(ns["summary"]) ?? outline.description ?? firstParagraph(outline.intro) ?? firstSectionParagraph(outline.sections) ?? null;
622
672
  const memory = {
623
673
  id,
@@ -632,7 +682,7 @@ function mapNoteToMemory(input) {
632
682
  })(),
633
683
  title,
634
684
  summary,
635
- scope: asString(ns["scope"]) ?? scopeFromPath(path),
685
+ scope,
636
686
  current_claim: asString(ns["current_claim"]),
637
687
  status: statusOf(ns["status"], issues),
638
688
  // Only the author's own `binding: true`. Never inferred: guessing that a
@@ -650,7 +700,8 @@ function mapNoteToMemory(input) {
650
700
  provenance: {
651
701
  sourcePath: path,
652
702
  sourceRevision: revision,
653
- mapping: explicit ? "explicit" : "heuristic"
703
+ mapping: explicit ? "explicit" : "heuristic",
704
+ scopeSource: declaredScope !== null ? "explicit" : scope === null ? "none" : "path"
654
705
  }
655
706
  };
656
707
  const claims = [];
@@ -764,7 +815,7 @@ var init_cache_io = __esm({
764
815
  "../../packages/memory-engine/src/view/cache-io.ts"() {
765
816
  "use strict";
766
817
  init_schema();
767
- SCHEMA_VERSION = 5;
818
+ SCHEMA_VERSION = 6;
768
819
  POLICY_VERSION = 1;
769
820
  CacheEntrySchema = z2.object({
770
821
  path: z2.string().min(1),
@@ -1371,18 +1422,40 @@ var init_conflicts = __esm({
1371
1422
  });
1372
1423
 
1373
1424
  // ../../packages/memory-engine/src/recall/search.ts
1425
+ function splitMixedScript(token) {
1426
+ return token.split(LATIN_HANGUL_BOUNDARY).filter((part) => part.length > 0);
1427
+ }
1374
1428
  function terms(text) {
1375
- return text.normalize("NFC").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((t) => t.length > 0);
1429
+ return [...text.normalize("NFC").toLowerCase().matchAll(TOKEN)].flatMap(
1430
+ (match) => splitMixedScript(match[0])
1431
+ );
1432
+ }
1433
+ function indexTerms(text) {
1434
+ return [...text.normalize("NFC").toLowerCase().matchAll(TOKEN)].flatMap((match) => {
1435
+ const token = match[0];
1436
+ const exact = splitMixedScript(token);
1437
+ if (!HAS_COMPONENT_SEPARATOR.test(token)) return exact;
1438
+ const components = token.split(COMPONENT_SEPARATOR).filter((part) => part.length > 0).flatMap(splitMixedScript);
1439
+ return [...exact, ...components];
1440
+ });
1441
+ }
1442
+ function exactOnly(term) {
1443
+ return HAS_COMPONENT_SEPARATOR.test(term) || new RegExp("\\p{N}", "u").test(term);
1376
1444
  }
1377
1445
  function startsWithFieldTerm(query, fieldTerms) {
1378
- if (query.length <= SUBSTRING_MIN) return false;
1446
+ if (query.length <= SUBSTRING_MIN || !HANGUL.test(query)) return false;
1379
1447
  for (const f of fieldTerms) {
1380
- if (f.length >= SUBSTRING_MIN && f.length < query.length && query.startsWith(f)) return true;
1448
+ if (HANGUL.test(f) && f.length >= SUBSTRING_MIN && f.length < query.length && query.startsWith(f))
1449
+ return true;
1381
1450
  }
1382
1451
  return false;
1383
1452
  }
1453
+ function particleSuffixedFieldTerm(query, firstFieldTerm) {
1454
+ if (!ONE_SYLLABLE_TOPIC_TERMS.has(query)) return false;
1455
+ return PARTICLES.some((particle) => firstFieldTerm === `${query}${particle}`);
1456
+ }
1384
1457
  function variantsOf(term) {
1385
- if (!HANGUL.test(term)) return [term];
1458
+ if (exactOnly(term) || !HANGUL.test(term)) return [term];
1386
1459
  for (const particle of PARTICLES) {
1387
1460
  if (!term.endsWith(particle)) continue;
1388
1461
  const stem = term.slice(0, -particle.length);
@@ -1390,19 +1463,22 @@ function variantsOf(term) {
1390
1463
  }
1391
1464
  return [term];
1392
1465
  }
1393
- function fieldScore(queryTerms, field, weight) {
1466
+ function fieldScore(queryTerms, field, weight, allowLeadingOneSyllable = false) {
1394
1467
  if (field === null || field === "") return 0;
1395
1468
  const normalized = field.normalize("NFC").toLowerCase();
1396
- const fieldTerms = new Set(terms(field));
1469
+ const orderedFieldTerms = indexTerms(field);
1470
+ const fieldTerms = new Set(orderedFieldTerms);
1397
1471
  let score = 0;
1398
- for (const q of queryTerms) {
1472
+ for (const [queryIndex, q] of queryTerms.entries()) {
1399
1473
  let best = 0;
1400
1474
  for (const variant of variantsOf(q)) {
1401
1475
  if (fieldTerms.has(variant)) {
1402
1476
  best = Math.max(best, weight);
1403
- } else if (variant.length >= SUBSTRING_MIN && normalized.includes(variant)) {
1477
+ } else if (allowLeadingOneSyllable && queryIndex === 0 && particleSuffixedFieldTerm(variant, orderedFieldTerms[0])) {
1478
+ best = Math.max(best, weight);
1479
+ } else if (!exactOnly(variant) && variant.length >= SUBSTRING_MIN && normalized.includes(variant)) {
1404
1480
  best = Math.max(best, weight / 2);
1405
- } else if (startsWithFieldTerm(variant, fieldTerms)) {
1481
+ } else if (!exactOnly(variant) && startsWithFieldTerm(variant, fieldTerms)) {
1406
1482
  best = Math.max(best, weight / 2);
1407
1483
  }
1408
1484
  }
@@ -1415,7 +1491,7 @@ function searchMemories(memories, query) {
1415
1491
  if (q.length === 0) return [];
1416
1492
  const scored = [];
1417
1493
  for (const memory of memories) {
1418
- let score = fieldScore(q, memory.title, WEIGHTS.title);
1494
+ let score = fieldScore(q, memory.title, WEIGHTS.title, true);
1419
1495
  for (const alias of memory.aliases) score += fieldScore(q, alias, WEIGHTS.alias);
1420
1496
  for (const entity of memory.entities) score += fieldScore(q, entity, WEIGHTS.entity);
1421
1497
  score += fieldScore(q, memory.summary, WEIGHTS.summary);
@@ -1426,11 +1502,15 @@ function searchMemories(memories, query) {
1426
1502
  (a, b) => b.score !== a.score ? b.score - a.score : a.memory.id < b.memory.id ? -1 : 1
1427
1503
  );
1428
1504
  }
1429
- var WEIGHTS, SUBSTRING_MIN, PARTICLES, HANGUL;
1505
+ var WEIGHTS, TOKEN, COMPONENT_SEPARATOR, HAS_COMPONENT_SEPARATOR, LATIN_HANGUL_BOUNDARY, SUBSTRING_MIN, PARTICLES, HANGUL, ONE_SYLLABLE_TOPIC_TERMS;
1430
1506
  var init_search = __esm({
1431
1507
  "../../packages/memory-engine/src/recall/search.ts"() {
1432
1508
  "use strict";
1433
1509
  WEIGHTS = { title: 6, alias: 4, entity: 3, summary: 2, scope: 1 };
1510
+ TOKEN = /[./]?[\p{L}\p{N}]+(?:[._/-][\p{L}\p{N}]+)*/gu;
1511
+ COMPONENT_SEPARATOR = /[._/-]+/u;
1512
+ HAS_COMPONENT_SEPARATOR = /[._/-]/u;
1513
+ LATIN_HANGUL_BOUNDARY = new RegExp("(?<=\\p{Script=Latin})(?=\\p{Script=Hangul})|(?<=\\p{Script=Hangul})(?=\\p{Script=Latin})", "u");
1434
1514
  SUBSTRING_MIN = 2;
1435
1515
  PARTICLES = [
1436
1516
  "\uC774\uB77C\uB3C4",
@@ -1468,6 +1548,7 @@ var init_search = __esm({
1468
1548
  "\uAED8"
1469
1549
  ];
1470
1550
  HANGUL = /[\uac00-\ud7a3]/;
1551
+ ONE_SYLLABLE_TOPIC_TERMS = /* @__PURE__ */ new Set(["\uD0A4"]);
1471
1552
  }
1472
1553
  });
1473
1554
 
@@ -1485,7 +1566,7 @@ var init_chunks = __esm({
1485
1566
  avgLength;
1486
1567
  constructor(refs) {
1487
1568
  for (const ref of refs) {
1488
- const tokens = terms(ref.text);
1569
+ const tokens = indexTerms(ref.text);
1489
1570
  if (tokens.length === 0) continue;
1490
1571
  const counts = /* @__PURE__ */ new Map();
1491
1572
  for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);
@@ -1547,6 +1628,34 @@ var init_chunks = __esm({
1547
1628
  function isRecallDepth(value) {
1548
1629
  return typeof value === "string" && RECALL_DEPTHS.includes(value);
1549
1630
  }
1631
+ function isStrongDecisionMarker(term) {
1632
+ return /^(?:정하|정했|정한|정해|결정하|결정했|결정한|합의하|합의했|합의한|지키|지켜|따르|골랐|고른)/u.test(
1633
+ term
1634
+ ) || /^(?:decide[ds]?|deciding|settle[ds]?|settling|agree[ds]?|agreeing|choose[ds]?|choosing|chose|chosen|follow(?:s|ed|ing)?)$/u.test(
1635
+ term
1636
+ );
1637
+ }
1638
+ function variantsInclude(term, candidates) {
1639
+ return variantsOf(term).some((variant) => candidates.has(variant));
1640
+ }
1641
+ function isDecisionContext(term) {
1642
+ return variantsInclude(term, DECISION_CONTEXT) || term.startsWith("\uC720\uD6A8");
1643
+ }
1644
+ function shapeRecallQuery(query) {
1645
+ const queryTerms = terms(query);
1646
+ const strong = queryTerms.some(isStrongDecisionMarker);
1647
+ const noun = queryTerms.some((term) => variantsInclude(term, DECISION_NOUNS));
1648
+ const contextual = queryTerms.some(isDecisionContext);
1649
+ const decisionIntent = strong || noun && contextual;
1650
+ if (!decisionIntent) return { lexical: query, decisionIntent: false };
1651
+ const topical = queryTerms.filter(
1652
+ (term) => !isStrongDecisionMarker(term) && !variantsInclude(term, DECISION_NOUNS) && !variantsInclude(term, DECISION_FILLERS) && !term.startsWith("\uC720\uD6A8")
1653
+ );
1654
+ return {
1655
+ lexical: topical.length === 0 ? query : topical.join(" "),
1656
+ decisionIntent
1657
+ };
1658
+ }
1550
1659
  function freshnessOf(memory, claims, now) {
1551
1660
  if (memory.status === "superseded") return "superseded";
1552
1661
  if (memory.status === "retired") return "retired";
@@ -1573,12 +1682,14 @@ function recall(entries, query, opts = {}) {
1573
1682
  const claimsById = new Map(inScope.map((e) => [e.memory.id, e.claims]));
1574
1683
  const relations = inScope.flatMap((e) => e.relations);
1575
1684
  const allMemories = inScope.map((e) => e.memory);
1576
- const cardHits = searchMemories(allMemories, query);
1685
+ const allMemoryIds = new Set(allMemories.map((memory) => memory.id));
1686
+ const queryShape = shapeRecallQuery(query);
1687
+ const cardHits = searchMemories(allMemories, queryShape.lexical);
1577
1688
  const chunkHits = depth === "exact" ? [] : new ChunkIndex(
1578
1689
  inScope.flatMap(
1579
1690
  (e) => (e.chunks ?? []).map((c2) => ({ memoryId: e.memory.id, label: c2.label, text: c2.text }))
1580
1691
  )
1581
- ).search(query);
1692
+ ).search(queryShape.lexical);
1582
1693
  const semanticHits = depth === "exact" ? [] : opts.semantic ?? [];
1583
1694
  const scoreById = /* @__PURE__ */ new Map();
1584
1695
  for (const hit of cardHits) scoreById.set(hit.memory.id, hit.score);
@@ -1594,14 +1705,26 @@ function recall(entries, query, opts = {}) {
1594
1705
  const memory = memoryById.get(id);
1595
1706
  return memory === void 0 ? [] : [{ memory, score }];
1596
1707
  }).sort((a, b) => b.score !== a.score ? b.score - a.score : a.memory.id < b.memory.id ? -1 : 1);
1597
- const queryTerms = Math.max(1, terms(query).length);
1708
+ const authoritativeSupersedes = queryShape.decisionIntent ? relations.filter(
1709
+ (relation) => relation.type === "supersedes" && relation.source === "explicit" && relation.weight > 0 && allMemoryIds.has(relation.from) && allMemoryIds.has(relation.to)
1710
+ ) : [];
1711
+ const supersededByAnother = new Set(authoritativeSupersedes.map((relation) => relation.to));
1712
+ const supersedingIds = new Set(
1713
+ authoritativeSupersedes.map((relation) => relation.from).filter((id) => !supersededByAnother.has(id))
1714
+ );
1715
+ const queryTerms = Math.max(1, terms(queryShape.lexical).length);
1598
1716
  const scored = hits.map((hit) => ({
1599
1717
  memory: hit.memory,
1600
1718
  currency: CURRENCY_RANK[freshnessOf(hit.memory, claimsById.get(hit.memory.id) ?? [], now)],
1601
- score: hit.score + boostFor(hit.memory) / queryTerms
1719
+ score: hit.score + boostFor(hit.memory) / queryTerms,
1720
+ // Only a final tie-break, and only when the question asks for a standing
1721
+ // decision. A valid outgoing supersedes edge says this note is the declared
1722
+ // replacement; it does not say an off-topic replacement beats a better
1723
+ // topical match.
1724
+ superseding: supersedingIds.has(hit.memory.id)
1602
1725
  }));
1603
1726
  const ranked = scored.sort(
1604
- (a, b) => a.currency !== b.currency ? a.currency - b.currency : b.score !== a.score ? b.score - a.score : a.memory.id < b.memory.id ? -1 : 1
1727
+ (a, b) => a.currency !== b.currency ? a.currency - b.currency : b.score !== a.score ? b.score - a.score : a.superseding !== b.superseding ? a.superseding ? -1 : 1 : a.memory.id < b.memory.id ? -1 : 1
1605
1728
  );
1606
1729
  const selected = ranked.slice(0, limit).map((s) => s.memory);
1607
1730
  const byId = new Map(allMemories.map((m) => [m.id, m]));
@@ -1686,7 +1809,7 @@ function depthOf(value) {
1686
1809
  function boostFor(memory) {
1687
1810
  return (memory.binding === true ? BINDING_BOOST : 0) + (memory.kind === void 0 ? 0 : KIND_BOOST[memory.kind] ?? 0);
1688
1811
  }
1689
- var RECALL_DEPTHS, RecallOptionError, DEFAULTS, BODY_WEIGHT, SEMANTIC_WEIGHT, BINDING_BOOST, KIND_BOOST, CURRENCY_RANK, WIDEST_ESTIMATE;
1812
+ var RECALL_DEPTHS, RecallOptionError, DEFAULTS, BODY_WEIGHT, SEMANTIC_WEIGHT, BINDING_BOOST, KIND_BOOST, DECISION_NOUNS, DECISION_CONTEXT, DECISION_FILLERS, CURRENCY_RANK, WIDEST_ESTIMATE;
1690
1813
  var init_planner = __esm({
1691
1814
  "../../packages/memory-engine/src/recall/planner.ts"() {
1692
1815
  "use strict";
@@ -1716,6 +1839,60 @@ var init_planner = __esm({
1716
1839
  decision: BINDING_BOOST / 2,
1717
1840
  preference: BINDING_BOOST / 2
1718
1841
  };
1842
+ DECISION_NOUNS = /* @__PURE__ */ new Set([
1843
+ "\uACB0\uC815",
1844
+ "\uADDC\uCE59",
1845
+ "\uB8F0",
1846
+ "\uD569\uC758",
1847
+ "decision",
1848
+ "decisions",
1849
+ "rule",
1850
+ "rules",
1851
+ "agreement"
1852
+ ]);
1853
+ DECISION_CONTEXT = /* @__PURE__ */ new Set([
1854
+ "\uC9C0\uAE08",
1855
+ "\uD604\uC7AC",
1856
+ "\uC544\uC9C1",
1857
+ "\uC608\uC804\uC5D0",
1858
+ "\uBB58",
1859
+ "\uBB50",
1860
+ "\uBB34\uC5C7",
1861
+ "\uC5B4\uB5BB\uAC8C",
1862
+ "what",
1863
+ "which",
1864
+ "did",
1865
+ "now",
1866
+ "current",
1867
+ "currently",
1868
+ "still"
1869
+ ]);
1870
+ DECISION_FILLERS = /* @__PURE__ */ new Set([
1871
+ ...DECISION_CONTEXT,
1872
+ "\uB0B4\uAC00",
1873
+ "\uC6B0\uB9AC\uAC00",
1874
+ "\uC6B0\uB9AC",
1875
+ "\uB300\uD574",
1876
+ "\uAD00\uD574",
1877
+ "\uD558\uB294",
1878
+ "\uD588\uB358",
1879
+ "\uD560",
1880
+ "\uAC83",
1881
+ "\uBC18\uB4DC\uC2DC",
1882
+ "\uC911",
1883
+ "about",
1884
+ "on",
1885
+ "for",
1886
+ "to",
1887
+ "do",
1888
+ "does",
1889
+ "is",
1890
+ "the",
1891
+ "we",
1892
+ "i",
1893
+ "our",
1894
+ "my"
1895
+ ]);
1719
1896
  CURRENCY_RANK = {
1720
1897
  current: 0,
1721
1898
  expired: 1,
@@ -1958,18 +2135,21 @@ function detectConsolidation(entries, opts = {}) {
1958
2135
  scanned: 0,
1959
2136
  truncated: false
1960
2137
  };
1961
- const out = [
1962
- ...similarTitles(live, budget),
1963
- ...sharedContext(live, budget),
1964
- // Neither of these enumerates pairs — one walks declared edges, the other
1965
- // walks notes — so neither can be the thing that blows up, and neither
1966
- // spends the budget.
2138
+ const semanticExcluded = semanticGlobalExclusions(live, []);
2139
+ const semantic = opts.semantic === void 0 ? [] : boundedSemanticSimilarities(live, budget, opts.semantic, semanticExcluded);
2140
+ const titles = similarTitles(live, budget);
2141
+ const context = sharedContext(live, budget);
2142
+ const scored = [
2143
+ // Declared edges enumerate no pairs and spend no budget. Supersession is
2144
+ // first because it names both the winner and the still-active loser.
2145
+ ...declaredSupersessions(live),
1967
2146
  ...declaredContradictions(live),
1968
- ...kindPromotions(live)
2147
+ ...kindPromotions(live),
2148
+ ...semantic,
2149
+ ...context,
2150
+ ...titles
1969
2151
  ];
1970
- out.sort(
1971
- (a, b) => CONSOLIDATION_RULES.indexOf(a.rule) - CONSOLIDATION_RULES.indexOf(b.rule) || a.memory_id.localeCompare(b.memory_id) || (a.other_id ?? "").localeCompare(b.other_id ?? "")
1972
- );
2152
+ const out = rankCandidates(scored);
1973
2153
  return {
1974
2154
  candidates: out.slice(0, opts.limit ?? DEFAULT_LIMIT),
1975
2155
  scanned: budget.scanned,
@@ -1991,24 +2171,63 @@ function overlap(a, b) {
1991
2171
  }
1992
2172
  function similarTitles(entries, budget) {
1993
2173
  const termSets = entries.map((e) => titleTerms(e.memory.title));
2174
+ const cyclicKeys = entries.map((entry) => cyclicTitleKey(entry.memory.title));
1994
2175
  const buckets = /* @__PURE__ */ new Map();
2176
+ const byCyclicTitle = /* @__PURE__ */ new Map();
1995
2177
  termSets.forEach((set, i) => {
2178
+ const cyclicKey = cyclicKeys[i];
2179
+ if (cyclicKey !== null && cyclicKey !== void 0) addToBucket(byCyclicTitle, cyclicKey, i);
1996
2180
  for (const term of set) {
1997
2181
  const bucket = buckets.get(term);
1998
2182
  if (bucket) bucket.push(i);
1999
2183
  else buckets.set(term, [i]);
2000
2184
  }
2001
2185
  });
2002
- const out = [];
2186
+ const out = cyclicTitleSpanningCandidates(entries, byCyclicTitle);
2003
2187
  for (const [i, j] of pairsFrom(buckets, budget)) {
2004
2188
  const score = overlap(termSets[i], termSets[j]);
2005
2189
  if (score < TITLE_OVERLAP) continue;
2006
- out.push(
2007
- pairCandidate("similar_title", entries[i], entries[j], {
2190
+ out.push({
2191
+ candidate: pairCandidate("similar_title", entries[i], entries[j], {
2008
2192
  evidence: `titles overlap ${Math.round(score * 100)}%: "${entries[i].memory.title}" / "${entries[j].memory.title}"`,
2009
2193
  suggested_action: "supersede"
2010
- })
2194
+ }),
2195
+ // Exact lexical identity is a little stronger than the broad
2196
+ // scope+entity rule. This changes only ordering/evidence selection — the
2197
+ // same pair was already detected at the 2/3 threshold.
2198
+ confidence: score === 1 && cyclicKeys[i] !== null && cyclicKeys[i] === cyclicKeys[j] ? 2.01 : 1 + score
2199
+ });
2200
+ }
2201
+ return out;
2202
+ }
2203
+ function cyclicTitleKey(title) {
2204
+ const sequence = terms(title).map((term) => term.normalize("NFC").toLowerCase());
2205
+ if (sequence.length === 0) return null;
2206
+ if (sequence.length < 3) return sequence.join("\0");
2207
+ let best = null;
2208
+ for (let offset = 0; offset < sequence.length; offset += 1) {
2209
+ const key2 = [...sequence.slice(offset), ...sequence.slice(0, offset)].join("\0");
2210
+ if (best === null || key2 < best) best = key2;
2211
+ }
2212
+ return best;
2213
+ }
2214
+ function cyclicTitleSpanningCandidates(entries, buckets) {
2215
+ const out = [];
2216
+ for (const indices of buckets.values()) {
2217
+ if (indices.length < 2) continue;
2218
+ const ordered = [...indices].sort(
2219
+ (a, b) => entries[a].memory.id.localeCompare(entries[b].memory.id) || entries[a].path.localeCompare(entries[b].path)
2011
2220
  );
2221
+ const anchor = ordered[0];
2222
+ for (const other of ordered.slice(1)) {
2223
+ out.push({
2224
+ candidate: pairCandidate("similar_title", entries[anchor], entries[other], {
2225
+ evidence: `titles overlap 100%: "${entries[anchor].memory.title}" / "${entries[other].memory.title}"`,
2226
+ suggested_action: "supersede"
2227
+ }),
2228
+ confidence: 2.01
2229
+ });
2230
+ }
2012
2231
  }
2013
2232
  return out;
2014
2233
  }
@@ -2038,12 +2257,221 @@ function sharedContext(entries, budget) {
2038
2257
  }
2039
2258
  for (const [i, j] of pairsFrom(byEntity, budget)) {
2040
2259
  const shared = [...entitySets.get(i)].filter((e) => entitySets.get(j).has(e));
2041
- out.push(
2042
- pairCandidate("shared_context", entries[i], entries[j], {
2260
+ out.push({
2261
+ candidate: pairCandidate("shared_context", entries[i], entries[j], {
2043
2262
  evidence: `both in scope "${scope}" and both about: ${shared.sort().join(", ")}`,
2044
2263
  suggested_action: "supersede"
2045
- })
2046
- );
2264
+ }),
2265
+ confidence: 2
2266
+ });
2267
+ }
2268
+ }
2269
+ return out;
2270
+ }
2271
+ function boundedSemanticSimilarities(entries, budget, source, globalExcluded) {
2272
+ const semanticBudget = {
2273
+ left: Math.min(budget.left, SEMANTIC_PAIR_BUDGET),
2274
+ scanned: 0,
2275
+ truncated: false
2276
+ };
2277
+ const out = semanticSimilarities(entries, semanticBudget, source, globalExcluded);
2278
+ budget.left -= semanticBudget.scanned;
2279
+ budget.scanned += semanticBudget.scanned;
2280
+ budget.truncated ||= semanticBudget.truncated;
2281
+ return out;
2282
+ }
2283
+ function semanticSimilarities(entries, budget, source, globalExcluded) {
2284
+ const buckets = /* @__PURE__ */ new Map();
2285
+ const scopes = entries.map(declaredSemanticScope);
2286
+ const entities = entries.map(
2287
+ (entry) => new Set(entry.memory.entities.map((entity) => normalizeContext(entity)).filter(isPresent))
2288
+ );
2289
+ entries.forEach((entry, i) => {
2290
+ if (!source.has(entry.memory.id)) return;
2291
+ const scope = scopes[i];
2292
+ if (scope !== null) addToBucket(buckets, `scope:${scope}`, i);
2293
+ for (const entity of entities[i]) addToBucket(buckets, `entity:${entity}`, i);
2294
+ if (scope === null && entities[i].size === 0 && !globalExcluded.has(entry.memory.id)) {
2295
+ addToBucket(buckets, "global:unanchored", i);
2296
+ }
2297
+ });
2298
+ const observations = [];
2299
+ const byIndex = /* @__PURE__ */ new Map();
2300
+ for (const [i, j] of pairsFrom(buckets, budget)) {
2301
+ const sharedScope = scopes[i] !== null && scopes[i] === scopes[j] ? scopes[i] : null;
2302
+ const sharedEntities = [...entities[i]].filter((entity) => entities[j].has(entity)).sort();
2303
+ const contextual = sharedScope !== null || sharedEntities.length > 0;
2304
+ const raw = source.compare(entries[i].memory.id, entries[j].memory.id);
2305
+ if (raw === null || raw === void 0 || !Number.isFinite(raw)) continue;
2306
+ const score = Math.max(-1, Math.min(1, raw));
2307
+ const threshold = sharedScope !== null ? SAME_SCOPE_SEMANTIC_SIMILARITY : sharedEntities.length > 0 ? SHARED_ENTITY_SEMANTIC_SIMILARITY : GLOBAL_SEMANTIC_SIMILARITY;
2308
+ if (score < threshold) continue;
2309
+ const observation = {
2310
+ i,
2311
+ j,
2312
+ key: indexPairKey(i, j),
2313
+ score,
2314
+ context: !contextual ? "with no lexical context, corroborated by a three-note semantic cluster" : sharedScope === null ? `inside shared entity ${sharedEntities.join(", ")}` : `inside scope "${entries[i].memory.scope ?? sharedScope}"`,
2315
+ contextual
2316
+ };
2317
+ observations.push(observation);
2318
+ addObservation(byIndex, i, observation);
2319
+ addObservation(byIndex, j, observation);
2320
+ }
2321
+ if (budget.truncated) return [];
2322
+ const nearest = /* @__PURE__ */ new Map();
2323
+ for (const [i, adjacent] of byIndex) {
2324
+ const ranked = adjacent.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key)).slice(0, RECIPROCAL_NEIGHBOURS);
2325
+ nearest.set(i, new Set(ranked.map((observation) => observation.key)));
2326
+ }
2327
+ const reciprocal = observations.filter(
2328
+ (observation) => nearest.get(observation.i)?.has(observation.key) === true && nearest.get(observation.j)?.has(observation.key) === true
2329
+ );
2330
+ const corroborated = triangleEdges(reciprocal);
2331
+ return reciprocal.filter((observation) => corroborated.has(observation.key)).map((observation) => ({
2332
+ candidate: pairCandidate(
2333
+ "semantic_similarity",
2334
+ entries[observation.i],
2335
+ entries[observation.j],
2336
+ {
2337
+ evidence: `indexed meaning is ${Math.round(observation.score * 100)}% similar ${observation.context} (reciprocal top-${RECIPROCAL_NEIGHBOURS}, three-note cluster)`,
2338
+ suggested_action: "supersede"
2339
+ }
2340
+ ),
2341
+ // Unanchored embedding evidence deliberately waits behind authored
2342
+ // context and strict lexical matches even after triangle corroboration.
2343
+ confidence: observation.contextual ? 3 + observation.score : 1.9
2344
+ }));
2345
+ }
2346
+ function triangleEdges(observations) {
2347
+ const adjacent = /* @__PURE__ */ new Map();
2348
+ for (const observation of observations) {
2349
+ addNeighbour(adjacent, observation.i, observation.j);
2350
+ addNeighbour(adjacent, observation.j, observation.i);
2351
+ }
2352
+ const out = /* @__PURE__ */ new Set();
2353
+ for (const observation of observations) {
2354
+ const left = adjacent.get(observation.i) ?? /* @__PURE__ */ new Set();
2355
+ const right = adjacent.get(observation.j) ?? /* @__PURE__ */ new Set();
2356
+ if ([...left].some((index) => index !== observation.j && right.has(index))) {
2357
+ out.add(observation.key);
2358
+ }
2359
+ }
2360
+ return out;
2361
+ }
2362
+ function addNeighbour(values, from, to) {
2363
+ const adjacent = values.get(from);
2364
+ if (adjacent) adjacent.add(to);
2365
+ else values.set(from, /* @__PURE__ */ new Set([to]));
2366
+ }
2367
+ function semanticGlobalExclusions(entries, titleCandidates) {
2368
+ const out = /* @__PURE__ */ new Set();
2369
+ const liveIds = new Set(entries.map((entry) => entry.memory.id));
2370
+ for (const entry of entries) {
2371
+ if (entry.relations.length === 0) continue;
2372
+ out.add(entry.memory.id);
2373
+ for (const relation of entry.relations) if (liveIds.has(relation.to)) out.add(relation.to);
2374
+ }
2375
+ for (const { candidate } of titleCandidates) {
2376
+ out.add(candidate.memory_id);
2377
+ if (candidate.other_id !== null) out.add(candidate.other_id);
2378
+ }
2379
+ return out;
2380
+ }
2381
+ function addObservation(byIndex, index, observation) {
2382
+ const values = byIndex.get(index);
2383
+ if (values) values.push(observation);
2384
+ else byIndex.set(index, [observation]);
2385
+ }
2386
+ function declaredSupersessions(entries) {
2387
+ const byId = new Map(entries.map((entry) => [entry.memory.id, entry]));
2388
+ const cyclicEdges = cyclicSupersessionEdges(entries, byId);
2389
+ const seen = /* @__PURE__ */ new Set();
2390
+ const out = [];
2391
+ for (const winner of entries) {
2392
+ for (const relation of winner.relations) {
2393
+ if (relation.type !== "supersedes") continue;
2394
+ const loser = byId.get(relation.to);
2395
+ if (!loser || loser.memory.id === winner.memory.id) continue;
2396
+ if (cyclicEdges.has(directedPairKey(winner.memory.id, loser.memory.id))) continue;
2397
+ const key2 = memoryPairKey(winner.memory.id, loser.memory.id);
2398
+ if (seen.has(key2)) continue;
2399
+ seen.add(key2);
2400
+ out.push({
2401
+ candidate: pairCandidate("declared_supersession", loser, winner, {
2402
+ evidence: `"${winner.memory.title}" already declares it supersedes "${loser.memory.title}", but the replaced note is still active`,
2403
+ suggested_action: "retire"
2404
+ }),
2405
+ confidence: 6
2406
+ });
2407
+ }
2408
+ }
2409
+ return out;
2410
+ }
2411
+ function cyclicSupersessionEdges(entries, byId) {
2412
+ const adjacent = /* @__PURE__ */ new Map();
2413
+ const reversed = /* @__PURE__ */ new Map();
2414
+ for (const id of byId.keys()) {
2415
+ adjacent.set(id, []);
2416
+ reversed.set(id, []);
2417
+ }
2418
+ for (const entry of entries) {
2419
+ for (const relation of entry.relations) {
2420
+ if (relation.type !== "supersedes" || relation.to === entry.memory.id) continue;
2421
+ if (!byId.has(relation.to)) continue;
2422
+ adjacent.get(entry.memory.id).push(relation.to);
2423
+ reversed.get(relation.to).push(entry.memory.id);
2424
+ }
2425
+ }
2426
+ const visited = /* @__PURE__ */ new Set();
2427
+ const finished = [];
2428
+ for (const start of byId.keys()) {
2429
+ if (visited.has(start)) continue;
2430
+ visited.add(start);
2431
+ const stack = [{ id: start, next: 0 }];
2432
+ while (stack.length > 0) {
2433
+ const frame = stack[stack.length - 1];
2434
+ const neighbours = adjacent.get(frame.id) ?? [];
2435
+ if (frame.next < neighbours.length) {
2436
+ const neighbour = neighbours[frame.next];
2437
+ frame.next += 1;
2438
+ if (!visited.has(neighbour)) {
2439
+ visited.add(neighbour);
2440
+ stack.push({ id: neighbour, next: 0 });
2441
+ }
2442
+ } else {
2443
+ finished.push(frame.id);
2444
+ stack.pop();
2445
+ }
2446
+ }
2447
+ }
2448
+ const componentById = /* @__PURE__ */ new Map();
2449
+ const componentSizes = /* @__PURE__ */ new Map();
2450
+ let component = 0;
2451
+ for (let index = finished.length - 1; index >= 0; index -= 1) {
2452
+ const start = finished[index];
2453
+ if (componentById.has(start)) continue;
2454
+ const stack = [start];
2455
+ componentById.set(start, component);
2456
+ let size = 0;
2457
+ while (stack.length > 0) {
2458
+ const id = stack.pop();
2459
+ size += 1;
2460
+ for (const neighbour of reversed.get(id) ?? []) {
2461
+ if (componentById.has(neighbour)) continue;
2462
+ componentById.set(neighbour, component);
2463
+ stack.push(neighbour);
2464
+ }
2465
+ }
2466
+ componentSizes.set(component, size);
2467
+ component += 1;
2468
+ }
2469
+ const out = /* @__PURE__ */ new Set();
2470
+ for (const [from, targets] of adjacent) {
2471
+ const fromComponent = componentById.get(from);
2472
+ if (fromComponent === void 0 || (componentSizes.get(fromComponent) ?? 0) < 2) continue;
2473
+ for (const to of targets) {
2474
+ if (componentById.get(to) === fromComponent) out.add(directedPairKey(from, to));
2047
2475
  }
2048
2476
  }
2049
2477
  return out;
@@ -2057,15 +2485,16 @@ function declaredContradictions(entries) {
2057
2485
  if (relation.type !== "contradicts") continue;
2058
2486
  const other = byId.get(relation.to);
2059
2487
  if (!other || other.memory.id === entry.memory.id) continue;
2060
- const key2 = [entry.memory.id, other.memory.id].sort().join("\0");
2488
+ const key2 = memoryPairKey(entry.memory.id, other.memory.id);
2061
2489
  if (seen.has(key2)) continue;
2062
2490
  seen.add(key2);
2063
- out.push(
2064
- pairCandidate("contradiction", entry, other, {
2491
+ out.push({
2492
+ candidate: pairCandidate("contradiction", entry, other, {
2065
2493
  evidence: `"${entry.memory.title}" declares it contradicts "${other.memory.title}" \u2014 recorded, never settled`,
2066
2494
  suggested_action: "supersede"
2067
- })
2068
- );
2495
+ }),
2496
+ confidence: 5
2497
+ });
2069
2498
  }
2070
2499
  }
2071
2500
  return out;
@@ -2076,20 +2505,88 @@ function kindPromotions(entries) {
2076
2505
  if (entry.memory.binding !== true) continue;
2077
2506
  if (entry.memory.kind === "decision") continue;
2078
2507
  out.push({
2079
- rule: "kind_promotion",
2080
- memory_id: entry.memory.id,
2081
- path: entry.path,
2082
- title: entry.memory.title,
2083
- other_id: null,
2084
- other_path: null,
2085
- other_title: null,
2086
- evidence: entry.memory.kind === void 0 ? "marked as binding but left unclassified \u2014 a memory that binds is a decision" : `marked as binding but classified as ${entry.memory.kind} \u2014 a memory that binds is a decision`,
2087
- suggested_action: "reclassify",
2088
- suggested_kind: "decision"
2508
+ candidate: {
2509
+ rule: "kind_promotion",
2510
+ memory_id: entry.memory.id,
2511
+ path: entry.path,
2512
+ title: entry.memory.title,
2513
+ other_id: null,
2514
+ other_path: null,
2515
+ other_title: null,
2516
+ evidence: entry.memory.kind === void 0 ? "marked as binding but left unclassified \u2014 a memory that binds is a decision" : `marked as binding but classified as ${entry.memory.kind} \u2014 a memory that binds is a decision`,
2517
+ suggested_action: "reclassify",
2518
+ suggested_kind: "decision"
2519
+ },
2520
+ confidence: 4
2089
2521
  });
2090
2522
  }
2091
2523
  return out;
2092
2524
  }
2525
+ function normalizeContext(value) {
2526
+ if (value === null) return null;
2527
+ const normalized = value.normalize("NFC").trim().toLowerCase();
2528
+ return normalized === "" ? null : normalized;
2529
+ }
2530
+ function declaredSemanticScope(entry) {
2531
+ const scope = normalizeContext(entry.memory.scope);
2532
+ if (scope === null) return null;
2533
+ if (entry.memory.provenance.scopeSource === "explicit") return scope;
2534
+ if (entry.memory.provenance.scopeSource === "path") return null;
2535
+ const parts = entry.path.split("/");
2536
+ const parent = parts.length >= 2 ? normalizeContext(parts[parts.length - 2] ?? null) : null;
2537
+ return scope === parent ? null : scope;
2538
+ }
2539
+ function isPresent(value) {
2540
+ return value !== null;
2541
+ }
2542
+ function addToBucket(buckets, key2, index) {
2543
+ const bucket = buckets.get(key2);
2544
+ if (bucket) bucket.push(index);
2545
+ else buckets.set(key2, [index]);
2546
+ }
2547
+ function memoryPairKey(left, right) {
2548
+ return left < right ? `${left}\0${right}` : `${right}\0${left}`;
2549
+ }
2550
+ function directedPairKey(from, to) {
2551
+ return `${from}\0${to}`;
2552
+ }
2553
+ function indexPairKey(left, right) {
2554
+ return left < right ? `${left}:${right}` : `${right}:${left}`;
2555
+ }
2556
+ function rankCandidates(scored) {
2557
+ const unique = /* @__PURE__ */ new Map();
2558
+ for (const item of scored) {
2559
+ const candidate = item.candidate;
2560
+ const key2 = candidate.other_id === null ? `single:${candidate.rule}:${candidate.memory_id}` : `pair:${memoryPairKey(candidate.memory_id, candidate.other_id)}`;
2561
+ const current = unique.get(key2);
2562
+ if (current === void 0 || compareScored(item, current) < 0) unique.set(key2, item);
2563
+ }
2564
+ const ordered = [...unique.values()].sort(compareScored);
2565
+ const tiers = /* @__PURE__ */ new Map();
2566
+ for (const item of ordered) addScored(tiers, Math.floor(item.confidence), item);
2567
+ const components = new CandidateComponents();
2568
+ const out = [];
2569
+ for (const tier of [...tiers.keys()].sort((a, b) => b - a)) {
2570
+ const useful = [];
2571
+ const cycles = [];
2572
+ for (const item of tiers.get(tier) ?? []) {
2573
+ const other = item.candidate.other_id;
2574
+ if (other === null || components.join(item.candidate.memory_id, other)) useful.push(item);
2575
+ else cycles.push(item);
2576
+ }
2577
+ for (const item of useful) out.push(item.candidate);
2578
+ for (const item of cycles) out.push(item.candidate);
2579
+ }
2580
+ return out;
2581
+ }
2582
+ function addScored(tiers, tier, item) {
2583
+ const values = tiers.get(tier);
2584
+ if (values) values.push(item);
2585
+ else tiers.set(tier, [item]);
2586
+ }
2587
+ function compareScored(a, b) {
2588
+ return b.confidence - a.confidence || CONSOLIDATION_RULES.indexOf(a.candidate.rule) - CONSOLIDATION_RULES.indexOf(b.candidate.rule) || a.candidate.memory_id.localeCompare(b.candidate.memory_id) || (a.candidate.other_id ?? "").localeCompare(b.candidate.other_id ?? "");
2589
+ }
2093
2590
  function pairCandidate(rule, a, b, rest) {
2094
2591
  return {
2095
2592
  rule,
@@ -2125,20 +2622,54 @@ function* pairsFrom(buckets, budget) {
2125
2622
  }
2126
2623
  }
2127
2624
  }
2128
- var CONSOLIDATION_RULES, DEFAULT_LIMIT, DEFAULT_PAIR_BUDGET, TITLE_OVERLAP;
2625
+ var CONSOLIDATION_RULES, DEFAULT_LIMIT, DEFAULT_PAIR_BUDGET, SEMANTIC_PAIR_BUDGET, TITLE_OVERLAP, SAME_SCOPE_SEMANTIC_SIMILARITY, SHARED_ENTITY_SEMANTIC_SIMILARITY, GLOBAL_SEMANTIC_SIMILARITY, RECIPROCAL_NEIGHBOURS, CandidateComponents;
2129
2626
  var init_detect = __esm({
2130
2627
  "../../packages/memory-engine/src/proposal/detect.ts"() {
2131
2628
  "use strict";
2132
2629
  init_search();
2133
2630
  CONSOLIDATION_RULES = [
2134
- "similar_title",
2135
- "shared_context",
2631
+ "declared_supersession",
2136
2632
  "contradiction",
2633
+ "semantic_similarity",
2634
+ "shared_context",
2635
+ "similar_title",
2137
2636
  "kind_promotion"
2138
2637
  ];
2139
2638
  DEFAULT_LIMIT = 50;
2140
2639
  DEFAULT_PAIR_BUDGET = 2e5;
2141
- TITLE_OVERLAP = 0.6;
2640
+ SEMANTIC_PAIR_BUDGET = 5e4;
2641
+ TITLE_OVERLAP = 2 / 3;
2642
+ SAME_SCOPE_SEMANTIC_SIMILARITY = 0.6;
2643
+ SHARED_ENTITY_SEMANTIC_SIMILARITY = 0.75;
2644
+ GLOBAL_SEMANTIC_SIMILARITY = 0.72;
2645
+ RECIPROCAL_NEIGHBOURS = 2;
2646
+ CandidateComponents = class {
2647
+ parent = /* @__PURE__ */ new Map();
2648
+ root(id) {
2649
+ const trail = [];
2650
+ let current = id;
2651
+ for (; ; ) {
2652
+ const parent = this.parent.get(current);
2653
+ if (parent === void 0) {
2654
+ this.parent.set(current, current);
2655
+ break;
2656
+ }
2657
+ if (parent === current) break;
2658
+ trail.push(current);
2659
+ current = parent;
2660
+ }
2661
+ for (const member of trail) this.parent.set(member, current);
2662
+ return current;
2663
+ }
2664
+ /** True when this edge connected two components rather than closing a cycle. */
2665
+ join(left, right) {
2666
+ const a = this.root(left);
2667
+ const b = this.root(right);
2668
+ if (a === b) return false;
2669
+ this.parent.set(b, a);
2670
+ return true;
2671
+ }
2672
+ };
2142
2673
  }
2143
2674
  });
2144
2675
 
@@ -2660,6 +3191,86 @@ function restoreUntouchedLines(original, rendered, changed) {
2660
3191
  return before.get(key2) ?? line;
2661
3192
  }).join("\n");
2662
3193
  }
3194
+ function yamlErrorLine(error, text) {
3195
+ const message = error instanceof Error ? error.message : String(error);
3196
+ let positioned = null;
3197
+ if (error && typeof error === "object") {
3198
+ const linePos = error.linePos;
3199
+ const line = linePos?.[0]?.line;
3200
+ if (typeof line === "number" && Number.isFinite(line) && line >= 1) positioned = line - 1;
3201
+ }
3202
+ const quote = /Missing closing (["'])quote/i.exec(message)?.[1];
3203
+ if (quote !== void 0) {
3204
+ const lines2 = text.split("\n");
3205
+ const end = positioned ?? lines2.length - 1;
3206
+ for (let i = 0; i <= end; i += 1) {
3207
+ const line = lines2[i] ?? "";
3208
+ const colon = line.indexOf(":");
3209
+ if (colon === -1) continue;
3210
+ const value = line.slice(colon + 1);
3211
+ let count = 0;
3212
+ for (let at = 0; at < value.length; at += 1) {
3213
+ if (value[at] !== quote) continue;
3214
+ let slashes = 0;
3215
+ for (let before = at - 1; before >= 0 && value[before] === "\\"; before -= 1) slashes += 1;
3216
+ if (slashes % 2 === 0) count += 1;
3217
+ }
3218
+ if (count % 2 === 1) return i;
3219
+ }
3220
+ }
3221
+ if (positioned !== null) return positioned;
3222
+ const namedLine = /\bat line (\d+)\b/i.exec(message)?.[1];
3223
+ if (namedLine !== void 0) return Math.max(0, Number(namedLine) - 1);
3224
+ const alias = /alias[^:]*:\s*([^\s]+)/i.exec(message)?.[1];
3225
+ if (alias !== void 0) {
3226
+ const found = text.split("\n").findIndex((line) => line.includes(`*${alias}`));
3227
+ if (found !== -1) return found;
3228
+ }
3229
+ return 0;
3230
+ }
3231
+ function yamlFieldAt(text, line, fallback) {
3232
+ const stack = [];
3233
+ const lines2 = text.split("\n");
3234
+ const end = Math.min(Math.max(line, 0), Math.max(lines2.length - 1, 0));
3235
+ for (let i = 0; i <= end; i += 1) {
3236
+ const match = ANY_INDENTED_KEY_RE.exec(lines2[i] ?? "");
3237
+ if (!match) continue;
3238
+ const whitespace = match[1] ?? "";
3239
+ const indent = whitespace.length;
3240
+ while ((stack.at(-1)?.indent ?? -1) >= indent) stack.pop();
3241
+ stack.push({ indent, key: (match[2] ?? "").trim() });
3242
+ }
3243
+ return stack.length > 0 ? stack.map(({ key: key2 }) => key2).join(".") : fallback;
3244
+ }
3245
+ function yamlFieldRefusal(text, error, fallback) {
3246
+ const message = error instanceof Error ? error.message : String(error);
3247
+ const localField = yamlFieldAt(text, yamlErrorLine(error, text), fallback);
3248
+ const field = fallback === NAMESPACE2 && localField !== NAMESPACE2 ? `${NAMESPACE2}.${localField}` : localField;
3249
+ const code = error && typeof error === "object" && typeof error.code === "string" ? error.code : "";
3250
+ let change;
3251
+ if (code === "TAB_AS_INDENT" || /tab/i.test(message)) {
3252
+ change = `Replace the tab indentation in ${field} with spaces`;
3253
+ } else if (/alias|anchor/i.test(message)) {
3254
+ change = `Replace the anchor or alias used by ${field} with its value inline`;
3255
+ } else if (code === "DUPLICATE_KEY" || /keys? must be unique/i.test(message)) {
3256
+ change = `Remove or rename the duplicate ${field} field`;
3257
+ } else {
3258
+ change = `Fix ${field} so it is valid YAML (close its quotes or brackets, and quote any value containing a colon)`;
3259
+ }
3260
+ return new PatchError(
3261
+ `the ${field} field could not be rewritten safely: ${message}`,
3262
+ `${change}, then try again. Nothing was written.`
3263
+ );
3264
+ }
3265
+ function assertReadableFrontmatter(frontmatter) {
3266
+ try {
3267
+ const doc = parseDocument(frontmatter);
3268
+ if (doc.errors[0]) throw doc.errors[0];
3269
+ doc.toJS();
3270
+ } catch (error) {
3271
+ throw yamlFieldRefusal(frontmatter, error, "frontmatter");
3272
+ }
3273
+ }
2663
3274
  function renderBlock(fields, existingText) {
2664
3275
  let body;
2665
3276
  if (existingText === null || existingText.trim() === "") {
@@ -2667,6 +3278,7 @@ function renderBlock(fields, existingText) {
2667
3278
  } else {
2668
3279
  try {
2669
3280
  const doc = parseDocument(existingText);
3281
+ if (doc.errors[0]) throw doc.errors[0];
2670
3282
  const changed = /* @__PURE__ */ new Set();
2671
3283
  for (const [key2, value] of Object.entries(fields)) {
2672
3284
  const current = doc.get(key2);
@@ -2680,14 +3292,21 @@ function renderBlock(fields, existingText) {
2680
3292
  }
2681
3293
  body = restoreUntouchedLines(
2682
3294
  existingText,
2683
- doc.toString({ lineWidth: 0 }).replace(/\n$/, ""),
3295
+ doc.toString({
3296
+ lineWidth: 0,
3297
+ // `yaml` turns a double-quoted scalar containing `\n` into three
3298
+ // physical lines once its JSON representation reaches 40 chars.
3299
+ // `restoreUntouchedLines` then sees the blank middle line, mistakes
3300
+ // the first one for a complete scalar, and restores it while
3301
+ // leaving the continuation behind. Keeping quoted newlines escaped
3302
+ // makes an untouched field one physical line on both sides, so the
3303
+ // byte-preserving restore and the YAML renderer agree on its span.
3304
+ doubleQuotedMinMultiLineLength: Number.POSITIVE_INFINITY
3305
+ }).replace(/\n$/, ""),
2684
3306
  changed
2685
3307
  );
2686
3308
  } catch (err) {
2687
- throw new PatchError(
2688
- `the memgineering block in this note could not be rewritten: ${err.message}`,
2689
- "Its YAML uses something this tool cannot rewrite safely \u2014 most often a tab indent, or an anchor/alias defined outside the block. Re-indent with spaces or inline the value, then try again. Nothing was written."
2690
- );
3309
+ throw yamlFieldRefusal(existingText, err, NAMESPACE2);
2691
3310
  }
2692
3311
  }
2693
3312
  return [`${NAMESPACE2}:`, ...body.split("\n").map((l) => l === "" ? l : ` ${l}`)];
@@ -2717,6 +3336,7 @@ ${block.join("\n")}
2717
3336
  };
2718
3337
  }
2719
3338
  const fenceBody = fenceMatch[1] ?? "";
3339
+ assertReadableFrontmatter(fenceBody);
2720
3340
  const fenceLines = splitLines(fenceBody);
2721
3341
  const span = findBlock(fenceLines);
2722
3342
  const existing = readNamespace(noteContent);
@@ -2882,7 +3502,7 @@ function reversePatch(content, patch) {
2882
3502
  }
2883
3503
  return null;
2884
3504
  }
2885
- var NAMESPACE2, FENCE_RE, LOOKS_FENCED_RE, PatchError, BLOCK_KEY_RE;
3505
+ var NAMESPACE2, FENCE_RE, LOOKS_FENCED_RE, PatchError, BLOCK_KEY_RE, ANY_INDENTED_KEY_RE;
2886
3506
  var init_patch = __esm({
2887
3507
  "../../packages/memory-engine/src/proposal/patch.ts"() {
2888
3508
  "use strict";
@@ -2900,6 +3520,7 @@ var init_patch = __esm({
2900
3520
  }
2901
3521
  };
2902
3522
  BLOCK_KEY_RE = /^([^\s#][^:]*?)\s*:/;
3523
+ ANY_INDENTED_KEY_RE = /^(\s*)([^\s#][^:]*?)\s*:/;
2903
3524
  }
2904
3525
  });
2905
3526
 
@@ -4430,13 +5051,17 @@ function linkCommand() {
4430
5051
  Nothing was indexed. This is what WOULD be stored:
4431
5052
  `));
4432
5053
  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)}`));
5054
+ printHuman(` ${c.bold(oneLine(r.title, WIDTH.title))}`);
5055
+ printHuman(
5056
+ c.gray(` ${oneLine(r.summary ?? "(no summary \u2014 title only)", WIDTH.text)}`)
5057
+ );
5058
+ printHuman(c.gray(` from ${middleLine(r.path, WIDTH.path)}`));
4436
5059
  }
4437
5060
  printHuman(
4438
- c.gray(`
4439
- ${rows.length} note(s). Run without --dry-run to index them.`)
5061
+ c.gray(
5062
+ `
5063
+ ${rows.length} note(s). After the person approves this preview, run again with --yes to index them.`
5064
+ )
4440
5065
  );
4441
5066
  if (refusedWhilePreviewing.length > 0) {
4442
5067
  printHuman(
@@ -4705,8 +5330,8 @@ Brain: ${root}`));
4705
5330
  if (samples.length > 0) {
4706
5331
  printHuman(c.gray("\n For example, these exact lines would be stored:"));
4707
5332
  for (const r of samples) {
4708
- printHuman(c.gray(` ${stripControl(r.title)}`));
4709
- printHuman(c.gray(` ${stripControl(r.summary ?? "(no summary \u2014 title only)")}`));
5333
+ printHuman(c.gray(` ${oneLine(r.title, WIDTH.title)}`));
5334
+ printHuman(c.gray(` ${oneLine(r.summary ?? "(no summary \u2014 title only)", WIDTH.text)}`));
4710
5335
  }
4711
5336
  if (notes.length > samples.length) {
4712
5337
  printHuman(
@@ -4826,7 +5451,7 @@ function loginCommand() {
4826
5451
  },
4827
5452
  human: () => {
4828
5453
  printHuman(
4829
- `Already signed in${already.display_name ? ` as ${c.bold(stripControl(already.display_name))}` : ""}.`
5454
+ `Already signed in${already.display_name ? ` as ${c.bold(oneLine(already.display_name, WIDTH.title))}` : ""}.`
4830
5455
  );
4831
5456
  printHuman(c.gray(" `memgineering logout` first if you want a different account."));
4832
5457
  }
@@ -4899,13 +5524,13 @@ function emit(pending, ctx) {
4899
5524
  },
4900
5525
  human: () => {
4901
5526
  printHuman("Show this code to whoever is signing in:\n");
4902
- printHuman(` ${c.bold().yellow(stripControl(pending.user_code))}
5527
+ printHuman(` ${c.bold().yellow(oneLine(pending.user_code, WIDTH.handle))}
4903
5528
  `);
4904
5529
  if (ctx.opened) {
4905
5530
  printHuman("Opened the approval page in your browser.");
4906
5531
  } else {
4907
5532
  printHuman("Open this to approve:\n");
4908
- printHuman(` ${c.cyan(stripControl(pending.verification_url))}`);
5533
+ printHuman(` ${c.cyan(oneLine(pending.verification_url, WIDTH.path))}`);
4909
5534
  }
4910
5535
  printHuman(
4911
5536
  c.gray(
@@ -4913,7 +5538,7 @@ function emit(pending, ctx) {
4913
5538
  )
4914
5539
  );
4915
5540
  if (ctx.opened && ctx.showUrl) {
4916
- printHuman(c.gray(` ${stripControl(pending.verification_url)}`));
5541
+ printHuman(c.gray(` ${oneLine(pending.verification_url, WIDTH.path)}`));
4917
5542
  }
4918
5543
  printHuman(
4919
5544
  c.gray(
@@ -4955,7 +5580,7 @@ async function waitForApproval(pending, base) {
4955
5580
  human: () => {
4956
5581
  printHuman(
4957
5582
  `
4958
- Signed in${claimed.display_name ? ` as ${c.bold(stripControl(claimed.display_name))}` : ""}.`
5583
+ Signed in${claimed.display_name ? ` as ${c.bold(oneLine(claimed.display_name, WIDTH.title))}` : ""}.`
4959
5584
  );
4960
5585
  printHuman(c.gray(" The token is stored on this machine only, readable by you alone."));
4961
5586
  }
@@ -5056,17 +5681,19 @@ function printSignInStep(step) {
5056
5681
  switch (step.status) {
5057
5682
  case "signed-in":
5058
5683
  printHuman(
5059
- c.gray(` signed in${step.displayName ? ` as ${stripControl(step.displayName)}` : ""}
5060
- `)
5684
+ c.gray(
5685
+ ` signed in${step.displayName ? ` as ${oneLine(step.displayName, WIDTH.title)}` : ""}
5686
+ `
5687
+ )
5061
5688
  );
5062
5689
  return;
5063
5690
  case "started":
5064
5691
  printHuman("Show this code to whoever is signing in:\n");
5065
- printHuman(` ${c.bold().yellow(stripControl(step.userCode))}
5692
+ printHuman(` ${c.bold().yellow(oneLine(step.userCode, WIDTH.handle))}
5066
5693
  `);
5067
5694
  printHuman(
5068
5695
  step.opened ? c.gray(" The approval page is open in the browser.") : `Open this to approve:
5069
- ${c.cyan(stripControl(step.verificationUrl))}`
5696
+ ${c.cyan(oneLine(step.verificationUrl, WIDTH.path))}`
5070
5697
  );
5071
5698
  printHuman(
5072
5699
  c.gray(" Not waiting \u2014 setup carries on, and the next command collects the token.\n")
@@ -5074,12 +5701,14 @@ function printSignInStep(step) {
5074
5701
  return;
5075
5702
  case "pending":
5076
5703
  printHuman(
5077
- c.gray(` a sign-in is waiting for approval \u2014 code ${stripControl(step.userCode)}
5078
- `)
5704
+ c.gray(
5705
+ ` a sign-in is waiting for approval \u2014 code ${oneLine(step.userCode, WIDTH.handle)}
5706
+ `
5707
+ )
5079
5708
  );
5080
5709
  return;
5081
5710
  case "unavailable":
5082
- printHuman(c.yellow(` Could not start a sign-in: ${stripControl(step.because)}`));
5711
+ printHuman(c.yellow(` Could not start a sign-in: ${oneLine(step.because, WIDTH.text)}`));
5083
5712
  printHuman(c.gray(" Setup continues \u2014 an account is not needed for any of it.\n"));
5084
5713
  return;
5085
5714
  case "would-start":
@@ -7387,11 +8016,11 @@ var init_setup_web = __esm({
7387
8016
  // src/commands/setup.ts
7388
8017
  import { createHash as createHash8 } from "crypto";
7389
8018
  import { access, mkdir as mkdir13, readFile as readFile19 } from "fs/promises";
7390
- import { homedir as homedir4 } from "os";
7391
8019
  import { dirname as dirname5, join as join19 } from "path";
7392
8020
  import { fileURLToPath } from "url";
7393
8021
  import { Command as Command22 } from "commander";
7394
8022
  import prompts3 from "prompts";
8023
+ import { homedir as homedir4 } from "os";
7395
8024
  function setupCommand() {
7396
8025
  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
8026
  "--tools <list>",
@@ -7463,6 +8092,18 @@ function setupCommand() {
7463
8092
  ...signInStepJson(signIn),
7464
8093
  defaults_used: defaultsTaken,
7465
8094
  dry_run: opts.dryRun === true,
8095
+ /*
8096
+ * The home these paths are relative TO, and it is only here when it
8097
+ * is not the ordinary one.
8098
+ *
8099
+ * Every path in `changes` is reported relative to the home being
8100
+ * installed into, so `.claude/skills/…` names a file without saying
8101
+ * whose. That ambiguity is why an agent writing into a real machine
8102
+ * from inside a sandbox had nothing in front of it that looked
8103
+ * wrong. Present only when scoped, so the common case stays quiet
8104
+ * and the unusual one cannot be mistaken for it.
8105
+ */
8106
+ ...agentHome() !== homedir4() ? { agent_home: agentHome() } : {},
7466
8107
  tools: choice.tools.map((t) => t.id),
7467
8108
  auto_update: choice.autoUpdate,
7468
8109
  hook: choice.hook,
@@ -7486,9 +8127,27 @@ function setupCommand() {
7486
8127
  if (opts.silent && changed.length === 0) return;
7487
8128
  if (opts.dryRun) {
7488
8129
  printSignInStep(signIn);
7489
- printHuman(`Would change ${changed.length} file(s):
8130
+ if (changed.length === 0) {
8131
+ printHuman(
8132
+ `Nothing to change \u2014 ${choice.tools.map((t) => t.label).join(", ")} ${choice.tools.length === 1 ? "is" : "are"} already up to date.
8133
+ `
8134
+ );
8135
+ printHuman(
8136
+ c.gray(
8137
+ ` ${results.length} file(s) checked. Run without \`--dry-run\` to reinstall them anyway.`
8138
+ )
8139
+ );
8140
+ return;
8141
+ }
8142
+ printHuman(`Would change ${changed.length} of ${results.length} file(s):
7490
8143
  `);
7491
8144
  for (const r of changed) printHuman(c.gray(` ${r.tool} \xB7 ${r.what}`));
8145
+ printHuman(
8146
+ c.gray(
8147
+ `
8148
+ The rest are already up to date. Tools found: ${choice.tools.map((t) => t.label).join(", ")}.`
8149
+ )
8150
+ );
7492
8151
  return;
7493
8152
  }
7494
8153
  printSignInStep(signIn);
@@ -7589,7 +8248,7 @@ async function applySetup(choice, dryRun) {
7589
8248
  const hub = await loadAsset(BRAND.hubFileName);
7590
8249
  const installed = { ...(await loadConfig()).system.installed };
7591
8250
  const mark = (path, hash) => {
7592
- const key2 = path.startsWith(homedir4()) ? path.slice(homedir4().length + 1) : path;
8251
+ const key2 = path.startsWith(agentHome()) ? path.slice(agentHome().length + 1) : path;
7593
8252
  installed[key2] = hash;
7594
8253
  };
7595
8254
  const persist = async () => {
@@ -7601,11 +8260,11 @@ async function applySetup(choice, dryRun) {
7601
8260
  });
7602
8261
  };
7603
8262
  const wasEdited = (path, onDisk) => {
7604
- const key2 = path.startsWith(homedir4()) ? path.slice(homedir4().length + 1) : path;
8263
+ const key2 = path.startsWith(agentHome()) ? path.slice(agentHome().length + 1) : path;
7605
8264
  const known = installed[key2];
7606
8265
  return known !== void 0 && onDisk !== null && fingerprint(onDisk) !== known;
7607
8266
  };
7608
- const keyOf = (path) => path.startsWith(homedir4()) ? path.slice(homedir4().length + 1) : path;
8267
+ const keyOf = (path) => path.startsWith(agentHome()) ? path.slice(agentHome().length + 1) : path;
7609
8268
  try {
7610
8269
  return await write();
7611
8270
  } finally {
@@ -7613,7 +8272,7 @@ async function applySetup(choice, dryRun) {
7613
8272
  }
7614
8273
  async function write() {
7615
8274
  for (const tool of choice.tools) {
7616
- const hubPath = join19(homedir4(), tool.hubFile);
8275
+ const hubPath = join19(agentHome(), tool.hubFile);
7617
8276
  const written = await injectBlock(
7618
8277
  hubPath,
7619
8278
  hub + tailFor(tool),
@@ -7629,7 +8288,7 @@ async function applySetup(choice, dryRun) {
7629
8288
  if (!dryRun && written.after !== null) mark(hubPath, written.after);
7630
8289
  for (const skill of BRAND.subSkills) {
7631
8290
  const content = await loadAsset(join19(skill, "SKILL.md"));
7632
- const path = join19(homedir4(), tool.skillsDir, skill, "SKILL.md");
8291
+ const path = join19(agentHome(), tool.skillsDir, skill, "SKILL.md");
7633
8292
  const out = await writeIfDifferent(path, content, dryRun);
7634
8293
  results.push({
7635
8294
  tool: tool.label,
@@ -7699,7 +8358,7 @@ async function resolveMode(opts) {
7699
8358
  return { kind: mode ?? "agent", chosen: mode !== void 0 };
7700
8359
  }
7701
8360
  async function detectTools() {
7702
- const home = homedir4();
8361
+ const home = agentHome();
7703
8362
  const found = [];
7704
8363
  for (const tool of TOOLS) {
7705
8364
  if (await exists2(join19(home, tool.dir))) found.push(tool);
@@ -7832,7 +8491,7 @@ async function writeIfDifferent(filePath, content, dryRun) {
7832
8491
  return { outcome: existing === null ? "injected" : "updated", before: existing, after };
7833
8492
  }
7834
8493
  async function installHook(spec, tool, dryRun) {
7835
- const settingsPath = join19(homedir4(), ...(tool.hooksFile ?? "").split("/"));
8494
+ const settingsPath = join19(agentHome(), ...(tool.hooksFile ?? "").split("/"));
7836
8495
  const raw = await readFile19(settingsPath, "utf8").catch(() => null);
7837
8496
  let settings = {};
7838
8497
  if (raw !== null) {
@@ -7882,7 +8541,7 @@ async function installHook(spec, tool, dryRun) {
7882
8541
  return raw === null ? "injected" : "updated";
7883
8542
  }
7884
8543
  async function removeHook(spec, tool, dryRun) {
7885
- const settingsPath = join19(homedir4(), ...(tool.hooksFile ?? "").split("/"));
8544
+ const settingsPath = join19(agentHome(), ...(tool.hooksFile ?? "").split("/"));
7886
8545
  const raw = await readFile19(settingsPath, "utf8").catch(() => null);
7887
8546
  if (raw === null) return "unchanged";
7888
8547
  let parsed;
@@ -8093,7 +8752,7 @@ For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around t
8093
8752
  });
8094
8753
 
8095
8754
  // src/program.ts
8096
- import { Command as Command27 } from "commander";
8755
+ import { Command as Command28 } from "commander";
8097
8756
 
8098
8757
  // src/help.ts
8099
8758
  init_ui();
@@ -8117,7 +8776,7 @@ var COMMAND_GROUPS = [
8117
8776
  {
8118
8777
  title: "ACCOUNT",
8119
8778
  blurb: "optional \u2014 connect a hosted brain; everything above works without it",
8120
- commands: ["login", "logout", "whoami", "push", "pull"]
8779
+ commands: ["login", "logout", "whoami", "brains", "push", "pull"]
8121
8780
  },
8122
8781
  {
8123
8782
  title: "SYSTEM",
@@ -8130,7 +8789,12 @@ var ENV_ROWS = [
8130
8789
  ["MEMGINEERING_HOME=<dir>", "state + derived index (default: ~/.memgineering)"],
8131
8790
  ["MEMGINEERING_JSON=1", "force --json framing without the flag"],
8132
8791
  ["MEMGINEERING_NO_UPDATE=1", "skip the update check for this run"],
8133
- ["MEMGINEERING_API_URL=<url>", "hosted brain server (default: https://api.memgineering.com)"]
8792
+ ["MEMGINEERING_API_URL=<url>", "hosted brain server (default: https://api.memgineering.com)"],
8793
+ // The one that keeps `setup` off a real machine, and it was missing from this
8794
+ // list while the other four were here. An agent testing this CLI in a sandbox
8795
+ // had no documented handle for where `setup` writes — which is how one of them
8796
+ // overwrote the live skill files of the session testing it.
8797
+ ["MEMGINEERING_AGENT_HOME=<dir>", "where `setup` installs (default: your home)"]
8134
8798
  ];
8135
8799
  function commandLabel(sub) {
8136
8800
  const args = sub.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
@@ -8283,12 +8947,16 @@ var cloudRules = (brain, args = {}) => call(brain, "POST", "/rules", {
8283
8947
  var cloudReindex = (brain, args = {}) => call(brain, "POST", "/reindex", {
8284
8948
  ...args.rebuild === true ? { rebuild: true } : {}
8285
8949
  });
8286
- async function listCloudBrains(token, baseUrl) {
8950
+ function findCloudBrain(brains, named) {
8951
+ return brains.find((brain) => brain.id === named) ?? brains.find((brain) => brain.name === named);
8952
+ }
8953
+ async function listCloudBrains(token, baseUrl, opts = {}) {
8287
8954
  const res = await apiRequest({
8288
8955
  method: "GET",
8289
8956
  path: "/v1/brains",
8290
8957
  token,
8291
- baseUrl
8958
+ baseUrl,
8959
+ ...opts.retries === void 0 ? {} : { retries: opts.retries }
8292
8960
  });
8293
8961
  return res.body?.brains ?? [];
8294
8962
  }
@@ -8360,11 +9028,11 @@ function describeHeldBack(held) {
8360
9028
  return [
8361
9029
  ...heldBackGroups(held).flatMap(
8362
9030
  ({ label, rows }) => rows.map(
8363
- (r) => `${stripControl(r.path)} \u2014 ${label}${r.detail === null ? "" : ` (${stripControl(r.detail)})`}`
9031
+ (r) => `${oneLine(r.path, WIDTH.path)} \u2014 ${label}${r.detail === null ? "" : ` (${oneLine(r.detail, WIDTH.text)})`}`
8364
9032
  )
8365
9033
  ),
8366
9034
  ...held.folders.map(
8367
- (dir) => `${stripControl(dir)}/ \u2014 an excluded folder, whose contents were never walked`
9035
+ (dir) => `${oneLine(dir, WIDTH.path)}/ \u2014 an excluded folder, whose contents were never walked`
8368
9036
  )
8369
9037
  ];
8370
9038
  }
@@ -8384,7 +9052,7 @@ ${chosenCount} note(s) are not in ${wording.where}, because your rules say so:`
8384
9052
  for (const { label, rows } of chosen) {
8385
9053
  if (rows.length === 0) continue;
8386
9054
  printHuman(c.gray(` ${rows.length} \u2014 ${label}:`));
8387
- for (const r of rows.slice(0, 10)) printHuman(c.gray(` ${stripControl(r.path)}`));
9055
+ for (const r of rows.slice(0, 10)) printHuman(c.gray(` ${oneLine(r.path, WIDTH.path)}`));
8388
9056
  if (rows.length > 10) printHuman(c.gray(` \u2026 and ${rows.length - 10} more`));
8389
9057
  }
8390
9058
  if (held.folders.length > 0) {
@@ -8394,7 +9062,8 @@ ${chosenCount} note(s) are not in ${wording.where}, because your rules say so:`
8394
9062
  inside them are not counted above:`
8395
9063
  )
8396
9064
  );
8397
- for (const dir of held.folders.slice(0, 10)) printHuman(c.gray(` ${stripControl(dir)}/`));
9065
+ for (const dir of held.folders.slice(0, 10))
9066
+ printHuman(c.gray(` ${oneLine(dir, WIDTH.path)}/`));
8398
9067
  if (held.folders.length > 10) {
8399
9068
  printHuman(c.gray(` \u2026 and ${held.folders.length - 10} more`));
8400
9069
  }
@@ -8408,7 +9077,11 @@ ${chosenCount} note(s) are not in ${wording.where}, because your rules say so:`
8408
9077
  )
8409
9078
  );
8410
9079
  for (const d of guessed.slice(0, 10)) {
8411
- printHuman(c.gray(` ${stripControl(d.path)} (matched "${stripControl(d.term ?? "?")}")`));
9080
+ printHuman(
9081
+ c.gray(
9082
+ ` ${oneLine(d.path, WIDTH.path)} (matched "${oneLine(d.term ?? "?", WIDTH.title)}")`
9083
+ )
9084
+ );
8412
9085
  }
8413
9086
  if (guessed.length > 10) printHuman(c.gray(` \u2026 and ${guessed.length - 10} more`));
8414
9087
  printHuman(
@@ -8456,13 +9129,13 @@ function isOpaqueId(id) {
8456
9129
  return id.length === OPAQUE_ID_LENGTH && /^[0-9ABCDEFGHJKMNPQRSTVWXYZ]+$/.test(id);
8457
9130
  }
8458
9131
  function displayHandle(id, allIds) {
8459
- if (!isOpaqueId(id)) return stripControl(id);
9132
+ if (!isOpaqueId(id)) return oneLineWhole(id);
8460
9133
  for (let len = MIN_HANDLE_LENGTH; len < id.length; len++) {
8461
9134
  const prefix = id.slice(0, len);
8462
9135
  const matches = allIds.filter((other) => other.startsWith(prefix));
8463
9136
  if (matches.length === 1) return prefix;
8464
9137
  }
8465
- return stripControl(id);
9138
+ return oneLineWhole(id);
8466
9139
  }
8467
9140
  function matchIds(ref, allIds) {
8468
9141
  const exact = allIds.filter((id) => id === ref);
@@ -8689,14 +9362,127 @@ init_api_client();
8689
9362
  init_credentials();
8690
9363
  init_pending_login();
8691
9364
  init_config();
9365
+ init_ui();
8692
9366
  init_vault();
8693
9367
  async function resolveTarget(opts = {}) {
9368
+ if (opts.brain !== void 0) return namedCloudTarget({ ...opts, brain: opts.brain });
8694
9369
  if (opts.local !== true) {
8695
9370
  const cloud = await cloudTarget();
8696
- if (cloud) return cloud;
9371
+ if (cloud && cloud.kind === "cloud") {
9372
+ if (opts.vault !== void 0) {
9373
+ throw memgError(
9374
+ "invalid_input",
9375
+ `\`--vault\` names a folder on this machine, and this machine reads the hosted brain \`${oneLine(cloud.name, WIDTH.title)}\``,
9376
+ `Nothing was read from \`${oneLine(opts.vault, WIDTH.path)}\`, and nothing was changed.
9377
+
9378
+ To read that folder just this once: add \`--local\`
9379
+ To change which hosted brain this machine reads: \`memgineering use --brain <name>\`
9380
+
9381
+ This used to answer from the hosted brain without saying so, so a check of one
9382
+ brain silently reported another.`
9383
+ );
9384
+ }
9385
+ return cloud;
9386
+ }
9387
+ }
9388
+ try {
9389
+ const { brain, via } = await resolveBrain(opts.vault);
9390
+ return { kind: "local", brain, via };
9391
+ } catch (err) {
9392
+ if (!isNoVaultLinked(err) || opts.local === true) throw err;
9393
+ throw await accountAwareRefusal() ?? err;
9394
+ }
9395
+ }
9396
+ async function namedCloudTarget(opts) {
9397
+ const named = opts.brain;
9398
+ if (named.startsWith("--")) {
9399
+ throw memgError(
9400
+ "invalid_input",
9401
+ `\`--brain\` was given the flag \`${oneLine(named, WIDTH.title)}\` as its name`,
9402
+ "Nothing was read or changed. Put the hosted brain name or id immediately after `--brain`, then pass the other flag separately."
9403
+ );
9404
+ }
9405
+ const clashes = [
9406
+ opts.local === true ? "`--local`" : null,
9407
+ opts.vault !== void 0 ? "`--vault`" : null
9408
+ ].filter((value) => value !== null);
9409
+ if (clashes.length > 0) {
9410
+ throw memgError(
9411
+ "invalid_input",
9412
+ `\`--brain\` cannot be combined with ${clashes.join(" and ")}`,
9413
+ "`--brain <name>` reads an existing brain on the account. `--local` and `--vault <path>` read a folder on this machine. Nothing was read or changed; choose one target kind."
9414
+ );
9415
+ }
9416
+ const base = apiUrl();
9417
+ const credentials = await requireCredentials(base);
9418
+ const brains = await listCloudBrains(credentials.token, base);
9419
+ if (brains.length === 0) {
9420
+ throw memgError(
9421
+ "invalid_input",
9422
+ "this account has no hosted brains",
9423
+ "Nothing was read or changed. `--brain` only names an existing hosted brain; it never creates one."
9424
+ );
9425
+ }
9426
+ const found = findCloudBrain(brains, named);
9427
+ if (!found) {
9428
+ const available = brains.map((brain) => ` ${oneLine(brain.name, WIDTH.title)} (${oneLine(brain.id, WIDTH.handle)})`).join("\n");
9429
+ throw memgError(
9430
+ "invalid_input",
9431
+ `no hosted brain called "${oneLine(named, WIDTH.title)}"`,
9432
+ `Nothing was read or changed. This account has:
9433
+ ${available}
9434
+ Use one of those names or ids. No brain was created.`
9435
+ );
9436
+ }
9437
+ return { kind: "cloud", brainId: found.id, name: found.name, apiUrl: base };
9438
+ }
9439
+ var isNoVaultLinked = (err) => typeof err === "object" && err !== null && err.code === "no_vault_linked";
9440
+ async function accountAwareRefusal() {
9441
+ try {
9442
+ const base = apiUrl();
9443
+ const credentials = await readCredentials();
9444
+ if (!credentials || credentials.api_url !== base) {
9445
+ const cloud = (await loadConfig()).brain.cloud;
9446
+ if (!cloud || cloud.api_url !== base) return null;
9447
+ return memgError(
9448
+ "not_signed_in",
9449
+ `this machine is signed out of \`${oneLine(cloud.name, WIDTH.title)}\``,
9450
+ "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."
9451
+ );
9452
+ }
9453
+ const brains = await listCloudBrains(credentials.token, base, { retries: 0 });
9454
+ if (brains.length === 0) return null;
9455
+ const names = brains.map((b) => ` ${oneLine(b.name, WIDTH.title)}`).join("\n");
9456
+ return memgError(
9457
+ "no_vault_linked",
9458
+ "this machine is not pointed at any brain yet",
9459
+ `Your notes are on your account \u2014 nothing is lost, and nothing here needs creating.
9460
+ This account holds:
9461
+ ${names}
9462
+
9463
+ Point at one: \`memgineering use --brain <name>\`
9464
+
9465
+ Do NOT run \`init\` or \`link\` to fix this. They make a NEW empty brain on this
9466
+ machine, which leaves the notes above exactly where they are and adds an empty
9467
+ notebook that looks like the answer.`
9468
+ );
9469
+ } catch (err) {
9470
+ if (err instanceof MemgError && err.code !== "server_unreachable") {
9471
+ return memgError(
9472
+ "not_signed_in",
9473
+ "this machine cannot read the account it is signed in to",
9474
+ `The server refused: ${oneLine(err.message, 120)}
9475
+
9476
+ Your notes are on that account \u2014 nothing is lost, and nothing here needs creating.
9477
+
9478
+ Sign in again: \`memgineering login\`
9479
+
9480
+ Do NOT run \`init\` or \`link\` to fix this. They make a NEW empty brain on this
9481
+ machine and leave the notes where they are.`
9482
+ );
9483
+ }
9484
+ return null;
8697
9485
  }
8698
- const { brain, via } = await resolveBrain(opts.vault);
8699
- return { kind: "local", brain, via };
8700
9486
  }
8701
9487
  async function hostedPointer() {
8702
9488
  const base = apiUrl();
@@ -8788,7 +9574,9 @@ function reindexCommand() {
8788
9574
  )
8789
9575
  );
8790
9576
  for (const d of byContent.slice(0, 10)) {
8791
- printHuman(c.gray(` ${stripControl(d.path)} (${stripControl(d.detail)})`));
9577
+ printHuman(
9578
+ c.gray(` ${oneLine(d.path, WIDTH.path)} (${oneLine(d.detail, WIDTH.text)})`)
9579
+ );
8792
9580
  }
8793
9581
  if (byContent.length > 10) {
8794
9582
  printHuman(c.gray(` \u2026 and ${byContent.length - 10} more`));
@@ -8809,7 +9597,7 @@ async function reindexHosted(target, rebuild) {
8809
9597
  // route documents, spelled the way the route spells it.
8810
9598
  json: { brain: { hosted: true, name: target.name }, ...answered },
8811
9599
  human: () => {
8812
- printHuman(`Reindexed ${stripControl(target.name)}.
9600
+ printHuman(`Reindexed ${oneLine(target.name, WIDTH.title)}.
8813
9601
  `);
8814
9602
  if (lexical === void 0) {
8815
9603
  printHuman(c.yellow("\u26A0 this brain\u2019s server did not report the card index"));
@@ -8847,7 +9635,7 @@ async function reindexHosted(target, rebuild) {
8847
9635
  if (answered.usable === false) {
8848
9636
  printHuman(
8849
9637
  c.yellow(
8850
- ` \u26A0 vector search is off for this brain (${stripControl(answered.unusable_reason ?? "reason not given")})`
9638
+ ` \u26A0 vector search is off for this brain (${oneLine(answered.unusable_reason ?? "reason not given", WIDTH.text)})`
8851
9639
  )
8852
9640
  );
8853
9641
  }
@@ -8866,7 +9654,7 @@ function printFailures(failed, failures) {
8866
9654
  for (const failure of (failures ?? []).slice(0, 3)) {
8867
9655
  printHuman(
8868
9656
  c.gray(
8869
- ` ${stripControl(failure.path)} (${stripControl(failure.kind)}: ${stripControl(failure.detail)})`
9657
+ ` ${oneLine(failure.path, WIDTH.path)} (${oneLine(failure.kind, WIDTH.title)}: ${oneLine(failure.detail, WIDTH.text)})`
8870
9658
  )
8871
9659
  );
8872
9660
  }
@@ -8883,18 +9671,39 @@ function nextStep(stopReason) {
8883
9671
  case "unusable_index":
8884
9672
  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
9673
  default:
8886
- return `The server stopped for \`${stripControl(stopReason)}\`, which this version has no next step for. Running \`memgineering reindex\` again is safe.`;
9674
+ return `The server stopped for \`${oneLine(stopReason, WIDTH.title)}\`, which this version has no next step for. Running \`memgineering reindex\` again is safe.`;
8887
9675
  }
8888
9676
  }
8889
9677
  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);
9678
+ 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) => {
9679
+ if (opts.brain === true) {
9680
+ await stopReadingHosted(path);
9681
+ return;
9682
+ }
9683
+ const hostedNow = await hostedPointer();
9684
+ const linkedNow = (await loadConfig()).brain.brains;
9685
+ if (hostedNow && linkedNow.length === 0) {
9686
+ throw memgError(
9687
+ "invalid_input",
9688
+ `nothing is linked on this disk \u2014 this machine reads the hosted brain "${oneLine(hostedNow.name, 72)}"`,
9689
+ "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."
9690
+ );
9691
+ }
9692
+ const { brain } = await resolveBrain(path).catch(async (err) => {
9693
+ await resolveTarget({});
9694
+ throw err;
9695
+ });
8892
9696
  const cfg = await loadConfig();
8893
9697
  const remaining = cfg.brain.brains.filter((b) => b.root !== brain.root);
8894
9698
  const nextDefault = cfg.brain.default === brain.root ? remaining[0]?.root : cfg.brain.default;
9699
+ const { default: _dropped, ...restOfBrain } = cfg.brain;
8895
9700
  await saveConfig({
8896
9701
  ...cfg,
8897
- brain: { brains: remaining, ...nextDefault ? { default: nextDefault } : {} }
9702
+ brain: {
9703
+ ...restOfBrain,
9704
+ brains: remaining,
9705
+ ...nextDefault === void 0 ? {} : { default: nextDefault }
9706
+ }
8898
9707
  });
8899
9708
  if (opts.purge) {
8900
9709
  await clearCache(cacheDirFor(brain.root, brandHome()));
@@ -8982,6 +9791,54 @@ async function readLines(root, name) {
8982
9791
  const raw = await readFile10(join10(root, name), "utf8").catch(() => "");
8983
9792
  return raw.split("\n").map((l) => l.replace(/\r$/, "").trim()).filter((l) => l !== "" && !l.startsWith("#"));
8984
9793
  }
9794
+ async function stopReadingHosted(path) {
9795
+ if (path !== void 0) {
9796
+ throw memgError(
9797
+ "invalid_input",
9798
+ "`unlink --brain` does not take a path",
9799
+ "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`."
9800
+ );
9801
+ }
9802
+ const cfg = await loadConfig();
9803
+ const previous = cfg.brain.cloud ?? null;
9804
+ if (previous === null) {
9805
+ throw memgError(
9806
+ "invalid_input",
9807
+ "this machine is not reading a hosted brain",
9808
+ "Nothing was changed. `memgineering brains` lists what the account holds, and `memgineering use --brain <name>` points at one."
9809
+ );
9810
+ }
9811
+ await saveConfig({
9812
+ ...cfg,
9813
+ // Rebuilt without `cloud` rather than set to undefined, so what lands on
9814
+ // disk is a config with no such key rather than one asserting null.
9815
+ brain: {
9816
+ brains: cfg.brain.brains,
9817
+ ...cfg.brain.default === void 0 ? {} : { default: cfg.brain.default }
9818
+ }
9819
+ });
9820
+ printDual({
9821
+ json: {
9822
+ stopped_reading: { id: previous.id, name: previous.name, api_url: previous.api_url },
9823
+ remaining_local: cfg.brain.brains.length
9824
+ },
9825
+ human: () => {
9826
+ printHuman(`No longer reading \`${oneLine(previous.name, 72)}\`.
9827
+ `);
9828
+ printHuman(
9829
+ c.gray(
9830
+ " 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."
9831
+ )
9832
+ );
9833
+ if (cfg.brain.brains.length === 0) {
9834
+ printHuman(
9835
+ c.yellow("\n No brain is linked on this disk either, so nothing answers here now.")
9836
+ );
9837
+ printHuman(c.gray(" `memgineering link <folder>` connects notes you already have."));
9838
+ }
9839
+ }
9840
+ });
9841
+ }
8985
9842
 
8986
9843
  // src/commands/consolidate.ts
8987
9844
  init_src();
@@ -9255,10 +10112,14 @@ async function assembleStage(adapter, entry, detail, sectionName) {
9255
10112
  init_vault();
9256
10113
  init_ui();
9257
10114
  function openCommand() {
9258
- return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(
10115
+ return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
10116
+ "--brain <name>",
10117
+ "read from one existing hosted brain by name or id, without changing the pointer"
10118
+ ).option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(
9259
10119
  async (ref, opts) => {
9260
10120
  const detail = parseDetail(opts.detail, opts.section ? "chunks" : "card");
9261
10121
  const target = await resolveTarget({
10122
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
9262
10123
  ...opts.local === void 0 ? {} : { local: opts.local },
9263
10124
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
9264
10125
  });
@@ -9279,17 +10140,23 @@ function openCommand() {
9279
10140
  brain: { hosted: true, name: target.name }
9280
10141
  },
9281
10142
  human: () => {
9282
- printHuman(`## ${stripControl(String(found.memory["title"] ?? found.path))}
9283
- `);
10143
+ printHuman(
10144
+ `## ${oneLine(String(found.memory["title"] ?? found.path), WIDTH.title)}
10145
+ `
10146
+ );
9284
10147
  const summary = found.memory["summary"];
9285
10148
  if (typeof summary === "string" && summary !== "") {
9286
- printHuman(`> ${stripControl(summary)}
10149
+ printHuman(`> ${oneLine(summary, WIDTH.text)}
9287
10150
  `);
9288
10151
  }
9289
- printHuman(c.gray(`${stripControl(found.path)} \xB7 ${stripControl(target.name)}`));
10152
+ printHuman(
10153
+ c.gray(
10154
+ `${oneLine(found.path, WIDTH.path)} \xB7 ${oneLine(target.name, WIDTH.title)}`
10155
+ )
10156
+ );
9290
10157
  if (detail !== "card" && found.body !== "") {
9291
- printHuman(`
9292
- ${stripControl(found.body)}`);
10158
+ printHuman("\n");
10159
+ printBlock(found.body);
9293
10160
  }
9294
10161
  }
9295
10162
  });
@@ -9326,7 +10193,7 @@ ${stripControl(found.body)}`);
9326
10193
  ...stage
9327
10194
  },
9328
10195
  human: () => {
9329
- printHuman(`## ${stripControl(entry.memory.title)}
10196
+ printHuman(`## ${oneLine(entry.memory.title, WIDTH.title)}
9330
10197
  `);
9331
10198
  if (scaffolding) {
9332
10199
  printHuman(
@@ -9339,13 +10206,17 @@ ${stripControl(found.body)}`);
9339
10206
  }
9340
10207
  printHuman(
9341
10208
  `
9342
- \`id: ${stripControl(entry.memory.id)}\` \xB7 \`${stripControl(entry.path)}\`
10209
+ \`id: ${oneLine(entry.memory.id, WIDTH.handle)}\` \xB7 \`${oneLine(entry.path, WIDTH.path)}\`
9343
10210
  `
9344
10211
  );
9345
10212
  if (entry.memory.summary) printHuman(quoteBlock(entry.memory.summary) + "\n");
9346
10213
  for (const claim of claims) {
9347
- printHuman(`- ${stripControl(claim.text)}`);
9348
- printHuman(c.gray(stripControl(` ${claim.confidence} \xB7 ${claim.status}`)));
10214
+ printHuman(`- ${oneLine(claim.text, WIDTH.text)}`);
10215
+ printHuman(
10216
+ c.gray(
10217
+ ` ${oneLine(claim.confidence, WIDTH.title)} \xB7 ${oneLine(claim.status, WIDTH.title)}`
10218
+ )
10219
+ );
9349
10220
  }
9350
10221
  if (claims.length === 0 && scaffolding) {
9351
10222
  printHuman(
@@ -9376,13 +10247,14 @@ makes the two surface together as a conflict.`
9376
10247
  }
9377
10248
  if (stage.outline && stage.outline.length > 0) {
9378
10249
  printHuman("\n### sections\n");
9379
- for (const heading of stage.outline) printHuman(`- ${stripControl(heading)}`);
10250
+ for (const heading of stage.outline)
10251
+ printHuman(`- ${oneLine(heading, WIDTH.title)}`);
9380
10252
  printHuman("");
9381
10253
  }
9382
10254
  if (stage.sections) {
9383
10255
  for (const section of stage.sections) {
9384
10256
  printHuman(`
9385
- ### ${stripControl(section.label)}
10257
+ ### ${oneLine(section.label, WIDTH.title)}
9386
10258
  `);
9387
10259
  printHuman(quoteBlock(section.body));
9388
10260
  }
@@ -9942,11 +10814,11 @@ To see what is there now: \`memgineering open ` + ref + "`"
9942
10814
  after: preview.after
9943
10815
  },
9944
10816
  human: () => {
9945
- printHuman(`## revise (dry run): ${stripControl(entry.memory.title)}
10817
+ printHuman(`## revise (dry run): ${oneLine(entry.memory.title, WIDTH.title)}
9946
10818
  `);
9947
- printHuman(`\`${stripControl(entry.path)}\`
10819
+ printHuman(`\`${oneLine(entry.path, WIDTH.path)}\`
9948
10820
  `);
9949
- printHuman(stripControl(renderDiff(preview.before, preview.after)));
10821
+ printBlock(renderDiff(preview.before, preview.after));
9950
10822
  printHuman(c.gray("\nNothing was written. Drop --dry-run to apply."));
9951
10823
  }
9952
10824
  });
@@ -10006,10 +10878,10 @@ To see what is there now: \`memgineering open ` + ref + "`"
10006
10878
  human: () => {
10007
10879
  if (proposal.action === "conflict") {
10008
10880
  printHuman(
10009
- `Recorded a disagreement about **${stripControl(entry.memory.title)}**.
10881
+ `Recorded a disagreement about **${oneLine(entry.memory.title, WIDTH.title)}**.
10010
10882
  `
10011
10883
  );
10012
- printHuman(`\`${stripControl(entry.path)}\`
10884
+ printHuman(`\`${oneLine(entry.path, WIDTH.path)}\`
10013
10885
  `);
10014
10886
  printHuman(
10015
10887
  c.gray(
@@ -10017,12 +10889,12 @@ To see what is there now: \`memgineering open ` + ref + "`"
10017
10889
  )
10018
10890
  );
10019
10891
  } else {
10020
- printHuman(`Revised **${stripControl(entry.memory.title)}**.
10892
+ printHuman(`Revised **${oneLine(entry.memory.title, WIDTH.title)}**.
10021
10893
  `);
10022
- printHuman(`\`${stripControl(entry.path)}\`
10894
+ printHuman(`\`${oneLine(entry.path, WIDTH.path)}\`
10023
10895
  `);
10024
10896
  }
10025
- printHuman(stripControl(renderDiff(preview.before, preview.after)));
10897
+ printBlock(renderDiff(preview.before, preview.after));
10026
10898
  if (drift) {
10027
10899
  printHuman(
10028
10900
  c.yellow(`
@@ -10089,18 +10961,18 @@ async function reviseHosted(where, ref, opts) {
10089
10961
  json: { brain: where.name, ...result },
10090
10962
  human: () => {
10091
10963
  if (result.dry_run) {
10092
- printHuman(`## revise (dry run) \u2014 ${stripControl(where.name)}
10964
+ printHuman(`## revise (dry run) \u2014 ${oneLine(where.name, WIDTH.title)}
10093
10965
  `);
10094
- printHuman(`\`${stripControl(result.target)}\`
10966
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
10095
10967
  `);
10096
- printHuman(stripControl(renderDiff(result.before, result.after)));
10968
+ printBlock(renderDiff(result.before, result.after));
10097
10969
  printHuman(c.gray("\nNothing was written. Drop --dry-run to apply."));
10098
10970
  return;
10099
10971
  }
10100
10972
  if (opts.action === "conflict") {
10101
- printHuman(`Recorded a disagreement in **${stripControl(where.name)}**.
10973
+ printHuman(`Recorded a disagreement in **${oneLine(where.name, WIDTH.title)}**.
10102
10974
  `);
10103
- printHuman(`\`${stripControl(result.target)}\`
10975
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
10104
10976
  `);
10105
10977
  printHuman(
10106
10978
  c.gray(
@@ -10108,16 +10980,16 @@ async function reviseHosted(where, ref, opts) {
10108
10980
  )
10109
10981
  );
10110
10982
  } else {
10111
- printHuman(`Revised in **${stripControl(where.name)}**.
10983
+ printHuman(`Revised in **${oneLine(where.name, WIDTH.title)}**.
10112
10984
  `);
10113
- printHuman(`\`${stripControl(result.target)}\`
10985
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
10114
10986
  `);
10115
10987
  }
10116
- printHuman(stripControl(renderDiff(result.before, result.after)));
10988
+ printBlock(renderDiff(result.before, result.after));
10117
10989
  if (result.body_drift) {
10118
10990
  printHuman(
10119
10991
  c.yellow(`
10120
- \u26A0 the body still reads "${stripControl(result.body_drift)}"`) + c.gray(
10992
+ \u26A0 the body still reads "${oneLine(result.body_drift, WIDTH.text)}"`) + c.gray(
10121
10993
  "\n \u2014 the frontmatter now concludes differently. The prose is yours and was not\n touched; edit it yourself if it should agree."
10122
10994
  )
10123
10995
  );
@@ -10141,7 +11013,7 @@ To put it back: \`memgineering revise ${ref} --claim ${JSON.stringify(
10141
11013
  );
10142
11014
  } else if (result.reverse?.hint) {
10143
11015
  printHuman(c.gray(`
10144
- ${stripControl(result.reverse.hint)}`));
11016
+ ${oneLine(result.reverse.hint, WIDTH.text)}`));
10145
11017
  }
10146
11018
  }
10147
11019
  });
@@ -10185,7 +11057,7 @@ function nothingToChange(path, ref, verb = "revise") {
10185
11057
  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
11058
  return memgError(
10187
11059
  "nothing_to_change",
10188
- `${stripControl(path)} already says exactly this`,
11060
+ `${oneLine(path, WIDTH.path)} already says exactly this`,
10189
11061
  `Nothing was written. ${way}
10190
11062
  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
11063
  To see what the note says now: \`memgineering open ${ref}\``
@@ -10293,20 +11165,22 @@ function renderSummary(p) {
10293
11165
  const claim = p.claim === void 0 || p.claim === null ? null : oneLine(p.claim, CLAIM_WIDTH);
10294
11166
  const unavailable = p.claim === void 0 && !claimless;
10295
11167
  const lines2 = [
10296
- `- **${stripControl(p.action)}**${p.proposed_kind ? ` \u2192 ${p.proposed_kind}` : ""} \`${stripControl(p.target)}\``,
11168
+ `- **${oneLine(p.action, WIDTH.title)}**${p.proposed_kind ? ` \u2192 ${p.proposed_kind}` : ""} \`${oneLine(p.target, WIDTH.path)}\``,
10297
11169
  ` ${p.id} ${p.state}`
10298
11170
  ];
10299
11171
  if (claim !== null && claim !== "") lines2.push(` \u2192 ${claim}`);
10300
11172
  else if (unavailable) {
10301
11173
  lines2.push(
10302
11174
  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.)`
11175
+ ` \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
11176
  )
10305
11177
  );
10306
11178
  }
10307
11179
  lines2.push(quoteBlock(p.reason, " > "));
10308
11180
  if (p.supersedes.length > 0) {
10309
- lines2.push(c.gray(` replaces: ${p.supersedes.map(stripControl).join(", ")}`));
11181
+ lines2.push(
11182
+ c.gray(` replaces: ${p.supersedes.map((id) => oneLine(id, WIDTH.handle)).join(", ")}`)
11183
+ );
10310
11184
  }
10311
11185
  return lines2.join("\n");
10312
11186
  }
@@ -10422,10 +11296,10 @@ function renderApproval(applied, vault) {
10422
11296
  },
10423
11297
  human: () => {
10424
11298
  printHuman(
10425
- `Applied **${stripControl(applied.action)}** to \`${stripControl(applied.target)}\`.
11299
+ `Applied **${oneLine(applied.action, WIDTH.title)}** to \`${oneLine(applied.target, WIDTH.path)}\`.
10426
11300
  `
10427
11301
  );
10428
- printHuman(stripControl(renderDiff(applied.before, applied.after)));
11302
+ printBlock(renderDiff(applied.before, applied.after));
10429
11303
  printHuman(c.gray(`
10430
11304
  Undo: \`${undoHint(applied.opId, vault)}\``));
10431
11305
  }
@@ -10435,7 +11309,7 @@ function renderRejection(id, state, target) {
10435
11309
  printDual({
10436
11310
  json: { proposal_id: id, state, target },
10437
11311
  human: () => {
10438
- printHuman(`Declined \`${stripControl(target)}\`.
11312
+ printHuman(`Declined \`${oneLine(target, WIDTH.path)}\`.
10439
11313
  `);
10440
11314
  printHuman(c.gray("The note was not touched."));
10441
11315
  }
@@ -10493,9 +11367,9 @@ function renderRebase(id, state, target, before, after) {
10493
11367
  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
11368
  },
10495
11369
  human: () => {
10496
- printHuman(`Recomputed against \`${stripControl(target)}\`.
11370
+ printHuman(`Recomputed against \`${oneLine(target, WIDTH.path)}\`.
10497
11371
  `);
10498
- printHuman(stripControl(renderDiff(before, after)));
11372
+ printBlock(renderDiff(before, after));
10499
11373
  printHuman(
10500
11374
  c.gray(
10501
11375
  `
@@ -10587,9 +11461,11 @@ function consolidateCommand() {
10587
11461
  [
10588
11462
  "",
10589
11463
  "What it looks for:",
10590
- " similar_title two notes titled with mostly the same words",
10591
- " shared_context two notes in one scope naming the same thing",
11464
+ " declared_supersession a replaced note that is still active",
10592
11465
  " contradiction a disagreement somebody declared and nobody settled",
11466
+ " semantic_similarity calibrated indexed meaning agrees reciprocally",
11467
+ " shared_context two notes in one scope naming the same thing",
11468
+ " similar_title two notes titled with mostly the same words",
10593
11469
  " kind_promotion a note marked as binding but filed as something other",
10594
11470
  " than a decision",
10595
11471
  "",
@@ -10618,7 +11494,7 @@ function consolidateCommand() {
10618
11494
  // Present on every answer, not only the empty one. A full list still
10619
11495
  // leaves the reader one step short — the candidates are evidence, and
10620
11496
  // the next command is the thing they cannot derive from evidence.
10621
- hint: truncated ? `This brain is large enough that detection stopped after ${report.scanned} pairs, so these are candidates rather than all of them. Narrow it \u2014 \`--vault\` a smaller brain, or run again after acting on these \u2014 and treat the list as a sample. Proposals are still written one at a time: \`memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"\`.` : candidates.length ? 'None of these is a proposal yet. Decide what each pair means, then: `memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"`. A duplicate is two proposals \u2014 supersede on the one that stays, retire on the one it replaces.' : "Nothing looked duplicated, contradicted, or misfiled. Detection reads titles, scope, entities and declared edges \u2014 a brain whose notes carry none of those has little for it to compare. `memgineering recall <topic>` is the way to find near-duplicates by hand."
11497
+ hint: truncated ? `This brain is large enough that detection stopped after ${report.scanned} pairs, so these are candidates rather than all of them. Narrow it \u2014 \`--vault\` a smaller brain, or run again after acting on these \u2014 and treat the list as a sample. Proposals are still written one at a time: \`memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"\`.` : candidates.length ? 'None of these is a proposal yet. Decide what each pair means, then: `memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"`. An inferred duplicate is two proposals \u2014 supersede the winner and retire the loser. `declared_supersession` already has the winner edge, so only the suggested loser retirement remains.' : "Nothing looked duplicated, contradicted, or misfiled. Detection reads titles, scope, entities and declared edges; a hosted brain also reuses a complete calibrated vector generation, with stricter corroboration when context is absent. `memgineering recall <topic>` remains the broader semantic search."
10622
11498
  },
10623
11499
  human: () => {
10624
11500
  if (candidates.length === 0) {
@@ -10645,7 +11521,7 @@ function consolidateCommand() {
10645
11521
  }
10646
11522
  printHuman(
10647
11523
  c.gray(
10648
- '\nNone of these is a proposal yet \u2014 nothing has been written.\nDecide what a pair means, then say so:\n memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"\nA duplicate is two proposals: supersede on the one that stays, retire on the one\nit replaces.'
11524
+ '\nNone of these is a proposal yet \u2014 nothing has been written.\nDecide what a pair means, then say so:\n memgineering propose <action> --memory <id> --claim "\u2026" --reason "\u2026" --excerpt "\u2026"\nAn inferred duplicate is two proposals: supersede the winner and retire the loser.\nA declared_supersession already has its winner edge; only retire the named loser.'
10649
11525
  )
10650
11526
  );
10651
11527
  }
@@ -10656,16 +11532,16 @@ var TITLE_WIDTH = 72;
10656
11532
  function noteLines(id, title, path) {
10657
11533
  const shown = oneLine(title, TITLE_WIDTH);
10658
11534
  return [
10659
- shown === "" ? ` ${stripControl(id)}` : ` ${stripControl(id)} ${shown}`,
11535
+ shown === "" ? ` ${oneLine(id, WIDTH.handle)}` : ` ${oneLine(id, WIDTH.handle)} ${shown}`,
10660
11536
  // Backticked, as it was before the title moved onto the row above it. This
10661
11537
  // output is read by agents as markdown, and a path outside code formatting
10662
11538
  // is a path they have to guess the extent of.
10663
- c.gray(` \`${stripControl(path)}\``)
11539
+ c.gray(` \`${middleLine(path, WIDTH.path)}\``)
10664
11540
  ];
10665
11541
  }
10666
11542
  function renderCandidate(candidate) {
10667
11543
  const lines2 = [
10668
- `- **${stripControl(candidate.rule)}** \u2014 ${stripControl(candidate.evidence)}`,
11544
+ `- **${oneLine(candidate.rule, WIDTH.title)}** \u2014 ${oneLine(candidate.evidence, WIDTH.text)}`,
10669
11545
  ...noteLines(candidate.memory_id, candidate.title, candidate.path)
10670
11546
  ];
10671
11547
  if (candidate.other_id !== null) {
@@ -10707,7 +11583,7 @@ function proposeCommand() {
10707
11583
  },
10708
11584
  human: () => {
10709
11585
  printHuman(
10710
- `Proposed **${stripControl(filed.action)}** on \`${stripControl(filed.target)}\`.
11586
+ `Proposed **${oneLine(filed.action, WIDTH.title)}** on \`${oneLine(filed.target, WIDTH.path)}\`.
10711
11587
  `
10712
11588
  );
10713
11589
  printHuman(c.gray(` ${filed.id}`));
@@ -11142,14 +12018,14 @@ function lifecycleCommand(action) {
11142
12018
  json: { action, memory_id: entry.memory.id, target: entry.path, op_id: record.op_id },
11143
12019
  human: () => {
11144
12020
  printHuman(
11145
- action === "retire" ? `Retired **${stripControl(entry.memory.title)}**.
11146
- ` : `**${stripControl(entry.memory.title)}** is current again.
12021
+ action === "retire" ? `Retired **${oneLine(entry.memory.title, WIDTH.title)}**.
12022
+ ` : `**${oneLine(entry.memory.title, WIDTH.title)}** is current again.
11147
12023
  `
11148
12024
  );
11149
12025
  if (action === "retire") {
11150
12026
  printHuman(
11151
12027
  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) + "`"
12028
+ "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
12029
  )
11154
12030
  );
11155
12031
  }
@@ -11207,8 +12083,10 @@ function ignoreCommand(mode) {
11207
12083
  mode === "exclude" ? (
11208
12084
  // The path is argv here, but argv came from a listing of the
11209
12085
  // 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.`)
12086
+ c.gray(
12087
+ `\`${oneLine(target, WIDTH.path)}\` was already excluded \u2014 nothing changed.`
12088
+ )
12089
+ ) : c.gray(`\`${oneLine(target, WIDTH.path)}\` was not excluded \u2014 nothing changed.`)
11212
12090
  );
11213
12091
  return;
11214
12092
  }
@@ -11238,13 +12116,13 @@ async function lifecycleHosted(where, action, ref, reason) {
11238
12116
  json: { brain: where.name, ...result },
11239
12117
  human: () => {
11240
12118
  printHuman(
11241
- action === "retire" ? `Retired in **${stripControl(where.name)}**.
11242
- ` : `Current again in **${stripControl(where.name)}**.
12119
+ action === "retire" ? `Retired in **${oneLine(where.name, WIDTH.title)}**.
12120
+ ` : `Current again in **${oneLine(where.name, WIDTH.title)}**.
11243
12121
  `
11244
12122
  );
11245
- printHuman(`\`${stripControl(result.target)}\`
12123
+ printHuman(`\`${oneLine(result.target, WIDTH.path)}\`
11246
12124
  `);
11247
- printHuman(c.gray(stripControl(result.note)));
12125
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
11248
12126
  printHuman(
11249
12127
  c.gray(
11250
12128
  `
@@ -11264,15 +12142,15 @@ async function ignoreHosted(where, mode, path, reason) {
11264
12142
  json: { brain: where.name, ...result },
11265
12143
  human: () => {
11266
12144
  if (!result.changed) {
11267
- printHuman(c.gray(stripControl(result.note)));
12145
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
11268
12146
  return;
11269
12147
  }
11270
12148
  printHuman(
11271
- mode === "exclude" ? `No longer reading \`${stripControl(result.path)}\` in **${stripControl(where.name)}**.
11272
- ` : `Reading \`${stripControl(result.path)}\` again in **${stripControl(where.name)}**.
12149
+ mode === "exclude" ? `No longer reading \`${oneLine(result.path, WIDTH.path)}\` in **${oneLine(where.name, WIDTH.title)}**.
12150
+ ` : `Reading \`${oneLine(result.path, WIDTH.path)}\` again in **${oneLine(where.name, WIDTH.title)}**.
11273
12151
  `
11274
12152
  );
11275
- printHuman(c.gray(stripControl(result.note)));
12153
+ printHuman(c.gray(oneLine(result.note, WIDTH.text)));
11276
12154
  if (result.op_id) printHuman(c.gray(`
11277
12155
  Undo: \`memgineering ${opposite(mode)} ${path}\``));
11278
12156
  }
@@ -11304,8 +12182,12 @@ init_index_build();
11304
12182
  init_vault();
11305
12183
  init_ui();
11306
12184
  function evidenceCommand() {
11307
- return new Command10("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").action(async (ref, opts) => {
12185
+ return new Command10("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
12186
+ "--brain <name>",
12187
+ "read from one existing hosted brain by name or id, without changing the pointer"
12188
+ ).action(async (ref, opts) => {
11308
12189
  const target = await resolveTarget({
12190
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
11309
12191
  ...opts.local === void 0 ? {} : { local: opts.local },
11310
12192
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
11311
12193
  });
@@ -11314,7 +12196,11 @@ function evidenceCommand() {
11314
12196
  const report2 = await cloudEvidence(cloud, ref);
11315
12197
  printDual({
11316
12198
  json: { ...report2, brain: { hosted: true, name: target.name } },
11317
- human: () => renderEvidence(viewFromCloud(report2), { now: /* @__PURE__ */ new Date(), hosted: true })
12199
+ human: () => renderEvidence(viewFromCloud(report2), {
12200
+ now: /* @__PURE__ */ new Date(),
12201
+ hosted: true,
12202
+ brain: target.name
12203
+ })
11318
12204
  });
11319
12205
  return;
11320
12206
  }
@@ -11336,10 +12222,13 @@ function evidenceCommand() {
11336
12222
  });
11337
12223
  }
11338
12224
  function renderEvidence(report, opts) {
11339
- printHuman(`## evidence: ${stripControl(report.title)}
11340
- `);
11341
- printHuman(`\`id: ${stripControl(report.id)}\` \xB7 \`${stripControl(report.path)}\`
12225
+ const brainHeading = opts.brain === void 0 ? "" : ` \u2014 ${oneLine(opts.brain, WIDTH.title)}`;
12226
+ printHuman(`## evidence: ${oneLine(report.title, WIDTH.title)}${brainHeading}
11342
12227
  `);
12228
+ printHuman(
12229
+ `\`id: ${oneLine(report.id, WIDTH.handle)}\` \xB7 \`${oneLine(report.path, WIDTH.path)}\`
12230
+ `
12231
+ );
11343
12232
  if (report.critical) {
11344
12233
  printHuman(c.yellow("\u26A0 critical \u2014 this note defines how the agent behaves.") + "\n");
11345
12234
  }
@@ -11389,11 +12278,12 @@ function renderEvidence(report, opts) {
11389
12278
  ]);
11390
12279
  }
11391
12280
  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}`));
12281
+ for (const [label, value] of rows)
12282
+ printHuman(` ${label.padEnd(pad)}${oneLine(value, WIDTH.text)}`);
11393
12283
  if (report.standing) {
11394
12284
  const means = STANDING_MEANS[report.standing];
11395
12285
  printHuman(`
11396
- **${stripControl(report.standing)}**${means ? ` \u2014 ${means}` : ""}`);
12286
+ **${oneLine(report.standing, WIDTH.title)}**${means ? ` \u2014 ${means}` : ""}`);
11397
12287
  }
11398
12288
  if (report.contradicts > 0) {
11399
12289
  printHuman(
@@ -11414,7 +12304,7 @@ function renderEvidence(report, opts) {
11414
12304
  report.last_reason.verb,
11415
12305
  report.last_reason.ts === null ? null : report.last_reason.ts.slice(0, 10)
11416
12306
  ].filter((part) => part !== null && part !== "").join(", ");
11417
- if (provenance !== "") printHuman(c.gray(` ${stripControl(provenance)}`));
12307
+ if (provenance !== "") printHuman(c.gray(` ${oneLine(provenance, WIDTH.text)}`));
11418
12308
  } else if (report.changed > 0) {
11419
12309
  printHuman(
11420
12310
  c.gray(
@@ -11550,6 +12440,7 @@ import { Command as Command11 } from "commander";
11550
12440
  // src/lib/rules.ts
11551
12441
  init_src();
11552
12442
  init_index_build();
12443
+ init_ui();
11553
12444
  init_vault();
11554
12445
  var RULE_LIMIT = 8;
11555
12446
  async function collectRules(vault, contextDir, now = /* @__PURE__ */ new Date()) {
@@ -11578,11 +12469,12 @@ async function collectRules(vault, contextDir, now = /* @__PURE__ */ new Date())
11578
12469
  };
11579
12470
  }
11580
12471
  function renderRuleLines(rules) {
11581
- return rules.map(({ entry }) => `- ${entry.memory.summary ?? entry.memory.title}`);
12472
+ return rules.map(({ entry }) => `- ${oneLine(entry.memory.summary ?? entry.memory.title, 200)}`);
11582
12473
  }
11583
12474
 
11584
12475
  // src/commands/guard.ts
11585
12476
  init_deadline();
12477
+ init_ui();
11586
12478
  function guardCommand() {
11587
12479
  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
12480
  try {
@@ -11622,7 +12514,7 @@ async function runGuard(payload, contextDirFlag) {
11622
12514
  return JSON.stringify({
11623
12515
  hookSpecificOutput: {
11624
12516
  hookEventName: "PreToolUse",
11625
- additionalContext: contextFor(claimed.rules, claimed.total)
12517
+ additionalContext: contextFor(claimed.lines, claimed.total)
11626
12518
  }
11627
12519
  });
11628
12520
  }
@@ -11636,16 +12528,22 @@ async function runStopGuard(payload, contextDirFlag) {
11636
12528
  return JSON.stringify({
11637
12529
  hookSpecificOutput: {
11638
12530
  hookEventName: "Stop",
11639
- additionalContext: stopContextFor(claimed.rules, claimed.total)
12531
+ additionalContext: stopContextFor(claimed.lines, claimed.total)
11640
12532
  }
11641
12533
  });
11642
12534
  }
11643
12535
  async function claimRules(sessionId, contextDir) {
12536
+ const hosted = await hostedRuleLines().catch(() => null);
12537
+ if (hosted !== null) {
12538
+ if (hosted.lines.length === 0) return null;
12539
+ if (!await claimSession(sessionId, hosted.brainId)) return null;
12540
+ return { lines: hosted.lines, total: hosted.total };
12541
+ }
11644
12542
  const collected = await collectRules(void 0, contextDir).catch(() => null);
11645
12543
  if (collected === null) return null;
11646
12544
  const { brainRoot, rules, total } = collected;
11647
12545
  if (rules.length === 0) return null;
11648
- if (!await claimSession(sessionId)) return null;
12546
+ if (!await claimSession(sessionId, brainRoot)) return null;
11649
12547
  await recordEvent(brainRoot, {
11650
12548
  verb: "guard",
11651
12549
  contextDir,
@@ -11653,22 +12551,20 @@ async function claimRules(sessionId, contextDir) {
11653
12551
  limit: RULE_LIMIT,
11654
12552
  returnedIds: rules.map((r) => r.entry.memory.id)
11655
12553
  });
11656
- return { rules, total };
12554
+ return { lines: renderRuleLines(rules), total };
11657
12555
  }
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.)` : "";
12556
+ function contextFor(lines2, total) {
12557
+ const more = total > lines2.length ? `
12558
+ (${total - lines2.length} more \u2014 \`memgineering rules\` for all of them.)` : "";
11662
12559
  return `Decisions this user has already made. They are theirs, not suggestions, and they hold whether or not this task mentions them:
11663
12560
 
11664
12561
  ${lines2.join("\n")}${more}
11665
12562
 
11666
12563
  Follow them. If what you are about to do goes against one, say so in a sentence instead of doing it quietly.`;
11667
12564
  }
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.)` : "";
12565
+ function stopContextFor(lines2, total) {
12566
+ const more = total > lines2.length ? `
12567
+ (${total - lines2.length} more \u2014 \`memgineering rules\` for all of them.)` : "";
11672
12568
  return `Decisions this user has already made. They are theirs, not suggestions, and they hold whether or not this task mentions them:
11673
12569
 
11674
12570
  ${lines2.join("\n")}${more}
@@ -11699,10 +12595,10 @@ async function readPayload() {
11699
12595
  }
11700
12596
  }
11701
12597
  var guardDir = () => join16(brandHome(), "guard");
11702
- async function claimSession(sessionId) {
12598
+ async function claimSession(sessionId, brain) {
11703
12599
  const marker = join16(
11704
12600
  guardDir(),
11705
- `${createHash7("sha256").update(sessionId).digest("hex").slice(0, 32)}`
12601
+ `${createHash7("sha256").update(`${sessionId}\0${brain}`).digest("hex").slice(0, 32)}`
11706
12602
  );
11707
12603
  try {
11708
12604
  await mkdir11(guardDir(), { recursive: true, mode: 448 });
@@ -11727,6 +12623,17 @@ async function pruneOldMarkers() {
11727
12623
  } catch {
11728
12624
  }
11729
12625
  }
12626
+ var RULE_WIDTH = 200;
12627
+ async function hostedRuleLines() {
12628
+ const where = await resolveTarget({});
12629
+ if (where.kind !== "cloud") return null;
12630
+ const { rules, total } = await cloudRules(await openCloudBrain(where));
12631
+ return {
12632
+ lines: rules.map((r) => `- ${oneLine(r.summary ?? r.title, RULE_WIDTH)}`),
12633
+ total,
12634
+ brainId: where.brainId
12635
+ };
12636
+ }
11730
12637
 
11731
12638
  // src/program.ts
11732
12639
  init_init();
@@ -11739,8 +12646,12 @@ import { Command as Command12 } from "commander";
11739
12646
  init_vault();
11740
12647
  init_ui();
11741
12648
  function logCommand() {
11742
- return new Command12("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").action(async (opts) => {
12649
+ return new Command12("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
12650
+ "--brain <name>",
12651
+ "read one existing hosted brain by name or id, without changing the pointer"
12652
+ ).action(async (opts) => {
11743
12653
  const target = await resolveTarget({
12654
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
11744
12655
  ...opts.local === void 0 ? {} : { local: opts.local },
11745
12656
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
11746
12657
  });
@@ -11769,7 +12680,7 @@ function logCommand() {
11769
12680
  human: () => {
11770
12681
  const diverged2 = Array.isArray(history["divergence"]) ? history["divergence"] : [];
11771
12682
  const total = typeof history["total"] === "number" ? history["total"] : rows.length;
11772
- printHuman(`## log \u2014 ${stripControl(target.name)} (${total} operations)
12683
+ printHuman(`## log \u2014 ${oneLine(target.name, WIDTH.title)} (${total} operations)
11773
12684
  `);
11774
12685
  if (rows.length === 0) {
11775
12686
  printHuman("_This brain has no recorded operations._\n");
@@ -11783,7 +12694,8 @@ function logCommand() {
11783
12694
  );
11784
12695
  }
11785
12696
  const hint = history["hint"];
11786
- if (typeof hint === "string" && hint !== "") printHuman(c.gray(stripControl(hint)));
12697
+ if (typeof hint === "string" && hint !== "")
12698
+ printHuman(c.gray(oneLine(hint, WIDTH.text)));
11787
12699
  }
11788
12700
  });
11789
12701
  return;
@@ -11905,15 +12817,19 @@ function renderOps(ops, opts) {
11905
12817
  op.diverged && !op.undone ? "note has changed since" : null
11906
12818
  ].filter(Boolean);
11907
12819
  printHuman(
11908
- `- ${stripControl(op.verb)} \`${stripControl(op.target)}\`${marks.length ? c.yellow(` (${marks.join(", ")})`) : ""}`
12820
+ `- ${oneLine(op.verb, WIDTH.title)} \`${oneLine(op.target, WIDTH.path)}\`${marks.length ? c.yellow(` (${marks.join(", ")})`) : ""}`
11909
12821
  );
11910
12822
  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)}`
12823
+ c.gray(
12824
+ ` ${oneLine(op.ts, WIDTH.handle)}${op.actor ? ` \xB7 ${oneLine(op.actor, WIDTH.title)}` : ""}`
12825
+ ) + ` ${c.gray(undoable(op, opts.hosted) ? "undo:" : "op:")} ${oneLine(op.op_id, WIDTH.handle)}`
11912
12826
  );
11913
12827
  if (op.rationale) printHuman(c.gray(quoteBlock(op.rationale, " > ")));
11914
12828
  if (opts.verbose) {
11915
12829
  printHuman(
11916
- c.gray(` ${stripControl(op.hash_before ?? "\u2205")} \u2192 ${stripControl(op.hash_after ?? "\u2205")}`)
12830
+ c.gray(
12831
+ ` ${oneLine(op.hash_before ?? "\u2205", WIDTH.handle)} \u2192 ${oneLine(op.hash_after ?? "\u2205", WIDTH.handle)}`
12832
+ )
11917
12833
  );
11918
12834
  }
11919
12835
  }
@@ -11969,7 +12885,7 @@ init_pending_login();
11969
12885
  init_ui();
11970
12886
  import { Command as Command14 } from "commander";
11971
12887
  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(
12888
+ 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
12889
  "--local",
11974
12890
  "forget the token here without telling the server \u2014 only for a server you cannot reach"
11975
12891
  ).action(async (opts) => {
@@ -12052,6 +12968,11 @@ function logoutCommand() {
12052
12968
  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
12969
  )
12054
12970
  );
12971
+ printHuman(
12972
+ c.gray(
12973
+ " 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."
12974
+ )
12975
+ );
12055
12976
  }
12056
12977
  });
12057
12978
  });
@@ -12135,12 +13056,12 @@ init_ui();
12135
13056
  function pullCommand() {
12136
13057
  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
13058
  const { brain: source, others } = await resolveSource(opts.brain);
12138
- const target = resolve8(destination ?? `./${stripControl(source.name)}`);
13059
+ const target = resolve8(destination ?? `./${oneLine(source.name, WIDTH.title)}`);
12139
13060
  const first = await cloudListNotes(source, { limit: 1 });
12140
13061
  if (first.total === 0) {
12141
13062
  throw memgError(
12142
13063
  "invalid_input",
12143
- `\u201C${stripControl(source.name)}\u201D has no notes to download`,
13064
+ `\u201C${oneLine(source.name, WIDTH.title)}\u201D has no notes to download`,
12144
13065
  "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
13066
  );
12146
13067
  }
@@ -12161,13 +13082,13 @@ function pullCommand() {
12161
13082
  printHuman(`## pull (dry run)
12162
13083
  `);
12163
13084
  printHuman(
12164
- `${first.total} note(s) from \u201C${stripControl(source.name)}\u201D into \`${target}\`
13085
+ `${first.total} note(s) from \u201C${oneLine(source.name, WIDTH.title)}\u201D into \`${target}\`
12165
13086
  `
12166
13087
  );
12167
13088
  for (const note of preview.slice(0, 20)) {
12168
13089
  printHuman(
12169
13090
  c.gray(
12170
- ` ${stripControl(note.path)}${note.excluded === true ? " (excluded)" : ""}`
13091
+ ` ${oneLine(note.path, WIDTH.path)}${note.excluded === true ? " (excluded)" : ""}`
12171
13092
  )
12172
13093
  );
12173
13094
  }
@@ -12182,7 +13103,7 @@ function pullCommand() {
12182
13103
  });
12183
13104
  return;
12184
13105
  }
12185
- progress(`downloading ${first.total} note(s) from \u201C${stripControl(source.name)}\u201D\u2026`);
13106
+ progress(`downloading ${first.total} note(s) from \u201C${oneLine(source.name, WIDTH.title)}\u201D\u2026`);
12186
13107
  const written = [];
12187
13108
  const already = [];
12188
13109
  const excluded = [];
@@ -12224,7 +13145,7 @@ function pullCommand() {
12224
13145
  human: () => {
12225
13146
  printHuman(
12226
13147
  c.green(
12227
- `\u2713 ${written.length} note(s) from \u201C${stripControl(source.name)}\u201D written to \`${target}\``
13148
+ `\u2713 ${written.length} note(s) from \u201C${oneLine(source.name, WIDTH.title)}\u201D written to \`${target}\``
12228
13149
  )
12229
13150
  );
12230
13151
  if (already.length > 0) {
@@ -12276,8 +13197,8 @@ async function resolveSource(named) {
12276
13197
  if (!found) {
12277
13198
  throw memgError(
12278
13199
  "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 ?? "")}\`)`
13200
+ named ? `no hosted brain called \u201C${oneLine(named, WIDTH.title)}\u201D` : "more than one hosted brain \u2014 say which",
13201
+ `Pass one of: ${brains.map((b) => oneLine(b.name, WIDTH.title)).join(", ")} (e.g. \`memgineering pull --brain ${oneLine(brains[0]?.name ?? "", WIDTH.title)}\`)`
12281
13202
  );
12282
13203
  }
12283
13204
  return {
@@ -12294,7 +13215,7 @@ async function resolveSource(named) {
12294
13215
  }
12295
13216
  function sayWhatIsElsewhere(others, into) {
12296
13217
  if (others.length === 0) return;
12297
- const names = others.map(stripControl);
13218
+ const names = others.map((name) => oneLine(name, WIDTH.title));
12298
13219
  printHuman(
12299
13220
  c.yellow(
12300
13221
  `
@@ -12392,7 +13313,7 @@ If your notes are in one of those, that rule is why \u2014 nothing here can see
12392
13313
  return;
12393
13314
  }
12394
13315
  const destination = await resolveDestination(opts.brain, local.root);
12395
- progress(`uploading ${paths.length} note(s) to \u201C${stripControl(destination.name)}\u201D\u2026`);
13316
+ progress(`uploading ${paths.length} note(s) to \u201C${oneLine(destination.name, WIDTH.title)}\u201D\u2026`);
12396
13317
  const uploaded = [];
12397
13318
  const already = [];
12398
13319
  const refused = [];
@@ -12462,7 +13383,9 @@ If your notes are in one of those, that rule is why \u2014 nothing here can see
12462
13383
  },
12463
13384
  human: () => {
12464
13385
  printHuman(
12465
- c.green(`\u2713 ${uploaded.length} note(s) uploaded to \u201C${stripControl(destination.name)}\u201D`)
13386
+ c.green(
13387
+ `\u2713 ${uploaded.length} note(s) uploaded to \u201C${oneLine(destination.name, WIDTH.title)}\u201D`
13388
+ )
12466
13389
  );
12467
13390
  if (already.length > 0) {
12468
13391
  printHuman(c.gray(` ${already.length} were already there \u2014 left as they are`));
@@ -12480,7 +13403,9 @@ If your notes are in one of those, that rule is why \u2014 nothing here can see
12480
13403
  \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
13404
  );
12482
13405
  for (const r of refused.slice(0, 10)) {
12483
- printHuman(c.gray(` ${stripControl(r.path)} (${stripControl(r.code)})`));
13406
+ printHuman(
13407
+ c.gray(` ${oneLine(r.path, WIDTH.path)} (${oneLine(r.code, WIDTH.title)})`)
13408
+ );
12484
13409
  }
12485
13410
  if (refused.length > 10) {
12486
13411
  printHuman(c.gray(` \u2026 and ${refused.length - 10} more`));
@@ -12502,14 +13427,14 @@ Nothing was deleted from \`${local.root}\` \u2014
12502
13427
  function printRepoint(repoint, destination) {
12503
13428
  if (repoint === null) {
12504
13429
  printHuman(c.gray(`
12505
- This machine still reads \u201C${stripControl(destination.name)}\u201D.`));
13430
+ This machine still reads \u201C${oneLine(destination.name, WIDTH.title)}\u201D.`));
12506
13431
  return;
12507
13432
  }
12508
13433
  if (repoint.from === null) {
12509
13434
  printHuman(
12510
13435
  c.gray(
12511
13436
  `
12512
- This machine now reads \u201C${stripControl(destination.name)}\u201D \u2014 it was not reading a hosted
13437
+ This machine now reads \u201C${oneLine(destination.name, WIDTH.title)}\u201D \u2014 it was not reading a hosted
12513
13438
  brain before. \`memgineering recall --local\` still reads the folder.`
12514
13439
  )
12515
13440
  );
@@ -12518,9 +13443,9 @@ brain before. \`memgineering recall --local\` still reads the folder.`
12518
13443
  printHuman(
12519
13444
  c.yellow(
12520
13445
  `
12521
- \u26A0 THIS MACHINE NOW READS \u201C${stripControl(destination.name)}\u201D \u2014 it was reading \u201C${stripControl(repoint.from.name)}\u201D.`
13446
+ \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
13447
  ) + 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."
13448
+ "\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
13449
  )
12525
13450
  );
12526
13451
  }
@@ -12573,7 +13498,10 @@ init_errors();
12573
13498
  init_vault();
12574
13499
  init_ui();
12575
13500
  function recallCommand() {
12576
- return new Command18("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--local", "search the brain on this machine, even when signed in to a hosted one").option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option(
13501
+ return new Command18("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--local", "search the brain on this machine, even when signed in to a hosted one").option(
13502
+ "--brain <name>",
13503
+ "search one existing hosted brain by name or id, without changing the pointer"
13504
+ ).option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option(
12577
13505
  "--detail <level>",
12578
13506
  "title | card | summary | chunks | full (default: card) \u2014 a hosted brain answers title and card only; deeper needs a brain in a folder, or `open <handle>`"
12579
13507
  ).option("--depth <depth>", "exact | standard (default: standard, which also searches bodies)").option("--budget <tokens>", "max estimated size of the answer", (v) => parseInt(v, 10)).action(
@@ -12582,10 +13510,13 @@ function recallCommand() {
12582
13510
  const depth = parseDepth(opts.depth);
12583
13511
  const budget = parseBudget(opts.budget) ?? DEFAULT_TOKEN_BUDGET;
12584
13512
  const target = await resolveTarget({
13513
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
12585
13514
  ...opts.local === void 0 ? {} : { local: opts.local },
12586
13515
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
12587
13516
  });
13517
+ const answeredBy = target.kind === "cloud" ? target.name : null;
12588
13518
  let rows;
13519
+ let unsearchedBrains = [];
12589
13520
  let isScaffolding;
12590
13521
  let reasons;
12591
13522
  let pathOf;
@@ -12613,6 +13544,9 @@ function recallCommand() {
12613
13544
  budget
12614
13545
  });
12615
13546
  rows = answered.memory_cards.map((card) => ({ card, handle: card.handle, stage: {} }));
13547
+ if (rows.length === 0) {
13548
+ unsearchedBrains = await listCloudBrains(cloud.token, cloud.apiUrl, { retries: 0 }).then((all) => all.filter((b) => b.id !== cloud.brainId)).catch(() => []);
13549
+ }
12616
13550
  isScaffolding = () => false;
12617
13551
  reasons = /* @__PURE__ */ new Map();
12618
13552
  pathOf = () => null;
@@ -12677,6 +13611,7 @@ function recallCommand() {
12677
13611
  json: {
12678
13612
  query,
12679
13613
  detail,
13614
+ ...answeredBy === null ? {} : { brain: { hosted: true, name: answeredBy } },
12680
13615
  // Echoed only when it was asked for. `detail` above is resolved
12681
13616
  // here because this CLI is what assembles the stages, but the depth
12682
13617
  // is the searching brain's to default — and on a hosted brain this
@@ -12709,6 +13644,7 @@ function recallCommand() {
12709
13644
  ...result.truncated === void 0 ? {} : { truncated: result.truncated }
12710
13645
  },
12711
13646
  human: () => {
13647
+ const brainHeading = answeredBy === null ? "" : ` \u2014 ${oneLine(answeredBy, WIDTH.title)}`;
12712
13648
  const reachedSomethingWritten = rows.some(({ card }) => !isScaffolding(card));
12713
13649
  const otherScript = reachedSomethingWritten ? null : entries ? unreachableScript(query, entries) : null;
12714
13650
  const sayWhy = () => {
@@ -12727,12 +13663,12 @@ function recallCommand() {
12727
13663
  if (semantic.state !== "skipped" || semantic.hint === void 0) return;
12728
13664
  printHuman(
12729
13665
  c.yellow("\u26A0 Meaning was not searched \u2014 this answer matched words only.") + c.gray(`
12730
- ${stripControl(semantic.hint)}
13666
+ ${oneLine(semantic.hint, WIDTH.text)}
12731
13667
  `)
12732
13668
  );
12733
13669
  };
12734
13670
  if (rows.length === 0) {
12735
- printHuman(`## recall: ${query}
13671
+ printHuman(`## recall: ${query}${brainHeading}
12736
13672
  `);
12737
13673
  printHuman("_Nothing matched._\n");
12738
13674
  sayDegraded();
@@ -12740,14 +13676,38 @@ function recallCommand() {
12740
13676
  sayWhy();
12741
13677
  return;
12742
13678
  }
13679
+ if (unsearchedBrains.length > 0) {
13680
+ printHuman(
13681
+ c.yellow(
13682
+ `\u26A0 Only \`${oneLine(answeredBy ?? "", WIDTH.title)}\` was searched \u2014 this account has ${unsearchedBrains.length + 1} brains.`
13683
+ ) + c.gray(
13684
+ `
13685
+ ${unsearchedBrains.map(
13686
+ (brain) => ` memgineering recall ${JSON.stringify(query)} --brain ${JSON.stringify(brain.name)}`
13687
+ ).join(
13688
+ "\n"
13689
+ )}
13690
+ Safe to run \u2014 reading another brain does not change which one this machine points at.
13691
+ `
13692
+ )
13693
+ );
13694
+ }
12743
13695
  printHuman(
12744
13696
  c.gray(
12745
- depth === "exact" ? "`--depth exact` searches the card only \u2014 titles, aliases, entities, summaries and scope \u2014 try `--depth standard` to search note bodies too, or fewer words, or drop --scope." : "Titles, aliases, summaries and note bodies are all searched \u2014 try fewer or broader words, or drop --scope."
13697
+ // "Within this brain" is load-bearing when the warning above
13698
+ // has just said another brain went unsearched. The enumeration
13699
+ // reads as the whole surface — a recheck tester called this
13700
+ // the last line on screen and a false all-clear, sitting below
13701
+ // the one line that says otherwise.
13702
+ (depth === "exact" ? "Within this brain, `--depth exact` searches the card only \u2014 titles, aliases, entities, summaries and scope \u2014 try `--depth standard` to search note bodies too, or fewer words" : "Within this brain, titles, aliases, summaries and note bodies are all searched \u2014 try fewer or broader words") + // Only when the reader actually passed one. Telling somebody
13703
+ // to drop an argument they never supplied cost one tester a
13704
+ // whole command to `--help` to find out it was not their bug.
13705
+ (opts.scope ? ", or drop --scope." : ".")
12746
13706
  )
12747
13707
  );
12748
13708
  return;
12749
13709
  }
12750
- printHuman(`## recall: ${query} (${rows.length} cards)
13710
+ printHuman(`## recall: ${query}${brainHeading} (${rows.length} cards)
12751
13711
  `);
12752
13712
  sayDegraded();
12753
13713
  if (!reachedSomethingWritten) {
@@ -12763,7 +13723,22 @@ function recallCommand() {
12763
13723
  );
12764
13724
  for (const [i, { card, handle: handle2, stage }] of rows.entries()) {
12765
13725
  const mark = reachedSomethingWritten && isScaffolding(card);
12766
- printHuman(renderCard(i + 1, card, handle2, detail, stage, i === glossOn, mark));
13726
+ printHuman(
13727
+ renderCard(
13728
+ i + 1,
13729
+ card,
13730
+ handle2,
13731
+ detail,
13732
+ stage,
13733
+ i === glossOn,
13734
+ mark,
13735
+ // Only when `--brain` aimed this elsewhere. A recall of the
13736
+ // brain this machine already points at mints handles `open`
13737
+ // resolves by default, and repeating the name there would be
13738
+ // noise on the common path.
13739
+ opts.brain === void 0 ? void 0 : answeredBy ?? void 0
13740
+ )
13741
+ );
12767
13742
  }
12768
13743
  if (result.conflicts.length > 0) {
12769
13744
  printHuman(`### \u26A0 contradictions (${result.conflicts.length})
@@ -12775,13 +13750,11 @@ function recallCommand() {
12775
13750
  for (const id of conflict.between) {
12776
13751
  const reason = reasons.get(id);
12777
13752
  if (!reason) {
12778
- printHuman(c.gray(` ${stripControl(id)} \u2014 no reason was recorded.`));
13753
+ printHuman(c.gray(` ${oneLine(id, WIDTH.handle)} \u2014 no reason was recorded.`));
12779
13754
  continue;
12780
13755
  }
12781
13756
  printHuman(
12782
- stripControl(
12783
- ` ${id} \u2014 ${reason.verb === "remember" ? "why it was recorded" : "why it last changed"} (${reason.ts.slice(0, 10)}):`
12784
- )
13757
+ ` ${oneLine(id, WIDTH.handle)} \u2014 ${reason.verb === "remember" ? "why it was recorded" : "why it last changed"} (${reason.ts.slice(0, 10)}):`
12785
13758
  );
12786
13759
  printHuman(quoteBlock(reason.text, " > "));
12787
13760
  }
@@ -12797,8 +13770,10 @@ function recallCommand() {
12797
13770
  );
12798
13771
  }
12799
13772
  if (result.related.length > 0) {
12800
- printHuman(`_related:_ ${result.related.map(stripControl).join(", ")}
12801
- `);
13773
+ printHuman(
13774
+ `_related:_ ${result.related.map((id) => oneLine(id, WIDTH.handle)).join(", ")}
13775
+ `
13776
+ );
12802
13777
  }
12803
13778
  if (result.truncated) {
12804
13779
  const { withheld, estimated_tokens, budget_tokens } = result.truncated;
@@ -12852,9 +13827,8 @@ function asksInSentences(query) {
12852
13827
  return query.trim().split(/\s+/).filter(Boolean).length >= 4;
12853
13828
  }
12854
13829
  function side(id, status, validFrom2) {
12855
- return stripControl(
12856
- `${id} (${status}${validFrom2 === null ? "" : ` \xB7 ${validFrom2.slice(0, 10)}`})`
12857
- );
13830
+ const date = validFrom2 === null ? "" : ` \xB7 ${oneLine(validFrom2.slice(0, 10), WIDTH.handle)}`;
13831
+ return `${oneLine(id, WIDTH.handle)} (${oneLine(status, WIDTH.title)}${date})`;
12858
13832
  }
12859
13833
  async function conflictReasons(vaultRoot, conflicts, byId) {
12860
13834
  const { ops } = await readOps(vaultRoot);
@@ -12869,11 +13843,11 @@ async function stageFor(adapter, entry, detail) {
12869
13843
  if (!entry) return {};
12870
13844
  return assembleStage(adapter, entry, detail);
12871
13845
  }
12872
- function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffolding = false) {
13846
+ function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffolding = false, qualifyWith) {
12873
13847
  const out = [];
12874
13848
  const standing = card.freshness === "current" ? "" : ` \xB7 ${card.freshness}`;
12875
13849
  const age = Number.isFinite(card.age_days) ? ` \xB7 ${ago2(Math.floor(card.age_days))}` : "";
12876
- out.push(`### ${n}. ${stripControl(card.title)}`);
13850
+ out.push(`### ${n}. ${oneLine(card.title, WIDTH.title)}`);
12877
13851
  if (scaffolding) {
12878
13852
  out.push(c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote"));
12879
13853
  } else {
@@ -12881,14 +13855,15 @@ function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffold
12881
13855
  card.binding ? c.bold("a standing decision") : null,
12882
13856
  `${card.confidence}${standing}${age}`
12883
13857
  ].filter(Boolean);
12884
- out.push(stripControl(marks.join(c.gray(" \xB7 "))));
13858
+ out.push(oneLine(marks.join(c.gray(" \xB7 ")), WIDTH.text));
12885
13859
  }
12886
13860
  if (glossHere && !scaffolding) {
12887
13861
  out.push(c.gray("stated = these fields came from the note, not from inference"));
12888
13862
  }
12889
13863
  if (detail !== "title") {
12890
13864
  if (card.summary) out.push("", quoteBlock(card.summary));
12891
- out.push("", `\`open: ${stripControl(handle2)}\``);
13865
+ const openLine = qualifyWith === void 0 ? `open: ${oneLine(handle2, WIDTH.handle)}` : `memgineering open ${oneLine(handle2, WIDTH.handle)} --brain ${JSON.stringify(qualifyWith)}`;
13866
+ out.push("", `\`${openLine}\``);
12892
13867
  }
12893
13868
  if (stage.outline && stage.outline.length > 0) {
12894
13869
  out.push("", ...stage.outline.map((h) => `- ${h}`));
@@ -13019,10 +13994,10 @@ function rememberCommand() {
13019
13994
  ...kindLanded ? {} : { kind_dropped: kind }
13020
13995
  },
13021
13996
  human: () => {
13022
- printHuman(`Remembered in ${stripControl(target.name)}.
13997
+ printHuman(`Remembered in ${oneLine(target.name, WIDTH.title)}.
13023
13998
  `);
13024
13999
  printHuman(
13025
- `note: \`${stripControl(written.id)}\` \xB7 \`${stripControl(written.path)}\``
14000
+ `note: \`${oneLine(written.id, WIDTH.handle)}\` \xB7 \`${oneLine(written.path, WIDTH.path)}\``
13026
14001
  );
13027
14002
  if (opts.rule === true) {
13028
14003
  printHuman("rule: shown before you write a file here, once a session");
@@ -13034,7 +14009,9 @@ function rememberCommand() {
13034
14009
  )
13035
14010
  );
13036
14011
  }
13037
- printHuman(c.gray(`undo: \`memgineering undo ${stripControl(written.op_id)}\``));
14012
+ printHuman(
14013
+ c.gray(`undo: \`memgineering undo ${oneLine(written.op_id, WIDTH.handle)}\``)
14014
+ );
13038
14015
  }
13039
14016
  });
13040
14017
  return;
@@ -13151,6 +14128,9 @@ init_vault();
13151
14128
  init_ui();
13152
14129
  function resurfaceCommand() {
13153
14130
  return new Command20("resurface").description("what is worth having in view right now, without being asked").option("--context-dir <path>", "where the work is happening (default: cwd)").option("--limit <n>", "how many cards", (v) => parseInt(v, 10), 5).option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
14131
+ "--brain <name>",
14132
+ "read one existing hosted brain by name or id, without changing the pointer"
14133
+ ).option(
13154
14134
  "--deadline <ms>",
13155
14135
  "give up and print nothing after this long",
13156
14136
  (v) => parseInt(v, 10)
@@ -13160,11 +14140,16 @@ function resurfaceCommand() {
13160
14140
  await bounded(async () => {
13161
14141
  const contextDir = opts.contextDir ?? process.cwd();
13162
14142
  const where = await resolveTarget({
14143
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
13163
14144
  ...opts.local === void 0 ? {} : { local: opts.local },
13164
14145
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
13165
14146
  });
13166
14147
  if (where.kind === "cloud") {
13167
- await resurfaceHosted(where, opts.limit);
14148
+ await resurfaceHosted(
14149
+ where,
14150
+ opts.limit,
14151
+ opts.brain === void 0 ? void 0 : where.name
14152
+ );
13168
14153
  return;
13169
14154
  }
13170
14155
  const { brain, unreachableDefault } = await resolveBrain(opts.vault, contextDir);
@@ -13235,9 +14220,7 @@ function resurfaceCommand() {
13235
14220
  `);
13236
14221
  if (unreachableDefault) {
13237
14222
  printHuman(
13238
- c.yellow(
13239
- `Your default brain is not on disk right now: ${unreachableDefault}`
13240
- )
14223
+ c.yellow(`Your default brain is not on disk right now: ${unreachableDefault}`)
13241
14224
  );
13242
14225
  printHuman(
13243
14226
  c.gray(
@@ -13251,7 +14234,7 @@ function resurfaceCommand() {
13251
14234
  );
13252
14235
  for (const [i, { entry, why }] of rankedShown.entries()) {
13253
14236
  const blank = untouched.has(entry.path);
13254
- printHuman(`### ${i + 1}. ${stripControl(entry.memory.title)}`);
14237
+ printHuman(`### ${i + 1}. ${oneLine(entry.memory.title, WIDTH.title)}`);
13255
14238
  printHuman(blank ? c.yellow("not filled in yet") : why.join(" \xB7 "));
13256
14239
  if (blank) {
13257
14240
  const hasWritten = rankedShown.some((r) => !untouched.has(r.entry.path));
@@ -13281,7 +14264,7 @@ ${quoteBlock(entry.memory.summary)}`);
13281
14264
  }
13282
14265
  );
13283
14266
  }
13284
- async function resurfaceHosted(where, limit) {
14267
+ async function resurfaceHosted(where, limit, qualifyWith) {
13285
14268
  const brain = await openCloudBrain(where);
13286
14269
  const result = await cloudResurface(brain, { limit });
13287
14270
  printDual({
@@ -13293,31 +14276,35 @@ async function resurfaceHosted(where, limit) {
13293
14276
  },
13294
14277
  human: () => {
13295
14278
  if (result.cards.length === 0) {
13296
- printHuman(`## resurface \u2014 ${stripControl(where.name)}
14279
+ printHuman(`## resurface \u2014 ${oneLine(where.name, WIDTH.title)}
13297
14280
  `);
13298
14281
  printHuman("_Nothing to bring up yet._\n");
13299
- if (result.hint) printHuman(c.gray(stripControl(result.hint)));
14282
+ if (result.hint) printHuman(c.gray(oneLine(result.hint, WIDTH.text)));
13300
14283
  return;
13301
14284
  }
13302
- printHuman(`## resurface \u2014 ${stripControl(where.name)} (${result.cards.length} cards)
13303
- `);
14285
+ printHuman(
14286
+ `## resurface \u2014 ${oneLine(where.name, WIDTH.title)} (${result.cards.length} cards)
14287
+ `
14288
+ );
13304
14289
  for (const [i, card] of result.cards.entries()) {
13305
- printHuman(`### ${i + 1}. ${stripControl(card.title)}`);
14290
+ printHuman(`### ${i + 1}. ${oneLine(card.title, WIDTH.title)}`);
13306
14291
  if (card.untouched === true) printHuman(c.yellow("not filled in yet"));
13307
- else if (card.why.length > 0) printHuman(stripControl(card.why.join(" \xB7 ")));
14292
+ else if (card.why.length > 0) printHuman(oneLine(card.why.join(" \xB7 "), WIDTH.text));
13308
14293
  if (card.summary) printHuman(`
13309
14294
  ${quoteBlock(card.summary)}`);
13310
- printHuman(`
13311
- \`open: ${stripControl(card.handle)}\`
13312
- `);
14295
+ printHuman(
14296
+ qualifyWith === void 0 ? `
14297
+ \`open: ${oneLine(card.handle, WIDTH.handle)}\`
14298
+ ` : `
14299
+ \`memgineering open ${oneLine(card.handle, WIDTH.handle)} --brain ${JSON.stringify(qualifyWith)}\`
14300
+ `
14301
+ );
13313
14302
  }
13314
- printHuman(
14303
+ printBlock(
13315
14304
  c.gray(
13316
- stripControl(
13317
- `Ranked by ${result.ranked_by.join(", ")}. A hosted brain does not record where a
14305
+ `Ranked by ${oneLine(result.ranked_by.join(", "), WIDTH.text)}. A hosted brain does not record where a
13318
14306
  question was asked from, so "in this folder" is not one of them \u2014 that signal
13319
14307
  exists only for a brain on this machine.`
13320
- )
13321
14308
  )
13322
14309
  );
13323
14310
  }
@@ -13328,8 +14315,12 @@ exists only for a brain on this machine.`
13328
14315
  import { Command as Command21 } from "commander";
13329
14316
  init_ui();
13330
14317
  function rulesCommand() {
13331
- return new Command21("rules").description("the user\u2019s standing decisions \u2014 what binds you here").option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (opts) => {
14318
+ return new Command21("rules").description("the user\u2019s standing decisions \u2014 what binds you here").option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
14319
+ "--brain <name>",
14320
+ "read one existing hosted brain by name or id, without changing the pointer"
14321
+ ).action(async (opts) => {
13332
14322
  const where = await resolveTarget({
14323
+ ...opts.brain === void 0 ? {} : { brain: opts.brain },
13333
14324
  ...opts.local === void 0 ? {} : { local: opts.local },
13334
14325
  ...opts.vault === void 0 ? {} : { vault: opts.vault }
13335
14326
  });
@@ -13338,7 +14329,7 @@ function rulesCommand() {
13338
14329
  printDual({
13339
14330
  json: { brain: where.name, hosted: true, rules: rules2, total: total2 },
13340
14331
  human: () => {
13341
- printHuman(`## rules \u2014 ${stripControl(where.name)} (${total2})
14332
+ printHuman(`## rules \u2014 ${oneLine(where.name, WIDTH.title)} (${total2})
13342
14333
  `);
13343
14334
  if (rules2.length === 0) {
13344
14335
  printHuman("_Nothing is marked as a rule in this brain._\n");
@@ -13350,8 +14341,8 @@ function rulesCommand() {
13350
14341
  return;
13351
14342
  }
13352
14343
  for (const rule of rules2) {
13353
- printHuman(`- ${stripControl(rule.summary ?? rule.title)}`);
13354
- printHuman(c.gray(` \`open: ${stripControl(rule.handle)}\``));
14344
+ printHuman(`- ${oneLine(rule.summary ?? rule.title, WIDTH.text)}`);
14345
+ printHuman(c.gray(` \`open: ${oneLine(rule.handle, WIDTH.handle)}\``));
13355
14346
  }
13356
14347
  if (total2 > rules2.length) {
13357
14348
  printHuman(c.gray(`
@@ -13403,10 +14394,10 @@ This answered from \`${brainRoot}\` instead, so rules you wrote in the missing o
13403
14394
  printHuman(`## rules (${rules.length}${total > rules.length ? ` of ${total}` : ""})
13404
14395
  `);
13405
14396
  for (const { entry } of rules) {
13406
- printHuman(`- ${stripControl(entry.memory.summary ?? entry.memory.title)}`);
14397
+ printHuman(`- ${oneLine(entry.memory.summary ?? entry.memory.title, WIDTH.text)}`);
13407
14398
  printHuman(
13408
14399
  c.gray(
13409
- ` \`open: ${displayHandle(entry.memory.id, allIds)}\`${entry.memory.scope ? ` \xB7 ${stripControl(entry.memory.scope)}` : ""}`
14400
+ ` \`open: ${displayHandle(entry.memory.id, allIds)}\`${entry.memory.scope ? ` \xB7 ${oneLine(entry.memory.scope, WIDTH.title)}` : ""}`
13410
14401
  )
13411
14402
  );
13412
14403
  }
@@ -13768,7 +14759,7 @@ async function reportPreviousAttempt() {
13768
14759
  function reportNotice(text) {
13769
14760
  if (!text) return;
13770
14761
  printHuman(c.gray(`
13771
- note from memgineering.com: "${stripControl(text)}"`));
14762
+ note from memgineering.com: "${oneLine(text, WIDTH.text)}"`));
13772
14763
  }
13773
14764
  function reportBelowMinimum(below) {
13774
14765
  if (!below) return;
@@ -13806,11 +14797,11 @@ function undoCommand() {
13806
14797
  json: { ...undone, brain: { hosted: true, name: where.name } },
13807
14798
  human: () => {
13808
14799
  printHuman(
13809
- `Took back \`${stripControl(undone.undone)}\` \u2014 a ${stripControl(undone.verb)}.
14800
+ `Took back \`${oneLine(undone.undone, WIDTH.handle)}\` \u2014 a ${oneLine(undone.verb, WIDTH.title)}.
13810
14801
  `
13811
14802
  );
13812
- printHuman(c.gray(` ${stripControl(undone.target)}`));
13813
- printHuman(c.gray(` recorded as \`${stripControl(undone.op_id)}\``));
14803
+ printHuman(c.gray(` ${oneLine(undone.target, WIDTH.path)}`));
14804
+ printHuman(c.gray(` recorded as \`${oneLine(undone.op_id, WIDTH.handle)}\``));
13814
14805
  }
13815
14806
  });
13816
14807
  return;
@@ -13896,7 +14887,7 @@ ${matches.slice(0, 8).map((id) => ` ${id}`).join("\n")}${matches.length > 8 ? "
13896
14887
  },
13897
14888
  human: () => {
13898
14889
  printHuman(
13899
- `Undone: ${stripControl(target.verb)} on \`${stripControl(target.target)}\`
14890
+ `Undone: ${oneLine(target.verb, WIDTH.title)} on \`${oneLine(target.target, WIDTH.path)}\`
13900
14891
  `
13901
14892
  );
13902
14893
  if (opId === void 0) {
@@ -13987,13 +14978,95 @@ Read what it says now, then decide what should stay.`;
13987
14978
  The text from before that change is still on this machine: ${join23(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
13988
14979
  }
13989
14980
 
14981
+ // src/commands/brains.ts
14982
+ init_errors();
14983
+ init_api_client();
14984
+ import { Command as Command25 } from "commander";
14985
+ import stringWidth from "string-width";
14986
+ init_config();
14987
+ init_ui();
14988
+ var NAME_WIDTH = 48;
14989
+ function brainsCommand() {
14990
+ return new Command25("brains").description("list the hosted brains on your account, and which one this machine reads").addHelpText(
14991
+ "after",
14992
+ "\nReads only. `memgineering use --brain <name>` points this machine at one of\nthem; `memgineering push --brain <name>` is what creates a new one."
14993
+ ).action(async () => {
14994
+ const base = apiUrl();
14995
+ const credentials = await requireCredentials(base);
14996
+ const brains = await listCloudBrains(credentials.token, base);
14997
+ const pointer = (await loadConfig()).brain.cloud;
14998
+ const currentId = pointer?.api_url === base ? pointer.id : null;
14999
+ if (brains.length === 0) {
15000
+ throw memgError(
15001
+ "invalid_input",
15002
+ "this account has no hosted brains",
15003
+ "Nothing is wrong \u2014 none has been made yet. `memgineering push --brain <name>` creates one and fills it from a folder on this machine."
15004
+ );
15005
+ }
15006
+ const rows = brains.map((b) => ({
15007
+ id: b.id,
15008
+ name: b.name,
15009
+ // Passed through rather than defaulted to 0. A server that does not send
15010
+ // a count and a brain that holds no notes are different facts, and the
15011
+ // second is alarming.
15012
+ note_count: b.note_count ?? null,
15013
+ created_at: b.created_at ?? null,
15014
+ current: b.id === currentId
15015
+ }));
15016
+ printDual({
15017
+ json: {
15018
+ // WHOSE ACCOUNT, on the command that lists what the account holds.
15019
+ //
15020
+ // "What is this computer connected to?" took three commands to answer:
15021
+ // `whoami` for the identity, this for the brains, `use` for the folder
15022
+ // — and `whoami`, the one named after the account, was the only one
15023
+ // that answered none of the rest. Two round-test agents ran all three.
15024
+ // The name is free here; it came back with the token.
15025
+ account: { id: credentials.account_id, name: credentials.display_name },
15026
+ server: base,
15027
+ brains: rows,
15028
+ count: rows.length,
15029
+ // Named at the top level too, so a caller does not have to scan the
15030
+ // array to answer the question it most often has.
15031
+ current: currentId,
15032
+ 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."
15033
+ },
15034
+ human: () => {
15035
+ printHuman(
15036
+ `## ${rows.length} brain${rows.length === 1 ? "" : "s"} on ${credentials.display_name === null ? "this account" : `${oneLine(credentials.display_name, NAME_WIDTH)}\u2019s account`}
15037
+ `
15038
+ );
15039
+ const shownNames = rows.map((row) => oneLine(row.name, NAME_WIDTH));
15040
+ const shownNameWidth = Math.max(...shownNames.map((name) => stringWidth(name)));
15041
+ for (const [index, row] of rows.entries()) {
15042
+ const shownName = shownNames[index] ?? "";
15043
+ const name = shownName + " ".repeat(shownNameWidth - stringWidth(shownName));
15044
+ const notes = row.note_count === null ? "" : `${String(row.note_count).padStart(6)} note(s)`;
15045
+ const made = row.created_at === null ? "" : ` made ${row.created_at.slice(0, 10)}`;
15046
+ printHuman(
15047
+ ` ${name}${c.gray(notes)}${c.gray(made)}` + (row.current ? c.yellow(" \u2190 this machine reads this one") : "")
15048
+ );
15049
+ }
15050
+ printHuman("");
15051
+ printHuman(
15052
+ c.gray(
15053
+ 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."
15054
+ )
15055
+ );
15056
+ }
15057
+ });
15058
+ });
15059
+ }
15060
+
13990
15061
  // src/commands/use.ts
13991
15062
  init_atomic();
13992
15063
  init_brand();
13993
15064
  init_errors();
13994
- init_config();
15065
+ init_api_client();
15066
+ init_credentials();
13995
15067
  import { isAbsolute as isAbsolute5, relative as relative3, resolve as resolve11 } from "path";
13996
- import { Command as Command25 } from "commander";
15068
+ import { Command as Command26 } from "commander";
15069
+ init_config();
13997
15070
  init_vault();
13998
15071
  init_ui();
13999
15072
  function lostDefaultLines(missing) {
@@ -14012,16 +15085,24 @@ var VIA_MEANS = {
14012
15085
  default: "because it is your default brain (`memgineering use --default <path>` changes it)"
14013
15086
  };
14014
15087
  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(
15088
+ return new Command26("use").description(
15089
+ "say which brain answers here \u2014 `--brain <name>` for the whole machine, or a path to bind this folder"
15090
+ ).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
15091
  "--default",
14017
- "make this the brain used anywhere no pointer applies, instead of binding one folder"
15092
+ "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
15093
  ).action(async (brainPath, opts) => {
15094
+ if (opts.brain !== void 0) {
15095
+ await pointAtHosted(opts.brain, opts, brainPath);
15096
+ return;
15097
+ }
14019
15098
  const where = await canonicalize(resolve11(opts.here ?? process.cwd()));
14020
15099
  if (!brainPath) {
14021
15100
  const pointer = await readPointer(where);
14022
15101
  const resolved = await resolveBrain(void 0, where).catch((err) => err);
14023
15102
  const stuck = resolved instanceof Error;
14024
15103
  const hosted = await hostedPointer();
15104
+ const stored = await readCredentials().catch(() => null);
15105
+ const signedIn = stored !== null && stored.api_url === apiUrl();
14025
15106
  printDual({
14026
15107
  json: {
14027
15108
  // The brain that answers `recall` and takes `remember` — the same
@@ -14056,14 +15137,24 @@ function useCommand() {
14056
15137
  brain: stuck ? null : resolved.brain.root,
14057
15138
  via: stuck ? null : resolved.via,
14058
15139
  pointer: pointer ? pointer.from : null,
14059
- ...stuck ? {
15140
+ // ONLY WHEN THE LOCAL FOLDER IS THE ANSWER. `stuck` describes the
15141
+ // FOLDER resolver, and on a machine reading a hosted brain that
15142
+ // resolver having nothing is the expected state, not a problem —
15143
+ // but it was reported unqualified beside `destination: "hosted"`,
15144
+ // so a successful switch produced `problem: "no_vault_linked"` and
15145
+ // `message: "no brain is linked yet"` in the same document that
15146
+ // named the brain it had just moved to. Two agents read `problem`
15147
+ // first and would have told the user the switch failed.
15148
+ //
15149
+ // `--local` is how somebody asks what the folder side would do.
15150
+ ...stuck && hosted?.signedIn !== true ? {
14060
15151
  problem: resolved.code ?? "unresolved",
14061
15152
  message: resolved.message
14062
15153
  } : {}
14063
15154
  },
14064
15155
  human: () => {
14065
15156
  if (hosted?.signedIn) {
14066
- printHuman(`Using the hosted brain \`${stripControl(hosted.name)}\`
15157
+ printHuman(`Using the hosted brain \`${oneLine(hosted.name, NAME_WIDTH2)}\`
14067
15158
  `);
14068
15159
  printHuman(
14069
15160
  c.gray(
@@ -14084,7 +15175,7 @@ function useCommand() {
14084
15175
  );
14085
15176
  printHuman(
14086
15177
  c.yellow(
14087
- ` A sign-in to \`${stripControl(hosted.name)}\` is waiting to be approved.`
15178
+ ` A sign-in to \`${oneLine(hosted.name, NAME_WIDTH2)}\` is waiting to be approved.`
14088
15179
  )
14089
15180
  );
14090
15181
  printHuman(
@@ -14101,7 +15192,9 @@ function useCommand() {
14101
15192
  printHuman(`No brain resolves here.
14102
15193
  `);
14103
15194
  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.")
15195
+ c.yellow(
15196
+ ` Pointed at the hosted brain \`${oneLine(hosted.name, NAME_WIDTH2)}\`,`
15197
+ ) + 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
15198
  );
14106
15199
  return;
14107
15200
  }
@@ -14110,7 +15203,7 @@ function useCommand() {
14110
15203
  printHuman(c.gray(` ${VIA_MEANS[resolved.via]}`));
14111
15204
  printHuman(
14112
15205
  c.yellow(
14113
- ` This machine is pointed at the hosted brain \`${stripControl(hosted.name)}\` and is not signed in to it,`
15206
+ ` This machine is pointed at the hosted brain \`${oneLine(hosted.name, NAME_WIDTH2)}\` and is not signed in to it,`
14114
15207
  ) + c.gray("\n so reads and writes are landing here instead.")
14115
15208
  );
14116
15209
  printHuman(
@@ -14121,6 +15214,14 @@ function useCommand() {
14121
15214
  if (stuck) {
14122
15215
  printHuman(`No brain resolves here.
14123
15216
  `);
15217
+ if (signedIn) {
15218
+ printHuman(
15219
+ c.yellow(" This machine is signed in, and pointed at no brain.") + c.gray(
15220
+ "\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."
15221
+ )
15222
+ );
15223
+ return;
15224
+ }
14124
15225
  printHuman(c.gray(` ${resolved.message}`));
14125
15226
  const hint = resolved.hint;
14126
15227
  if (hint) printHuman(c.gray(`
@@ -14187,13 +15288,114 @@ A pointer to an unlinked folder would fail on every command instead of at this o
14187
15288
  });
14188
15289
  });
14189
15290
  }
15291
+ var NAME_WIDTH2 = 72;
15292
+ function describeSize(count) {
15293
+ if (count === void 0) return "";
15294
+ if (count === 0)
15295
+ return " \u2014 which is EMPTY. Nothing here has been lost; that brain has no notes yet";
15296
+ return ` \u2014 ${count} note(s)`;
15297
+ }
15298
+ async function pointAtHosted(named, opts, path) {
15299
+ if (named.startsWith("--")) {
15300
+ throw memgError(
15301
+ "invalid_input",
15302
+ `\`--brain\` was given the flag \`${oneLine(named, 32)}\` as its name`,
15303
+ "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."
15304
+ );
15305
+ }
15306
+ const clash = path !== void 0 ? "a folder path" : opts.default === true ? "`--default`" : opts.here !== void 0 ? "`--here`" : null;
15307
+ if (clash !== null) {
15308
+ throw memgError(
15309
+ "invalid_input",
15310
+ `\`--brain\` cannot be combined with ${clash}`,
15311
+ "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."
15312
+ );
15313
+ }
15314
+ const base = apiUrl();
15315
+ const credentials = await requireCredentials(base).catch((err) => {
15316
+ if (err instanceof MemgError && err.code === "not_signed_in") {
15317
+ throw memgError(
15318
+ "not_signed_in",
15319
+ "this machine is not signed in to any account",
15320
+ "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."
15321
+ );
15322
+ }
15323
+ throw err;
15324
+ });
15325
+ const brains = await listCloudBrains(credentials.token, base);
15326
+ if (brains.length === 0) {
15327
+ throw memgError(
15328
+ "invalid_input",
15329
+ "this account has no hosted brains",
15330
+ "Nothing was changed. `memgineering push --brain <name>` creates one and fills it from a folder on this machine."
15331
+ );
15332
+ }
15333
+ const found = findCloudBrain(brains, named);
15334
+ if (!found) {
15335
+ throw memgError(
15336
+ "invalid_input",
15337
+ `no hosted brain called "${oneLine(named, NAME_WIDTH2)}"`,
15338
+ "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."
15339
+ );
15340
+ }
15341
+ const config = await loadConfig();
15342
+ const previous = config.brain.cloud ?? null;
15343
+ const unchanged = previous?.id === found.id && previous.api_url === base && previous.name === found.name;
15344
+ if (!unchanged) {
15345
+ await saveConfig({
15346
+ ...config,
15347
+ brain: { ...config.brain, cloud: { id: found.id, name: found.name, api_url: base } }
15348
+ });
15349
+ }
15350
+ printDual({
15351
+ json: {
15352
+ brain: {
15353
+ id: found.id,
15354
+ name: found.name,
15355
+ api_url: base,
15356
+ // Carried because the human line says it. A caller that has to ask a
15357
+ // second time to learn the destination is empty is a caller that will
15358
+ // not ask.
15359
+ ...found.note_count === void 0 ? {} : { note_count: found.note_count }
15360
+ },
15361
+ previous: previous ? { id: previous.id, name: previous.name } : null,
15362
+ // Present even when nothing moved, so a caller can tell "already there"
15363
+ // from "switched" without comparing two names itself.
15364
+ changed: !unchanged
15365
+ },
15366
+ human: () => {
15367
+ if (unchanged) {
15368
+ printHuman(`Already using \`${oneLine(found.name, NAME_WIDTH2)}\`.
15369
+ `);
15370
+ printHuman(c.gray(" Nothing changed."));
15371
+ return;
15372
+ }
15373
+ printHuman(
15374
+ `Now using the hosted brain \`${oneLine(found.name, NAME_WIDTH2)}\`${describeSize(found.note_count)}.
15375
+ `
15376
+ );
15377
+ if (previous) {
15378
+ printHuman(
15379
+ c.yellow(
15380
+ ` This machine was reading \`${oneLine(previous.name, NAME_WIDTH2)}\` and now is not.`
15381
+ )
15382
+ );
15383
+ }
15384
+ printHuman(
15385
+ c.gray(
15386
+ " Reads and writes go there from now on. Nothing was uploaded and nothing\n was downloaded \u2014 `push` and `pull` are what move notes."
15387
+ )
15388
+ );
15389
+ }
15390
+ });
15391
+ }
14190
15392
 
14191
15393
  // src/commands/whoami.ts
14192
15394
  init_api_client();
14193
15395
  init_ui();
14194
- import { Command as Command26 } from "commander";
15396
+ import { Command as Command27 } from "commander";
14195
15397
  function whoamiCommand() {
14196
- return new Command26("whoami").description("show which account this machine is signed in as").action(async () => {
15398
+ return new Command27("whoami").description("show which account this machine is signed in as").action(async () => {
14197
15399
  const base = apiUrl();
14198
15400
  const credentials = await requireCredentials(base);
14199
15401
  const res = await apiRequest({
@@ -14219,9 +15421,11 @@ function whoamiCommand() {
14219
15421
  },
14220
15422
  human: () => {
14221
15423
  printHuman(
14222
- c.bold(stripControl(account.display_name ?? account.id ?? credentials.account_id))
15424
+ c.bold(
15425
+ oneLine(account.display_name ?? account.id ?? credentials.account_id, WIDTH.title)
15426
+ )
14223
15427
  );
14224
- if (account.email) printHuman(c.gray(` ${stripControl(account.email)}`));
15428
+ if (account.email) printHuman(c.gray(` ${oneLine(account.email, WIDTH.title)}`));
14225
15429
  printHuman(c.gray(` on ${base}`));
14226
15430
  if (live !== null) {
14227
15431
  printHuman(
@@ -14236,6 +15440,8 @@ function whoamiCommand() {
14236
15440
  }
14237
15441
 
14238
15442
  // src/lib/next-step.ts
15443
+ init_api_client();
15444
+ init_credentials();
14239
15445
  init_config();
14240
15446
  function isSetUp(cfg) {
14241
15447
  return Object.keys(cfg.system.installed).length > 0;
@@ -14289,7 +15495,16 @@ Then run \`memgineering onboard\`: it prints what to ask so the brain knows whos
14289
15495
  it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
14290
15496
 
14291
15497
  Read \`memgineering-setup\` for accounts and which of their tools this reaches.`;
14292
- function nextStepFor(cfg) {
15498
+ var POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
15499
+
15500
+ \`memgineering brains\` lists what is on the account.
15501
+ \`memgineering use --brain <name>\` points this machine at one.
15502
+ `;
15503
+ function nextStepFor(cfg, signedIn = false) {
15504
+ if (signedIn && cfg.brain.cloud === void 0) {
15505
+ return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
15506
+ }
15507
+ if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
14293
15508
  if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
14294
15509
  if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
14295
15510
  return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
@@ -14298,7 +15513,8 @@ function nextStepFor(cfg) {
14298
15513
  }
14299
15514
  async function currentNextStep() {
14300
15515
  try {
14301
- return nextStepFor(await loadConfig());
15516
+ const stored = await readCredentials().catch(() => null);
15517
+ return nextStepFor(await loadConfig(), stored !== null && stored.api_url === apiUrl());
14302
15518
  } catch {
14303
15519
  return null;
14304
15520
  }
@@ -14308,7 +15524,7 @@ async function currentNextStep() {
14308
15524
  init_ui();
14309
15525
  init_brand();
14310
15526
  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) => {
15527
+ 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
15528
  const opts = thisCommand.optsWithGlobals();
14313
15529
  if (opts.json || process.env[BRAND.jsonEnvVar] === "1") setJsonMode(true);
14314
15530
  await announceNextStep(actionCommand.name());
@@ -14329,6 +15545,7 @@ function buildProgram() {
14329
15545
  program.addCommand(initCommand());
14330
15546
  program.addCommand(linkCommand());
14331
15547
  program.addCommand(useCommand());
15548
+ program.addCommand(brainsCommand());
14332
15549
  program.addCommand(unlinkCommand());
14333
15550
  program.addCommand(reindexCommand());
14334
15551
  program.addCommand(syncRulesCommand());
@@ -14368,8 +15585,15 @@ function makeParseErrorsActionable(program) {
14368
15585
  outputError: (str, write) => {
14369
15586
  write(str);
14370
15587
  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
15588
  const path = cmd.parent === null ? cmd.name() : `${cmd.parent.name()} ${cmd.name()}`;
15589
+ if (/unknown command/i.test(str) && cmd.commands.length > 0) {
15590
+ write(
15591
+ `hint: ${path} has ${cmd.commands.map((c2) => c2.name()).sort().join(" ")}
15592
+ `
15593
+ );
15594
+ return;
15595
+ }
15596
+ const flags = cmd.options.map((o) => o.long ?? o.short).filter(Boolean);
14373
15597
  write(
14374
15598
  flags.length > 0 ? `hint: ${path} takes ${flags.join(" ")}
14375
15599
  ` : `hint: \`${path} --help\` lists what this takes.
@@ -14397,7 +15621,7 @@ async function handleError(err) {
14397
15621
  if (isJsonMode()) {
14398
15622
  printJson({ error: { code: err.code, message: err.message, hint: err.hint } });
14399
15623
  } else {
14400
- printHuman(c.red(`\u2717 ${stripControl(err.message)}`));
15624
+ printHuman(c.red(`\u2717 ${oneLine(err.message, WIDTH.text)}`));
14401
15625
  for (const line of stripControl(err.hint).split("\n")) printHuman(c.gray(` \u2192 ${line}`));
14402
15626
  }
14403
15627
  process.exit(1);
@@ -14414,7 +15638,7 @@ async function handleError(err) {
14414
15638
  }
14415
15639
  });
14416
15640
  } else {
14417
- printHuman(c.red(`\u2717 ${stripControl(err.message)}`));
15641
+ printHuman(c.red(`\u2717 ${oneLine(err.message, WIDTH.text)}`));
14418
15642
  if (hintStr) {
14419
15643
  for (const line of stripControl(hintStr).split("\n")) printHuman(c.gray(` \u2192 ${line}`));
14420
15644
  }
@@ -14422,7 +15646,7 @@ async function handleError(err) {
14422
15646
  process.exit(1);
14423
15647
  }
14424
15648
  if (isJsonMode()) printJson({ error: { code: "generic_failure", message: String(err) } });
14425
- else printHuman(c.red(`\u2717 ${stripControl(String(err))}`));
15649
+ else printHuman(c.red(`\u2717 ${oneLine(String(err), WIDTH.text)}`));
14426
15650
  process.exit(1);
14427
15651
  }
14428
15652