zam-core 0.7.1 → 0.7.2

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/cli/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { readFileSync as readFileSync16 } from "fs";
5
5
  import { dirname as dirname10, join as join25 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5 } from "url";
7
- import { Command as Command25 } from "commander";
7
+ import { Command as Command26 } from "commander";
8
8
 
9
9
  // src/cli/commands/agent.ts
10
10
  import { existsSync as existsSync13 } from "fs";
@@ -543,6 +543,7 @@ var SCHEMA = `
543
543
  CREATE TABLE IF NOT EXISTS tokens (
544
544
  id TEXT PRIMARY KEY,
545
545
  slug TEXT UNIQUE NOT NULL,
546
+ title TEXT NOT NULL DEFAULT '',
546
547
  concept TEXT NOT NULL,
547
548
  domain TEXT NOT NULL DEFAULT '',
548
549
  bloom_level INTEGER NOT NULL DEFAULT 1 CHECK (bloom_level BETWEEN 1 AND 5),
@@ -691,6 +692,8 @@ CREATE INDEX IF NOT EXISTS idx_cards_token_user ON cards(token_id, user_id);
691
692
  CREATE INDEX IF NOT EXISTS idx_review_logs_card ON review_logs(card_id);
692
693
  CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed_at);
693
694
  CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
695
+ CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title);
696
+ CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain);
694
697
  `;
695
698
 
696
699
  // src/kernel/db/sync-adapter.ts
@@ -1024,6 +1027,15 @@ async function runMigrations(db) {
1024
1027
  embedded_at TEXT NOT NULL DEFAULT (datetime('now'))
1025
1028
  )
1026
1029
  `);
1030
+ if (!tokenCols.some((c) => c.name === "title")) {
1031
+ await db.exec(
1032
+ `ALTER TABLE tokens ADD COLUMN title TEXT NOT NULL DEFAULT ''`
1033
+ );
1034
+ }
1035
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title)`);
1036
+ await db.exec(
1037
+ `CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain)`
1038
+ );
1027
1039
  }
1028
1040
 
1029
1041
  // src/kernel/db/snapshot.ts
@@ -1516,12 +1528,14 @@ async function createToken(db, input8) {
1516
1528
  if (bloom < 1 || bloom > 5) {
1517
1529
  throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1518
1530
  }
1531
+ const title = input8.title ?? "";
1519
1532
  await db.prepare(`
1520
- INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
1521
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1533
+ INSERT INTO tokens (id, slug, title, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
1534
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1522
1535
  `).run(
1523
1536
  id,
1524
1537
  input8.slug,
1538
+ title,
1525
1539
  input8.concept,
1526
1540
  input8.domain ?? "",
1527
1541
  bloom,
@@ -1564,6 +1578,10 @@ async function updateToken(db, slug, updates) {
1564
1578
  }
1565
1579
  const fields = [];
1566
1580
  const values = [];
1581
+ if (updates.title !== void 0) {
1582
+ fields.push("title = ?");
1583
+ values.push(updates.title ?? "");
1584
+ }
1567
1585
  if (updates.concept !== void 0) {
1568
1586
  fields.push("concept = ?");
1569
1587
  values.push(updates.concept);
@@ -1669,13 +1687,14 @@ async function deleteToken(db, slug) {
1669
1687
  await db.transaction(async (tx) => {
1670
1688
  const now = (/* @__PURE__ */ new Date()).toISOString();
1671
1689
  const skillRows = await tx.prepare("SELECT id, token_slugs FROM agent_skills").all();
1690
+ const skillUpdateStmt = tx.prepare(
1691
+ "UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
1692
+ );
1672
1693
  for (const row of skillRows) {
1673
1694
  const tokenSlugs = JSON.parse(row.token_slugs);
1674
1695
  const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
1675
1696
  if (filtered.length !== tokenSlugs.length) {
1676
- await tx.prepare(
1677
- "UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
1678
- ).run(JSON.stringify(filtered), now, row.id);
1697
+ await skillUpdateStmt.run(JSON.stringify(filtered), now, row.id);
1679
1698
  }
1680
1699
  }
1681
1700
  await tx.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
@@ -1689,10 +1708,16 @@ async function findTokens(db, query) {
1689
1708
  const shortTerms = searchTokens.filter((t2) => t2.length <= 2);
1690
1709
  const longTerms = searchTokens.filter((t2) => t2.length > 2);
1691
1710
  const scoreMap = /* @__PURE__ */ new Map();
1692
- const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
1711
+ const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(title) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
1712
+ const likeStmt = db.prepare(likeSQL);
1693
1713
  for (const term of longTerms) {
1694
1714
  const pattern = `%${term}%`;
1695
- const rows = await db.prepare(likeSQL).all(pattern, pattern, pattern);
1715
+ const rows = await likeStmt.all(
1716
+ pattern,
1717
+ pattern,
1718
+ pattern,
1719
+ pattern
1720
+ );
1696
1721
  for (const row of rows) {
1697
1722
  const entry = scoreMap.get(row.id);
1698
1723
  if (entry) {
@@ -1705,7 +1730,7 @@ async function findTokens(db, query) {
1705
1730
  if (shortTerms.length > 0 || longTerms.length === 0) {
1706
1731
  const allTokens = await db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
1707
1732
  for (const token of allTokens) {
1708
- const words = `${token.slug} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
1733
+ const words = `${token.slug} ${token.title} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
1709
1734
  let matchCount = 0;
1710
1735
  for (const term of shortTerms.length > 0 ? shortTerms : searchTokens) {
1711
1736
  for (const w of words) {
@@ -1739,6 +1764,11 @@ async function listTokens(db, options) {
1739
1764
  tokens = await db.prepare(
1740
1765
  "SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
1741
1766
  ).all(options.domain);
1767
+ } else if (options?.domainPrefix) {
1768
+ const prefix = options.domainPrefix;
1769
+ tokens = await db.prepare(
1770
+ `SELECT * FROM tokens WHERE (domain = ? OR domain LIKE ?) AND deprecated_at IS NULL ORDER BY bloom_level, slug`
1771
+ ).all(prefix, `${prefix}/%`);
1742
1772
  } else {
1743
1773
  tokens = await db.prepare(
1744
1774
  "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
@@ -1750,7 +1780,20 @@ async function listTokens(db, options) {
1750
1780
  return tokens;
1751
1781
  }
1752
1782
  function slugify(text) {
1753
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1783
+ return text.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1784
+ }
1785
+ function getShortSlug(slug, domainPrefix) {
1786
+ if (domainPrefix) {
1787
+ const folded = domainPrefix.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1788
+ if (folded && slug.startsWith(`${folded}-`)) {
1789
+ return slug.substring(folded.length + 1);
1790
+ }
1791
+ }
1792
+ return slug;
1793
+ }
1794
+ function getDisplayTitle(t2, activeDomainScope) {
1795
+ if (t2.title?.trim()) return t2.title.trim();
1796
+ return getShortSlug(t2.slug, activeDomainScope);
1754
1797
  }
1755
1798
  async function generateTokenSlug(db, domain, concept, question) {
1756
1799
  const baseText = question && question.trim().length > 0 ? question : concept;
@@ -1780,6 +1823,7 @@ async function listPersonalCards(db, userId, options) {
1780
1823
  SELECT
1781
1824
  t.id AS tokenId,
1782
1825
  t.slug,
1826
+ t.title,
1783
1827
  t.concept,
1784
1828
  t.domain,
1785
1829
  t.bloom_level AS bloomLevel,
@@ -1881,6 +1925,7 @@ async function importCurriculumCards(db, userId, cards) {
1881
1925
  );
1882
1926
  token = await createToken(tx, {
1883
1927
  slug: finalSlug,
1928
+ title: card.title,
1884
1929
  concept: card.concept,
1885
1930
  domain: card.domain,
1886
1931
  bloom_level: bloom,
@@ -1998,23 +2043,28 @@ async function confirmCardSplit(db, userId, originalSlug, action, originalQuesti
1998
2043
  (/* @__PURE__ */ new Date()).toISOString(),
1999
2044
  originalToken.id
2000
2045
  );
2046
+ const insertPrereqStmt = tx.prepare(
2047
+ "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
2048
+ );
2001
2049
  for (const propToken of proposalTokens) {
2002
- await tx.prepare(
2003
- "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
2004
- ).run(originalToken.id, propToken.id);
2050
+ await insertPrereqStmt.run(originalToken.id, propToken.id);
2005
2051
  }
2006
2052
  await tx.prepare(
2007
2053
  "UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
2008
2054
  ).run(originalToken.id, userId);
2055
+ const checkPrereqsStmt = tx.prepare(
2056
+ "SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
2057
+ );
2058
+ const unblockCardStmt = tx.prepare(
2059
+ "UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?"
2060
+ );
2009
2061
  for (const propToken of proposalTokens) {
2010
2062
  const card = await ensureCard(tx, propToken.id, userId);
2011
2063
  if (card.blocked === 1) {
2012
- const prereqOfPrereq = await tx.prepare(
2013
- "SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
2014
- ).get(propToken.id);
2064
+ const prereqOfPrereq = await checkPrereqsStmt.get(propToken.id);
2015
2065
  if (prereqOfPrereq.n === 0) {
2016
2066
  const now = (/* @__PURE__ */ new Date()).toISOString();
2017
- await tx.prepare("UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?").run(now, card.id);
2067
+ await unblockCardStmt.run(now, card.id);
2018
2068
  }
2019
2069
  }
2020
2070
  }
@@ -2085,6 +2135,7 @@ async function confirmFoundations(db, userId, originalSlug, proposals) {
2085
2135
  );
2086
2136
  token = await createToken(tx, {
2087
2137
  slug: finalSlug,
2138
+ title: card.title,
2088
2139
  concept: card.concept,
2089
2140
  domain: card.domain,
2090
2141
  bloom_level: bloom,
@@ -2151,6 +2202,7 @@ async function confirmSourceImport(db, userId, sourceId, proposals) {
2151
2202
  }
2152
2203
  token = await createToken(tx, {
2153
2204
  slug: finalSlug,
2205
+ title: card.title,
2154
2206
  concept: card.concept,
2155
2207
  domain: card.domain,
2156
2208
  bloom_level: bloom,
@@ -2253,7 +2305,7 @@ async function addPrerequisite(db, tokenId, requiresId) {
2253
2305
  }
2254
2306
  async function getPrerequisites(db, tokenId) {
2255
2307
  return await db.prepare(
2256
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
2308
+ `SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
2257
2309
  FROM prerequisites p
2258
2310
  JOIN tokens t ON t.id = p.requires_id
2259
2311
  WHERE p.token_id = ?`
@@ -2261,7 +2313,7 @@ async function getPrerequisites(db, tokenId) {
2261
2313
  }
2262
2314
  async function getDependents(db, tokenId) {
2263
2315
  return await db.prepare(
2264
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
2316
+ `SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
2265
2317
  FROM prerequisites p
2266
2318
  JOIN tokens t ON t.id = p.token_id
2267
2319
  WHERE p.requires_id = ?`
@@ -2292,6 +2344,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2292
2344
  const toNode = (t2, card) => ({
2293
2345
  id: t2.id,
2294
2346
  slug: t2.slug,
2347
+ title: t2.title ?? t2.slug,
2295
2348
  concept: t2.concept,
2296
2349
  domain: t2.domain,
2297
2350
  bloom_level: t2.bloom_level,
@@ -2311,6 +2364,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2311
2364
  {
2312
2365
  id: p.requires_id,
2313
2366
  slug: p.slug,
2367
+ title: p.title,
2314
2368
  concept: p.concept,
2315
2369
  domain: p.domain,
2316
2370
  bloom_level: p.bloom_level
@@ -2323,6 +2377,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2323
2377
  {
2324
2378
  id: d.token_id,
2325
2379
  slug: d.slug,
2380
+ title: d.title,
2326
2381
  concept: d.concept,
2327
2382
  domain: d.domain,
2328
2383
  bloom_level: d.bloom_level
@@ -2437,7 +2492,8 @@ import { createHash as createHash2 } from "crypto";
2437
2492
  function embeddingContentForToken(t2) {
2438
2493
  return `${t2.concept}
2439
2494
  ${t2.question ?? ""}
2440
- ${t2.domain}`;
2495
+ ${t2.domain}
2496
+ ${t2.title ?? ""}`;
2441
2497
  }
2442
2498
  function computeContentHash(text) {
2443
2499
  return createHash2("sha256").update(text, "utf8").digest("hex");
@@ -4601,6 +4657,7 @@ async function buildReviewQueue(db, options) {
4601
4657
  c.id AS card_id,
4602
4658
  c.token_id AS token_id,
4603
4659
  t.slug AS slug,
4660
+ t.title AS title,
4604
4661
  t.concept AS concept,
4605
4662
  t.domain AS domain,
4606
4663
  t.bloom_level AS bloom_level,
@@ -4622,6 +4679,7 @@ async function buildReviewQueue(db, options) {
4622
4679
  c.id AS card_id,
4623
4680
  c.token_id AS token_id,
4624
4681
  t.slug AS slug,
4682
+ t.title AS title,
4625
4683
  t.concept AS concept,
4626
4684
  t.domain AS domain,
4627
4685
  t.bloom_level AS bloom_level,
@@ -4681,6 +4739,7 @@ function rowToItem(row) {
4681
4739
  cardId: row.card_id,
4682
4740
  tokenId: row.token_id,
4683
4741
  slug: row.slug,
4742
+ title: getDisplayTitle(row),
4684
4743
  concept: row.concept,
4685
4744
  domain: row.domain,
4686
4745
  bloomLevel: row.bloom_level,
@@ -6640,6 +6699,11 @@ function parseGeneratedCardArray(responseText, label, limits) {
6640
6699
  );
6641
6700
  }
6642
6701
  }
6702
+ if (card.title !== void 0 && (typeof card.title !== "string" || card.title.trim().length === 0)) {
6703
+ throw new Error(
6704
+ `Invalid ${label} card at index ${index}: title must be a non-empty string if provided`
6705
+ );
6706
+ }
6643
6707
  if (typeof card.bloom_level !== "number" || !Number.isInteger(card.bloom_level) || card.bloom_level < 1 || card.bloom_level > 5) {
6644
6708
  throw new Error(
6645
6709
  `Invalid ${label} card at index ${index}: bloom_level must be an integer from 1 to 5`
@@ -6653,6 +6717,7 @@ function parseGeneratedCardArray(responseText, label, limits) {
6653
6717
  return {
6654
6718
  question: card.question,
6655
6719
  concept: card.concept,
6720
+ title: card.title ? card.title : void 0,
6656
6721
  domain: card.domain,
6657
6722
  context: card.context,
6658
6723
  bloom_level: card.bloom_level,
@@ -6703,10 +6768,11 @@ Your task is to analyze curriculum objectives, syllabus requirements, or textboo
6703
6768
  For each extracted learning card, you MUST generate:
6704
6769
  1. "question": A clear, concise active-recall question testing the concept. The question must not reveal the answer itself.
6705
6770
  2. "concept": The reference answer, core fact, or target conceptual explanation.
6706
- 3. "domain": The category of the card. Use "${targetCategory}" as the default, but you may refine it if a specific sub-topic is evident.
6707
- 4. "context": The exact sentence or short excerpt from the source text that this card is based on.
6708
- 5. "bloom_level": Initial Bloom cognitive level (1 = Remember, 2 = Understand, 3 = Apply, 4 = Analyze, 5 = Synthesize).
6709
- 6. "symbiosis_mode": ZAM agent symbiosis level: "shadowing" (reading/monitoring), "copilot" (interactive helper), or "autonomy" (autonomous tasks).
6771
+ 3. "title": A short, human-friendly title for the knowledge graph node. Concise name for the concept. No domain prefix. Human-readable, natural language. Support Unicode (umlauts, Chinese, Japanese, Arabic, etc.). Name the core idea as well as possible.
6772
+ 4. "domain": The category of the card. Use "${targetCategory}" as the default, but you may refine it if a specific sub-topic is evident.
6773
+ 5. "context": The exact sentence or short excerpt from the source text that this card is based on.
6774
+ 6. "bloom_level": Initial Bloom cognitive level (1 = Remember, 2 = Understand, 3 = Apply, 4 = Analyze, 5 = Synthesize).
6775
+ 7. "symbiosis_mode": ZAM agent symbiosis level: "shadowing" (reading/monitoring), "copilot" (interactive helper), or "autonomy" (autonomous tasks).
6710
6776
 
6711
6777
  Guidelines:
6712
6778
  - Break complex requirements into multiple separate, atomic cards.
@@ -7635,6 +7701,114 @@ async function ensureHighQualityQuestion(db, token) {
7635
7701
  }
7636
7702
  return null;
7637
7703
  }
7704
+ async function generateTitleViaLLM(db, input8, opts = {}) {
7705
+ const cfg = await getProviderForRole(db, "text");
7706
+ const endpoint = await resolveUsableTextEndpoint(db);
7707
+ const systemPrompt = `You are an expert at naming knowledge items for a personal knowledge graph.
7708
+ Your task: given a knowledge token's concept (the full reference answer), question, domain, and optional context, produce a short, descriptive, human-friendly TITLE.
7709
+
7710
+ Strict rules from the project ADR:
7711
+ - Concise name for the concept (\u2264 80 chars ideal).
7712
+ - Thoughtful, memorable name \u2014 NOT a definition or the first sentence of the concept.
7713
+ - NEVER echo the domain name (e.g. do not say "Node Drain" if domain is "axon-ivy"; just "Node Drain Protection").
7714
+ - Prefer a name over spoiling the full concept.
7715
+ - Support Unicode (umlauts, etc.).
7716
+ - Output the title in the same language as the concept/content (e.g., German if the concept is German, English if the concept is English).
7717
+ - Output ONLY the raw title text. No preamble, quotes, explanations, or markdown.
7718
+
7719
+ Example good titles:
7720
+ - "RAG Retrieval Quality Evaluation" (for RAG failure modes focused on retrieval)
7721
+ - "Pythagorean Theorem Converse Proof"
7722
+ - "macOS Applications Directory Layout"
7723
+ - "Chronotype Sleep Preference"`;
7724
+ const userPrompt = `Domain: ${input8.domain}
7725
+ Slug: ${input8.slug}
7726
+ Question: ${input8.question || "(none)"}
7727
+ Concept: ${input8.concept}
7728
+ Context: ${input8.context || "(none)"}
7729
+
7730
+ Title:`;
7731
+ const url = `${endpoint.url}/chat/completions`;
7732
+ const apiKey = endpoint.apiKey;
7733
+ const model = endpoint.model;
7734
+ try {
7735
+ const res = await fetchWithInteractiveTimeout(url, {
7736
+ method: "POST",
7737
+ headers: {
7738
+ "Content-Type": "application/json",
7739
+ Authorization: `Bearer ${apiKey}`
7740
+ },
7741
+ body: JSON.stringify({
7742
+ model,
7743
+ messages: [
7744
+ { role: "system", content: systemPrompt },
7745
+ { role: "user", content: userPrompt }
7746
+ ],
7747
+ temperature: 0.2,
7748
+ max_tokens: 100
7749
+ }),
7750
+ locale: cfg.locale,
7751
+ timeoutMs: opts.timeoutMs,
7752
+ hardTimeoutMs: opts.timeoutMs
7753
+ });
7754
+ const text = await readChatContent(res, "title generation");
7755
+ return {
7756
+ text: text.trim(),
7757
+ model,
7758
+ providerName: endpoint.providerName
7759
+ };
7760
+ } catch (_e) {
7761
+ return {
7762
+ text: input8.concept.split(/[.;]/)[0].trim().substring(0, 80),
7763
+ model: "fallback",
7764
+ providerName: "heuristic"
7765
+ };
7766
+ }
7767
+ }
7768
+ async function repairUmlautsViaLLM(db, input8, opts = {}) {
7769
+ const cfg = await getProviderForRole(db, "text");
7770
+ const endpoint = await resolveUsableTextEndpoint(db);
7771
+ const systemPrompt = `You are an expert editor specializing in German language orthography.
7772
+ Your task: given a text that may contain legacy ASCII-folded German umlauts (like 'ue' instead of '\xFC', 'ae' instead of '\xE4', 'oe' instead of '\xF6', 'Ue' instead of '\xDC', etc.), repair them back to proper German umlauts (\xE4, \xF6, \xFC, \xC4, \xD6, \xDC, \xDF) where appropriate.
7773
+
7774
+ Strict rules:
7775
+ - ONLY change character sequences that are clearly meant to be German umlauts (e.g. change "Ueber" to "\xDCber", "fuer" to "f\xFCr", "moeglich" to "m\xF6glich").
7776
+ - DO NOT change words that are correct in their current form (e.g. do NOT change "nahe" to "n\xE4he", do NOT change English words or technical terms, do NOT change names like "Ueda").
7777
+ - Output ONLY the repaired text. No preamble, no explanation, no markdown formatting.`;
7778
+ const userPrompt = `Text to repair:
7779
+ """
7780
+ ${input8.text}
7781
+ """
7782
+
7783
+ Repaired text:`;
7784
+ const url = `${endpoint.url}/chat/completions`;
7785
+ const apiKey = endpoint.apiKey;
7786
+ const model = endpoint.model;
7787
+ try {
7788
+ const res = await fetchWithInteractiveTimeout(url, {
7789
+ method: "POST",
7790
+ headers: {
7791
+ "Content-Type": "application/json",
7792
+ Authorization: `Bearer ${apiKey}`
7793
+ },
7794
+ body: JSON.stringify({
7795
+ model,
7796
+ messages: [
7797
+ { role: "system", content: systemPrompt },
7798
+ { role: "user", content: userPrompt }
7799
+ ],
7800
+ temperature: 0.1
7801
+ }),
7802
+ locale: cfg.locale,
7803
+ timeoutMs: opts.timeoutMs,
7804
+ hardTimeoutMs: opts.timeoutMs
7805
+ });
7806
+ const text = await readChatContent(res, "umlaut repair");
7807
+ return text.trim();
7808
+ } catch (_e) {
7809
+ return input8.text;
7810
+ }
7811
+ }
7638
7812
 
7639
7813
  // src/cli/adapters/source-reader.ts
7640
7814
  var MAX_SOURCE_BYTES = 2 * 1024 * 1024;
@@ -7946,7 +8120,7 @@ function cleanHtmlText(html) {
7946
8120
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
7947
8121
  }
7948
8122
  function normalizeForComparison(str) {
7949
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8123
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
7950
8124
  }
7951
8125
 
7952
8126
  // src/cli/curriculum/providers/bildungsplan-bw/manifest.ts
@@ -8117,7 +8291,7 @@ function cleanHtmlText2(html) {
8117
8291
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8118
8292
  }
8119
8293
  function normalizeForComparison2(str) {
8120
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8294
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8121
8295
  }
8122
8296
 
8123
8297
  // src/cli/curriculum/providers/bildungsplan-hamburg/manifest.ts
@@ -8275,7 +8449,7 @@ function cleanHtmlText3(html) {
8275
8449
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8276
8450
  }
8277
8451
  function normalizeForComparison3(str) {
8278
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8452
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8279
8453
  }
8280
8454
 
8281
8455
  // src/cli/curriculum/providers/fachanforderungen-sh/manifest.ts
@@ -8432,7 +8606,7 @@ function cleanHtmlText4(html) {
8432
8606
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8433
8607
  }
8434
8608
  function normalizeForComparison4(str) {
8435
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8609
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8436
8610
  }
8437
8611
 
8438
8612
  // src/cli/curriculum/providers/kerncurriculum-hessen/manifest.ts
@@ -8626,7 +8800,7 @@ function cleanHtmlText5(html) {
8626
8800
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8627
8801
  }
8628
8802
  function normalizeForComparison5(str) {
8629
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8803
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8630
8804
  }
8631
8805
 
8632
8806
  // src/cli/curriculum/providers/kerncurriculum-niedersachsen/manifest.ts
@@ -8798,7 +8972,7 @@ function cleanHtmlText6(html) {
8798
8972
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8799
8973
  }
8800
8974
  function normalizeForComparison6(str) {
8801
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8975
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8802
8976
  }
8803
8977
 
8804
8978
  // src/cli/curriculum/providers/kernlehrplan-nrw/manifest.ts
@@ -9013,7 +9187,7 @@ function cleanHtmlText7(html) {
9013
9187
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9014
9188
  }
9015
9189
  function normalizeForComparison7(str) {
9016
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9190
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9017
9191
  }
9018
9192
 
9019
9193
  // src/cli/curriculum/providers/lehrplaene-rp/manifest.ts
@@ -9170,7 +9344,7 @@ function cleanHtmlText8(html) {
9170
9344
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9171
9345
  }
9172
9346
  function normalizeForComparison8(str) {
9173
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9347
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9174
9348
  }
9175
9349
 
9176
9350
  // src/cli/curriculum/providers/lehrplan-saarland/manifest.ts
@@ -9327,7 +9501,7 @@ function cleanHtmlText9(html) {
9327
9501
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9328
9502
  }
9329
9503
  function normalizeForComparison9(str) {
9330
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9504
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9331
9505
  }
9332
9506
 
9333
9507
  // src/cli/curriculum/providers/lehrplan-sachsen/manifest.ts
@@ -9512,7 +9686,7 @@ function cleanHtmlText10(html) {
9512
9686
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9513
9687
  }
9514
9688
  function normalizeForComparison10(str) {
9515
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9689
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9516
9690
  }
9517
9691
 
9518
9692
  // src/cli/curriculum/providers/lehrplan-thueringen/manifest.ts
@@ -9667,7 +9841,7 @@ function cleanHtmlText11(html) {
9667
9841
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9668
9842
  }
9669
9843
  function normalizeForComparison11(str) {
9670
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9844
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9671
9845
  }
9672
9846
 
9673
9847
  // src/cli/curriculum/providers/lehrplanplus-bayern/manifest.ts
@@ -9911,7 +10085,7 @@ function cleanHtmlText12(html) {
9911
10085
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9912
10086
  }
9913
10087
  function normalizeForComparison12(str) {
9914
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10088
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9915
10089
  }
9916
10090
 
9917
10091
  // src/cli/curriculum/providers/rahmenlehrplan-berlin-brandenburg/manifest.ts
@@ -10096,7 +10270,7 @@ function cleanHtmlText13(html) {
10096
10270
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
10097
10271
  }
10098
10272
  function normalizeForComparison13(str) {
10099
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10273
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10100
10274
  }
10101
10275
 
10102
10276
  // src/cli/curriculum/providers/rahmenplan-mv/manifest.ts
@@ -10253,7 +10427,7 @@ function cleanHtmlText14(html) {
10253
10427
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
10254
10428
  }
10255
10429
  function normalizeForComparison14(str) {
10256
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10430
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10257
10431
  }
10258
10432
 
10259
10433
  // src/cli/curriculum/providers/rahmenrichtlinien-st/manifest.ts
@@ -10408,7 +10582,7 @@ function cleanHtmlText15(html) {
10408
10582
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
10409
10583
  }
10410
10584
  function normalizeForComparison15(str) {
10411
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10585
+ return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10412
10586
  }
10413
10587
 
10414
10588
  // src/cli/curriculum/registry.ts
@@ -10606,7 +10780,8 @@ async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
10606
10780
  const queryText = embeddingContentForToken({
10607
10781
  concept: candidate.concept,
10608
10782
  question: candidate.question ?? null,
10609
- domain: candidate.domain ?? ""
10783
+ domain: candidate.domain ?? "",
10784
+ title: candidate.title ?? null
10610
10785
  });
10611
10786
  const q2 = await embed(db, queryText);
10612
10787
  if (!q2) {
@@ -10637,6 +10812,7 @@ async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
10637
10812
  results.push({
10638
10813
  slug: hit.slug,
10639
10814
  concept: hit.concept,
10815
+ title: hit.title,
10640
10816
  similarity: hit.similarity
10641
10817
  });
10642
10818
  }
@@ -12400,10 +12576,12 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
12400
12576
  const possibleDuplicates = await findPossibleDuplicates(db, {
12401
12577
  concept: data?.concept,
12402
12578
  question: data?.question ?? null,
12403
- domain: data?.domain
12579
+ domain: data?.domain,
12580
+ title: data?.title ?? null
12404
12581
  });
12405
12582
  const token = await createToken(db, {
12406
12583
  slug: data?.slug,
12584
+ title: data?.title,
12407
12585
  concept: data?.concept,
12408
12586
  domain: data?.domain,
12409
12587
  bloom_level: data?.bloom_level ?? 1,
@@ -12483,6 +12661,8 @@ bridgeCommand.command("relevant-tokens").description("Find tokens relevant to a
12483
12661
  const card = await getCard(db, t2.id, userId);
12484
12662
  tokens.push({
12485
12663
  slug: t2.slug,
12664
+ title: t2.title,
12665
+ display_title: getDisplayTitle(t2),
12486
12666
  concept: t2.concept,
12487
12667
  domain: t2.domain,
12488
12668
  bloom_level: t2.bloom_level,
@@ -12545,7 +12725,8 @@ bridgeCommand.command("suggest-foundations").description("Propose existing token
12545
12725
  queryText = embeddingContentForToken({
12546
12726
  concept: data.concept,
12547
12727
  question: typeof data.question === "string" ? data.question : null,
12548
- domain: typeof data.domain === "string" ? data.domain : ""
12728
+ domain: typeof data.domain === "string" ? data.domain : "",
12729
+ title: typeof data.title === "string" ? data.title : null
12549
12730
  });
12550
12731
  if (data.bloom_level !== void 0) {
12551
12732
  if (typeof data.bloom_level !== "number" || !Number.isInteger(data.bloom_level) || data.bloom_level < 1 || data.bloom_level > 5) {
@@ -13777,12 +13958,18 @@ bridgeCommand.command("list-tokens").description(
13777
13958
  ).option(
13778
13959
  "--user <id>",
13779
13960
  "User ID (default: whoami) \u2014 when provided, includes personal card info"
13780
- ).option("--domain <domain>", "Filter by domain").action(async (opts) => {
13961
+ ).option("--domain <domain>", "Filter by exact domain").option(
13962
+ "--domain-prefix <prefix>",
13963
+ "Filter by domain prefix (e.g. docuware-cops) \u2014 uses / separator for hierarchy"
13964
+ ).action(async (opts) => {
13781
13965
  await withDb2(async (db) => {
13782
13966
  const userId = opts.user ? await resolveUser(opts, db, { json: true }) : void 0;
13967
+ const listOpts = {};
13968
+ if (opts.domain) listOpts.domain = opts.domain;
13969
+ if (opts.domainPrefix) listOpts.domainPrefix = opts.domainPrefix;
13783
13970
  const tokens = await listTokens(
13784
13971
  db,
13785
- opts.domain ? { domain: opts.domain } : void 0
13972
+ Object.keys(listOpts).length ? listOpts : void 0
13786
13973
  );
13787
13974
  const cardMap = /* @__PURE__ */ new Map();
13788
13975
  if (userId && tokens.length > 0) {
@@ -13799,6 +13986,8 @@ bridgeCommand.command("list-tokens").description(
13799
13986
  return {
13800
13987
  id: t2.id,
13801
13988
  slug: t2.slug,
13989
+ title: t2.title,
13990
+ display_title: getDisplayTitle(t2),
13802
13991
  concept: t2.concept,
13803
13992
  domain: t2.domain,
13804
13993
  bloomLevel: t2.bloom_level,
@@ -13832,6 +14021,8 @@ bridgeCommand.command("get-neighborhood").description(
13832
14021
  const mapToken = (nt) => ({
13833
14022
  id: nt.id,
13834
14023
  slug: nt.slug,
14024
+ title: nt.title,
14025
+ display_title: getDisplayTitle(nt),
13835
14026
  concept: nt.concept,
13836
14027
  domain: nt.domain,
13837
14028
  bloomLevel: nt.bloom_level,
@@ -13863,7 +14054,7 @@ bridgeCommand.command("personal-card-list").description("List and search persona
13863
14054
  jsonOut2({ cards });
13864
14055
  });
13865
14056
  });
13866
- bridgeCommand.command("personal-card-create").description("Atomically create a token and its personal card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--concept <concept>", "Concept description / answer").option("--domain <domain>", "Knowledge category / domain", "").option("--question <question>", "Question prompt for recall").option("--source-link <link>", "Source file path or reference URL").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option(
14057
+ bridgeCommand.command("personal-card-create").description("Atomically create a token and its personal card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--concept <concept>", "Concept description / answer").option("--title <title>", "Human-friendly display title").option("--domain <domain>", "Knowledge category / domain", "").option("--question <question>", "Question prompt for recall").option("--source-link <link>", "Source file path or reference URL").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option(
13867
14058
  "--mode <mode>",
13868
14059
  "Symbiosis mode: shadowing | copilot | autonomy | none",
13869
14060
  "none"
@@ -13891,6 +14082,7 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
13891
14082
  const { token, card } = await db.transaction(async (tx) => {
13892
14083
  const createdToken = await createToken(tx, {
13893
14084
  slug,
14085
+ title: opts.title,
13894
14086
  concept: opts.concept,
13895
14087
  domain: opts.domain,
13896
14088
  bloom_level: bloom,
@@ -13907,6 +14099,8 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
13907
14099
  token: {
13908
14100
  id: token.id,
13909
14101
  slug: token.slug,
14102
+ title: token.title,
14103
+ display_title: getDisplayTitle(token),
13910
14104
  concept: token.concept,
13911
14105
  domain: token.domain,
13912
14106
  bloomLevel: token.bloom_level,
@@ -13928,13 +14122,14 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
13928
14122
  });
13929
14123
  });
13930
14124
  });
13931
- bridgeCommand.command("personal-card-update").description("Update the mutable token fields of a personal card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--slug <slug>", "Token slug to update").option("--concept <concept>", "Updated concept text").option("--domain <domain>", "Updated domain / category").option("--bloom <level>", "Updated Bloom taxonomy level (1-5)").option("--context <context>", "Updated context").option(
14125
+ bridgeCommand.command("personal-card-update").description("Update the mutable token fields of a personal card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--slug <slug>", "Token slug to update").option("--title <title>", "Updated display title").option("--concept <concept>", "Updated concept text").option("--domain <domain>", "Updated domain / category").option("--bloom <level>", "Updated Bloom taxonomy level (1-5)").option("--context <context>", "Updated context").option(
13932
14126
  "--mode <mode>",
13933
14127
  "Updated symbiosis mode: shadowing | copilot | autonomy | none"
13934
14128
  ).option("--source-link <link>", "Updated source link").option("--question <question>", "Updated question text").action(async (opts) => {
13935
14129
  await withDb2(async (db) => {
13936
14130
  const userId = await resolveUser(opts, db, { json: true });
13937
14131
  const updates = {};
14132
+ if (opts.title !== void 0) updates.title = opts.title;
13938
14133
  if (opts.concept !== void 0) updates.concept = opts.concept;
13939
14134
  if (opts.domain !== void 0) updates.domain = opts.domain;
13940
14135
  if (opts.bloom !== void 0) {
@@ -13976,6 +14171,8 @@ bridgeCommand.command("personal-card-update").description("Update the mutable to
13976
14171
  token: {
13977
14172
  id: token.id,
13978
14173
  slug: token.slug,
14174
+ title: token.title,
14175
+ display_title: getDisplayTitle(token),
13979
14176
  concept: token.concept,
13980
14177
  domain: token.domain,
13981
14178
  bloomLevel: token.bloom_level,
@@ -14008,7 +14205,12 @@ bridgeCommand.command("personal-card-remove").description(
14008
14205
  success: true,
14009
14206
  preview: true,
14010
14207
  requiresConfirmation: true,
14011
- token: { id: token.id, slug: token.slug },
14208
+ token: {
14209
+ id: token.id,
14210
+ slug: token.slug,
14211
+ title: token.title,
14212
+ display_title: getDisplayTitle(token)
14213
+ },
14012
14214
  impact
14013
14215
  });
14014
14216
  return;
@@ -14037,7 +14239,12 @@ bridgeCommand.command("personal-card-delete").description("Hard-delete a token a
14037
14239
  success: true,
14038
14240
  preview: true,
14039
14241
  requiresConfirmation: true,
14040
- token: { id: token.id, slug: token.slug },
14242
+ token: {
14243
+ id: token.id,
14244
+ slug: token.slug,
14245
+ title: token.title,
14246
+ display_title: getDisplayTitle(token)
14247
+ },
14041
14248
  impact
14042
14249
  });
14043
14250
  return;
@@ -14929,11 +15136,567 @@ async function setupTurso(urlArg, tokenArg, mode) {
14929
15136
  }
14930
15137
  }
14931
15138
 
15139
+ // src/cli/commands/doctor.ts
15140
+ import { Command as Command5 } from "commander";
15141
+ function shouldApplyFixes(opts) {
15142
+ return opts.fix === true && opts.dryRun !== true;
15143
+ }
15144
+ async function confirmDeterministicChanges(opts, message) {
15145
+ if (opts.yes) return true;
15146
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
15147
+ console.error(
15148
+ "Cannot request confirmation in a non-interactive terminal. Re-run with --yes to apply deterministic fixes."
15149
+ );
15150
+ return false;
15151
+ }
15152
+ const { confirm: confirm5 } = await import("@inquirer/prompts");
15153
+ return confirm5({ message, default: false });
15154
+ }
15155
+ function canRunInteractiveChoices(opts, taskName) {
15156
+ if (opts.yes) {
15157
+ console.error(
15158
+ `--yes cannot choose how to resolve ${taskName}. Re-run without --yes in an interactive terminal.`
15159
+ );
15160
+ return false;
15161
+ }
15162
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
15163
+ console.error(
15164
+ `${taskName} fixes require an interactive terminal because each change needs an explicit choice.`
15165
+ );
15166
+ return false;
15167
+ }
15168
+ return true;
15169
+ }
15170
+ function getWeakTitleReason(t2) {
15171
+ const title = (t2.title || "").trim();
15172
+ if (!title) {
15173
+ return "Missing or empty title";
15174
+ }
15175
+ if (title === t2.slug) {
15176
+ return "Title is equal to technical slug";
15177
+ }
15178
+ if (title.length < 3) {
15179
+ return "Title is suspiciously short";
15180
+ }
15181
+ if (title.length > 100) {
15182
+ return "Title is too long (> 100 chars)";
15183
+ }
15184
+ if (t2.domain) {
15185
+ const domParts = t2.domain.split(/[-/]/).filter((p) => p.length > 2);
15186
+ const titleLower2 = title.toLowerCase();
15187
+ for (const part of domParts) {
15188
+ if (titleLower2.includes(part.toLowerCase())) {
15189
+ return `Domain echo (contains '${part}' from domain '${t2.domain}')`;
15190
+ }
15191
+ }
15192
+ }
15193
+ const conceptClean = (t2.concept || "").trim().toLowerCase();
15194
+ const titleLower = title.toLowerCase();
15195
+ if (conceptClean.startsWith(titleLower) && t2.concept.length > title.length + 5 && !/[.!?]/.test(title)) {
15196
+ const titleWords = titleLower.split(/\s+/).length;
15197
+ if (titleWords <= 5) {
15198
+ return "Concept-prefix copy";
15199
+ }
15200
+ }
15201
+ if (title.endsWith("?") || /^(what|how|why|who|where|when|which|is|are|was|were|can|do|does|did|ueber|fuer|nahe)\b/i.test(
15202
+ title
15203
+ )) {
15204
+ return "Question stump or interrogative structure";
15205
+ }
15206
+ if (title.length > 3 && /[a-zA-Z]/.test(title)) {
15207
+ if (title === title.toUpperCase()) {
15208
+ return "All uppercase";
15209
+ }
15210
+ if (title === title.toLowerCase()) {
15211
+ return "All lowercase";
15212
+ }
15213
+ }
15214
+ return null;
15215
+ }
15216
+ function stripLeadingDomainWords(value, domain) {
15217
+ let result = value.trim();
15218
+ for (const word of domain.split(/[-/]/).filter(Boolean)) {
15219
+ const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15220
+ result = result.replace(new RegExp(`^${escaped}\\s+`, "i"), "");
15221
+ }
15222
+ return result.trim();
15223
+ }
15224
+ function truncateTitle(value) {
15225
+ const trimmed = value.trim().replace(/^["']+|["']+$/g, "");
15226
+ return trimmed.length <= 80 ? trimmed : `${trimmed.substring(0, 77).trimEnd()}...`;
15227
+ }
15228
+ function titleFromSlug(token) {
15229
+ return getShortSlug(token.slug, token.domain).split(/[-_]+/).filter(Boolean).map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join(" ");
15230
+ }
15231
+ function validatedTitleProposal(token, raw) {
15232
+ const candidates = [
15233
+ raw,
15234
+ (token.concept || "").split(/[.;]/)[0] || "",
15235
+ titleFromSlug(token)
15236
+ ];
15237
+ for (const candidate of candidates) {
15238
+ const normalized = truncateTitle(
15239
+ stripLeadingDomainWords(candidate, token.domain || "")
15240
+ );
15241
+ if (normalized.length > 0 && getWeakTitleReason({ ...token, title: normalized }) === null) {
15242
+ return normalized;
15243
+ }
15244
+ }
15245
+ return null;
15246
+ }
15247
+ var doctorTasks = [
15248
+ {
15249
+ name: "titles",
15250
+ description: "Backfill missing/weak human-friendly titles (no domain echoes, descriptive names).",
15251
+ run: async (db, opts) => {
15252
+ const tokens = await listTokens(db, {});
15253
+ const needing = tokens.map((t2) => {
15254
+ const reason = getWeakTitleReason(t2);
15255
+ return reason ? { token: t2, reason } : null;
15256
+ }).filter((x) => x !== null);
15257
+ console.log(
15258
+ `Found ${needing.length} tokens needing title work (of ${tokens.length}).`
15259
+ );
15260
+ if (needing.length === 0) return;
15261
+ const cfg = await getLlmConfig(db);
15262
+ const useLLM = cfg.enabled && !opts.noLlm;
15263
+ console.log(
15264
+ `Title generation mode: ${useLLM ? "LLM" : "Heuristic fallback"}${opts.timeoutMs ? ` (timeout: ${opts.timeoutMs}ms)` : ""}`
15265
+ );
15266
+ console.log("Generating proposed titles...");
15267
+ const proposals = [];
15268
+ let rejected = 0;
15269
+ for (let i = 0; i < needing.length; i++) {
15270
+ const item = needing[i];
15271
+ process.stdout.write(
15272
+ `\r [${i + 1}/${needing.length}] ${item.token.slug.slice(0, 40)}...`
15273
+ );
15274
+ let rawProposal = "";
15275
+ if (useLLM) {
15276
+ try {
15277
+ const gen = await generateTitleViaLLM(
15278
+ db,
15279
+ {
15280
+ slug: item.token.slug,
15281
+ concept: item.token.concept,
15282
+ domain: item.token.domain,
15283
+ question: item.token.question,
15284
+ context: item.token.context
15285
+ },
15286
+ { timeoutMs: opts.timeoutMs }
15287
+ );
15288
+ rawProposal = gen.text;
15289
+ } catch (_e) {
15290
+ rawProposal = (item.token.concept || "").split(/[.;]/)[0]?.trim() || item.token.slug.replace(/-/g, " ");
15291
+ }
15292
+ } else {
15293
+ rawProposal = (item.token.concept || "").split(/[.;]/)[0]?.trim() || item.token.slug.replace(/-/g, " ");
15294
+ }
15295
+ const proposed = validatedTitleProposal(item.token, rawProposal);
15296
+ if (!proposed) {
15297
+ rejected++;
15298
+ continue;
15299
+ }
15300
+ proposals.push({
15301
+ token: item.token,
15302
+ oldTitle: item.token.title,
15303
+ newTitle: proposed,
15304
+ reason: item.reason
15305
+ });
15306
+ }
15307
+ process.stdout.write("\n");
15308
+ if (rejected > 0) {
15309
+ console.warn(
15310
+ `Skipped ${rejected} token(s) because no proposal passed title quality validation.`
15311
+ );
15312
+ }
15313
+ if (proposals.length === 0) return;
15314
+ if (!shouldApplyFixes(opts)) {
15315
+ console.log("\nProposed changes (Dry run):");
15316
+ for (const prop of proposals) {
15317
+ console.log(
15318
+ ` ${prop.token.slug}: "${prop.oldTitle}" -> "${prop.newTitle}" (Reason: ${prop.reason})`
15319
+ );
15320
+ }
15321
+ console.log("\nRun with --fix to apply these changes.");
15322
+ return;
15323
+ }
15324
+ if (!opts.yes) {
15325
+ console.log("\nProposed changes:");
15326
+ for (const prop of proposals) {
15327
+ console.log(
15328
+ ` ${prop.token.slug}: "${prop.oldTitle}" -> "${prop.newTitle}" (Reason: ${prop.reason})`
15329
+ );
15330
+ }
15331
+ }
15332
+ const confirmed = await confirmDeterministicChanges(
15333
+ opts,
15334
+ `Apply these ${proposals.length} proposed titles?`
15335
+ );
15336
+ if (!confirmed) {
15337
+ console.log("Cancelled. No titles were updated.");
15338
+ return;
15339
+ }
15340
+ let fixed = 0;
15341
+ for (const prop of proposals) {
15342
+ await updateToken(db, prop.token.slug, { title: prop.newTitle });
15343
+ fixed++;
15344
+ console.log(`Set title for ${prop.token.slug} -> ${prop.newTitle}`);
15345
+ }
15346
+ console.log(`Successfully fixed ${fixed} titles.`);
15347
+ }
15348
+ },
15349
+ {
15350
+ name: "texts",
15351
+ description: "Repair legacy ASCII umlauts in prose fields (question/concept/context).",
15352
+ run: async (db, opts) => {
15353
+ const tokens = await listTokens(db, {});
15354
+ const cfg = await getLlmConfig(db);
15355
+ const useLLM = cfg.enabled && !opts.noLlm;
15356
+ const replacements = [
15357
+ [/\bUeber\b/g, "\xDCber"],
15358
+ [/\bueber\b/g, "\xFCber"],
15359
+ [/\bFuer\b/g, "F\xFCr"],
15360
+ [/\bfuer\b/g, "f\xFCr"],
15361
+ [/\bMuehe\b/g, "M\xFChe"],
15362
+ [/\bmuehe\b/g, "m\xFChe"],
15363
+ [/\bKuer\b/g, "K\xFCr"],
15364
+ [/\bkuer\b/g, "k\xFCr"],
15365
+ [/\bUebung\b/g, "\xDCbung"],
15366
+ [/\buebung\b/g, "\xFCbung"],
15367
+ [/\bMoeglich\b/g, "M\xF6glich"],
15368
+ [/\bmoeglich\b/g, "m\xF6glich"],
15369
+ [/\bMoeglichkeiten\b/g, "M\xF6glichkeiten"],
15370
+ [/\bmoeglichkeiten\b/g, "m\xF6glichkeiten"],
15371
+ [/\bSchoen\b/g, "Sch\xF6n"],
15372
+ [/\bschoen\b/g, "sch\xF6n"],
15373
+ [/\bKoennen\b/g, "K\xF6nnen"],
15374
+ [/\bkoennen\b/g, "k\xF6nnen"],
15375
+ [/\bMuessen\b/g, "M\xFCssen"],
15376
+ [/\bmuessen\b/g, "m\xFCssen"],
15377
+ [/\bDuerfen\b/g, "D\xFCrfen"],
15378
+ [/\bduerfen\b/g, "d\xFCrfen"],
15379
+ [/\bAerger\b/g, "\xC4rger"],
15380
+ [/\baerger\b/g, "\xE4rger"],
15381
+ [/\bGruende\b/g, "Gr\xFCnde"],
15382
+ [/\bgruende\b/g, "gr\xFCnde"]
15383
+ ];
15384
+ const toFix = [];
15385
+ console.log("Analyzing prose text fields for legacy umlauts...");
15386
+ for (const t2 of tokens) {
15387
+ const fields = [
15388
+ { key: "question", val: t2.question },
15389
+ { key: "concept", val: t2.concept },
15390
+ { key: "context", val: t2.context },
15391
+ { key: "title", val: t2.title }
15392
+ ];
15393
+ for (const f of fields) {
15394
+ if (!f.val || typeof f.val !== "string") continue;
15395
+ let repaired = f.val;
15396
+ if (useLLM) {
15397
+ const hasPossibleUmlautFold = /[aou]e/i.test(f.val);
15398
+ if (hasPossibleUmlautFold) {
15399
+ try {
15400
+ repaired = await repairUmlautsViaLLM(
15401
+ db,
15402
+ { text: f.val },
15403
+ { timeoutMs: opts.timeoutMs }
15404
+ );
15405
+ if (repaired === f.val) {
15406
+ for (const [from, to] of replacements) {
15407
+ repaired = repaired.replace(from, to);
15408
+ }
15409
+ }
15410
+ } catch (_e) {
15411
+ for (const [from, to] of replacements) {
15412
+ repaired = repaired.replace(from, to);
15413
+ }
15414
+ }
15415
+ }
15416
+ } else {
15417
+ for (const [from, to] of replacements) {
15418
+ repaired = repaired.replace(from, to);
15419
+ }
15420
+ }
15421
+ if (repaired !== f.val) {
15422
+ toFix.push({ token: t2, field: f.key, old: f.val, new: repaired });
15423
+ }
15424
+ }
15425
+ }
15426
+ console.log(`Found ${toFix.length} prose fields with legacy umlauts.`);
15427
+ if (toFix.length === 0) return;
15428
+ if (!shouldApplyFixes(opts)) {
15429
+ console.log("\nProposed changes (Dry run):");
15430
+ for (const fix of toFix) {
15431
+ console.log(` ${fix.token.slug} [${fix.field}]:`);
15432
+ console.log(` Old: "${fix.old}"`);
15433
+ console.log(` New: "${fix.new}"`);
15434
+ }
15435
+ console.log("\nRun with --fix to apply these changes.");
15436
+ return;
15437
+ }
15438
+ if (!opts.yes) {
15439
+ console.log("\nProposed changes:");
15440
+ for (const fix of toFix) {
15441
+ console.log(
15442
+ ` ${fix.token.slug} [${fix.field}]: "${fix.old}" -> "${fix.new}"`
15443
+ );
15444
+ }
15445
+ }
15446
+ const confirmed = await confirmDeterministicChanges(
15447
+ opts,
15448
+ `Apply these ${toFix.length} prose fixes?`
15449
+ );
15450
+ if (!confirmed) {
15451
+ console.log("Cancelled. No changes made.");
15452
+ return;
15453
+ }
15454
+ let fixed = 0;
15455
+ for (const fix of toFix) {
15456
+ const updates = {};
15457
+ updates[fix.field] = fix.new;
15458
+ await updateToken(db, fix.token.slug, updates);
15459
+ fixed++;
15460
+ }
15461
+ console.log(
15462
+ `Fixed ${fixed} fields. (Re-embed will happen automatically for changed content.)`
15463
+ );
15464
+ }
15465
+ },
15466
+ {
15467
+ name: "duplicates",
15468
+ description: "List semantic duplicates for review/merge.",
15469
+ run: async (db, opts) => {
15470
+ const applyingFixes = shouldApplyFixes(opts);
15471
+ if (applyingFixes && !canRunInteractiveChoices(opts, "duplicate fixes")) {
15472
+ return;
15473
+ }
15474
+ const modelSetting = await getSetting(db, "llm.embedding.model");
15475
+ if (!modelSetting) {
15476
+ console.log(
15477
+ "No embedding model configured. Please configure an embedding model to scan duplicates."
15478
+ );
15479
+ return;
15480
+ }
15481
+ const model = canonicalEmbeddingModelId(modelSetting);
15482
+ if (model !== modelSetting.trim().toLowerCase()) {
15483
+ console.log(
15484
+ `Using canonical embedding model "${model}" for configured alias "${modelSetting}".`
15485
+ );
15486
+ }
15487
+ let coverage = await getEmbeddingCoverage(db, model);
15488
+ const initiallyIncomplete = coverage.missing + coverage.stale;
15489
+ if (initiallyIncomplete > 0) {
15490
+ if (applyingFixes) {
15491
+ console.log(
15492
+ `Embedding coverage is incomplete (${coverage.missing} missing, ${coverage.stale} stale). Attempting backfill...`
15493
+ );
15494
+ const result = await ensureTokenEmbeddings(db, {
15495
+ limit: coverage.tokens
15496
+ });
15497
+ coverage = await getEmbeddingCoverage(db, model);
15498
+ const remaining = coverage.missing + coverage.stale;
15499
+ if (remaining > 0) {
15500
+ console.warn(
15501
+ `Warning: Duplicate scan is incomplete: ${remaining} of ${coverage.tokens} active tokens still lack a fresh "${model}" embedding.${result.reason ? ` ${result.reason}` : ""}`
15502
+ );
15503
+ } else {
15504
+ console.log(
15505
+ `Embedding backfill complete (${result.embedded} updated).`
15506
+ );
15507
+ }
15508
+ } else {
15509
+ console.warn(
15510
+ `Warning: Duplicate scan is incomplete: ${initiallyIncomplete} of ${coverage.tokens} active tokens lack a fresh "${model}" embedding. Dry-run will not backfill them; run 'zam token reembed' first for a complete scan.`
15511
+ );
15512
+ }
15513
+ }
15514
+ console.log(`Listing embedded tokens for model "${model}"...`);
15515
+ const embedded = await listEmbeddedTokens(db, model);
15516
+ const threshold = await resolveDedupThreshold(db);
15517
+ console.log(
15518
+ `Scanning ${embedded.length} tokens for semantic duplicates (threshold: ${threshold})...`
15519
+ );
15520
+ const duplicates = [];
15521
+ const seenPairs = /* @__PURE__ */ new Set();
15522
+ for (let i = 0; i < embedded.length; i++) {
15523
+ for (let j = i + 1; j < embedded.length; j++) {
15524
+ const sim = cosineSimilarity(
15525
+ embedded[i].embedding,
15526
+ embedded[j].embedding
15527
+ );
15528
+ if (sim >= threshold) {
15529
+ const idA = embedded[i].token.id;
15530
+ const idB = embedded[j].token.id;
15531
+ const pairKey = idA < idB ? `${idA}:${idB}` : `${idB}:${idA}`;
15532
+ if (!seenPairs.has(pairKey)) {
15533
+ seenPairs.add(pairKey);
15534
+ duplicates.push({
15535
+ a: embedded[i].token,
15536
+ b: embedded[j].token,
15537
+ similarity: sim
15538
+ });
15539
+ }
15540
+ }
15541
+ }
15542
+ }
15543
+ console.log(`Found ${duplicates.length} duplicate pairs.`);
15544
+ if (duplicates.length === 0) return;
15545
+ if (!applyingFixes) {
15546
+ console.log("\nDuplicate pairs found (Dry run):");
15547
+ for (const pair of duplicates) {
15548
+ console.log(
15549
+ ` - "${pair.a.slug}" and "${pair.b.slug}" (similarity: ${pair.similarity.toFixed(3)})`
15550
+ );
15551
+ }
15552
+ console.log("\nRun with --fix to resolve duplicates interactively.");
15553
+ return;
15554
+ }
15555
+ const { select: select3 } = await import("@inquirer/prompts");
15556
+ let deprecatedCount = 0;
15557
+ for (const pair of duplicates) {
15558
+ console.log(
15559
+ `
15560
+ Duplicate pair found (similarity: ${pair.similarity.toFixed(3)}):`
15561
+ );
15562
+ console.log(
15563
+ ` 1. ${pair.a.slug}: "${pair.a.concept.substring(0, 100)}..."`
15564
+ );
15565
+ console.log(
15566
+ ` 2. ${pair.b.slug}: "${pair.b.concept.substring(0, 100)}..."`
15567
+ );
15568
+ const choice = await select3({
15569
+ message: "Select action:",
15570
+ choices: [
15571
+ { name: "Keep both", value: "keep" },
15572
+ { name: `Deprecate ${pair.a.slug}`, value: "deprecate_a" },
15573
+ { name: `Deprecate ${pair.b.slug}`, value: "deprecate_b" }
15574
+ ]
15575
+ });
15576
+ if (choice === "deprecate_a") {
15577
+ await deprecateToken(db, pair.a.slug);
15578
+ console.log(` Deprecated ${pair.a.slug}`);
15579
+ deprecatedCount++;
15580
+ } else if (choice === "deprecate_b") {
15581
+ await deprecateToken(db, pair.b.slug);
15582
+ console.log(` Deprecated ${pair.b.slug}`);
15583
+ deprecatedCount++;
15584
+ }
15585
+ }
15586
+ console.log(
15587
+ `
15588
+ Duplicates scan complete. Deprecated ${deprecatedCount} token(s).`
15589
+ );
15590
+ }
15591
+ },
15592
+ {
15593
+ name: "domains",
15594
+ description: "Help rename/unify domains into / hierarchy (slugs untouched).",
15595
+ run: async (db, opts) => {
15596
+ const tokens = await listTokens(db, {});
15597
+ const domains = Array.from(
15598
+ new Set(tokens.map((t2) => t2.domain).filter(Boolean))
15599
+ ).sort();
15600
+ console.log(`Current domains (${domains.length}):`);
15601
+ for (const d of domains) {
15602
+ const count2 = tokens.filter((t2) => t2.domain === d).length;
15603
+ const status = d.includes("/") ? "(hierarchical)" : "(flat, valid)";
15604
+ console.log(` ${d} \u2014 ${count2} token(s) ${status}`);
15605
+ }
15606
+ if (!shouldApplyFixes(opts)) {
15607
+ console.log("\nUse --fix to interactively rename/unify domains.");
15608
+ return;
15609
+ }
15610
+ if (!canRunInteractiveChoices(opts, "domain renames")) return;
15611
+ const { confirm: confirm5, input: input8 } = await import("@inquirer/prompts");
15612
+ const changes = [];
15613
+ for (const d of domains) {
15614
+ const count2 = tokens.filter((t2) => t2.domain === d).length;
15615
+ const wantRename = await confirm5({
15616
+ message: `Rename domain "${d}" (${count2} tokens)?`,
15617
+ default: false
15618
+ });
15619
+ if (wantRename) {
15620
+ const newDomain = await input8({
15621
+ message: `Enter new domain name for "${d}":`,
15622
+ validate: (val) => val.trim().length > 0 ? true : "Domain name cannot be empty"
15623
+ });
15624
+ const trimmed = newDomain.trim();
15625
+ if (trimmed === d) {
15626
+ console.log(" New name matches old name. Skipped.");
15627
+ continue;
15628
+ }
15629
+ changes.push({ oldDomain: d, newDomain: trimmed });
15630
+ }
15631
+ }
15632
+ if (changes.length > 0) {
15633
+ await db.transaction(async (tx) => {
15634
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15635
+ const stmt = tx.prepare(
15636
+ "UPDATE tokens SET domain = ?, updated_at = ? WHERE domain = ?"
15637
+ );
15638
+ for (const change of changes) {
15639
+ const res = await stmt.run(change.newDomain, now, change.oldDomain);
15640
+ console.log(
15641
+ ` Renamed "${change.oldDomain}" to "${change.newDomain}" for ${res.changes} token(s).`
15642
+ );
15643
+ }
15644
+ });
15645
+ }
15646
+ console.log(
15647
+ `Domains rename complete. Renamed ${changes.length} domain(s).`
15648
+ );
15649
+ }
15650
+ }
15651
+ ];
15652
+ function parseDoctorTimeout(value) {
15653
+ const parsed = Number(value);
15654
+ if (!Number.isInteger(parsed) || parsed <= 0) {
15655
+ throw new Error("--timeout must be a positive integer in milliseconds");
15656
+ }
15657
+ return parsed;
15658
+ }
15659
+ var doctorCommand = new Command5("doctor").description(
15660
+ "Diagnose and repair the knowledge base (titles, legacy data, duplicates, domains)."
15661
+ ).option("--fix", "Apply changes (default is dry-run)").option("--dry-run", "Explicit dry run (default)").option(
15662
+ "--yes",
15663
+ "Auto-confirm deterministic title/text fixes (duplicate/domain choices remain interactive)"
15664
+ ).option("--no-llm", "Skip LLM calls, use heuristic fallback only").option(
15665
+ "--timeout <ms>",
15666
+ "LLM timeout in ms per call (default: 20000)",
15667
+ "20000"
15668
+ ).argument("[task]", "Specific task: titles, texts, duplicates, domains").action(async (taskName, opts) => {
15669
+ await withDb(async (db) => {
15670
+ const fix = !!opts.fix;
15671
+ const dryRun = !fix || !!opts.dryRun;
15672
+ const yes = !!opts.yes;
15673
+ const noLlm = opts.llm === false;
15674
+ const timeoutMs = parseDoctorTimeout(opts.timeout);
15675
+ if (!taskName) {
15676
+ console.log("Available doctor tasks:");
15677
+ for (const t2 of doctorTasks) {
15678
+ console.log(` ${t2.name} \u2014 ${t2.description}`);
15679
+ }
15680
+ console.log("\nRun with a task name, e.g. `zam doctor titles --fix`");
15681
+ return;
15682
+ }
15683
+ const task = doctorTasks.find((t2) => t2.name === taskName);
15684
+ if (!task) {
15685
+ console.error(`Unknown task: ${taskName}`);
15686
+ process.exit(1);
15687
+ }
15688
+ console.log(
15689
+ `Running doctor task: ${task.name} (fix=${fix}, dryRun=${dryRun}${noLlm ? ", noLlm=true" : ""})`
15690
+ );
15691
+ await task.run(db, { fix, dryRun, yes, noLlm, timeoutMs });
15692
+ });
15693
+ });
15694
+
14932
15695
  // src/cli/commands/git-sync.ts
14933
15696
  import { execSync as execSync5 } from "child_process";
14934
15697
  import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync8 } from "fs";
14935
15698
  import { join as join19 } from "path";
14936
- import { Command as Command5 } from "commander";
15699
+ import { Command as Command6 } from "commander";
14937
15700
  function installHook2() {
14938
15701
  const gitDir = join19(process.cwd(), ".git");
14939
15702
  if (!existsSync18(gitDir)) {
@@ -14963,7 +15726,7 @@ zam git-sync --commit HEAD --quiet
14963
15726
  process.exit(1);
14964
15727
  }
14965
15728
  }
14966
- var gitSyncCommand = new Command5("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
15729
+ var gitSyncCommand = new Command6("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
14967
15730
  if (opts.install) {
14968
15731
  installHook2();
14969
15732
  return;
@@ -15055,7 +15818,7 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
15055
15818
  import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
15056
15819
  import { resolve as resolve5 } from "path";
15057
15820
  import { input as input2 } from "@inquirer/prompts";
15058
- import { Command as Command6 } from "commander";
15821
+ import { Command as Command7 } from "commander";
15059
15822
  async function resolveGoalsDir() {
15060
15823
  let goalsDir;
15061
15824
  let db;
@@ -15068,7 +15831,7 @@ async function resolveGoalsDir() {
15068
15831
  }
15069
15832
  return goalsDir ? resolve5(goalsDir) : resolve5("goals");
15070
15833
  }
15071
- var goalCommand = new Command6("goal").description(
15834
+ var goalCommand = new Command7("goal").description(
15072
15835
  "Manage learning goals (markdown files)"
15073
15836
  );
15074
15837
  goalCommand.command("list").description("List all goals").option(
@@ -15241,7 +16004,7 @@ import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as
15241
16004
  import { homedir as homedir11 } from "os";
15242
16005
  import { join as join20, resolve as resolve6 } from "path";
15243
16006
  import { confirm, input as input3 } from "@inquirer/prompts";
15244
- import { Command as Command7 } from "commander";
16007
+ import { Command as Command8 } from "commander";
15245
16008
  var HOME2 = homedir11();
15246
16009
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
15247
16010
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
@@ -15278,7 +16041,7 @@ Here, I declare the core concepts and principles I want to master.
15278
16041
  );
15279
16042
  }
15280
16043
  }
15281
- var initCommand = new Command7("init").description("Launch the guided interactive onboarding wizard").action(async () => {
16044
+ var initCommand = new Command8("init").description("Launch the guided interactive onboarding wizard").action(async () => {
15282
16045
  printLine();
15283
16046
  console.log(
15284
16047
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -15455,7 +16218,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
15455
16218
 
15456
16219
  // src/cli/commands/learn.ts
15457
16220
  import { input as input5 } from "@inquirer/prompts";
15458
- import { Command as Command8 } from "commander";
16221
+ import { Command as Command9 } from "commander";
15459
16222
 
15460
16223
  // src/cli/learn-format.ts
15461
16224
  var BLOOM_VERBS3 = {
@@ -15471,16 +16234,22 @@ function clampBloom(n) {
15471
16234
  function formatHeader(input8) {
15472
16235
  const lvl = clampBloom(input8.bloomLevel);
15473
16236
  const parts = [`${BLOOM_VERBS3[lvl]} (Bloom ${lvl})`];
15474
- if (input8.domain?.trim()) {
16237
+ const name = input8.title || input8.slug;
16238
+ if (name) {
16239
+ parts.push(name);
16240
+ } else if (input8.domain?.trim()) {
15475
16241
  parts.push(input8.domain.trim());
15476
16242
  }
15477
16243
  return parts.join(" \xB7 ");
15478
16244
  }
15479
16245
  function formatReveal(input8) {
15480
16246
  const lines = [
15481
- `Token: #${input8.slug}`,
16247
+ `Token: ${input8.title || input8.slug}`,
15482
16248
  `Concept: ${input8.concept}`
15483
16249
  ];
16250
+ if (input8.title) {
16251
+ lines.push(`ID: #${input8.slug}`);
16252
+ }
15484
16253
  if (input8.context?.trim()) {
15485
16254
  lines.push("", `Context: ${input8.context.trim()}`);
15486
16255
  }
@@ -15793,7 +16562,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
15793
16562
  function isExitPrompt(err) {
15794
16563
  return err instanceof Error && err.name === "ExitPromptError";
15795
16564
  }
15796
- var learnCommand = new Command8("learn").description(
16565
+ var learnCommand = new Command9("learn").description(
15797
16566
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
15798
16567
  ).option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into the revealed answer").action(async (opts) => {
15799
16568
  let db;
@@ -15867,7 +16636,7 @@ ${t(locale, "welcome", { count: queue.items.length })}`);
15867
16636
  console.log(`
15868
16637
  ${"\u2500".repeat(50)}`);
15869
16638
  console.log(
15870
- `[${index + 1}/${queue.items.length}] ${formatHeader(item)}`
16639
+ `[${index + 1}/${queue.items.length}] ${formatHeader({ bloomLevel: item.bloomLevel, domain: item.domain })}`
15871
16640
  );
15872
16641
  console.log(`
15873
16642
  ${prompt.question}`);
@@ -15931,6 +16700,7 @@ ${t(locale, "eval_skipped", { reason: err.message })}`
15931
16700
  );
15932
16701
  const reveal = formatReveal({
15933
16702
  slug: item.slug,
16703
+ title: item.title,
15934
16704
  concept: item.concept,
15935
16705
  context: token?.context,
15936
16706
  resolved
@@ -16038,8 +16808,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
16038
16808
  });
16039
16809
 
16040
16810
  // src/cli/commands/monitor.ts
16041
- import { Command as Command9 } from "commander";
16042
- var monitorCommand = new Command9("monitor").description(
16811
+ import { Command as Command10 } from "commander";
16812
+ var monitorCommand = new Command10("monitor").description(
16043
16813
  "Shell observation for real-time task monitoring"
16044
16814
  );
16045
16815
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -16221,8 +16991,8 @@ monitorCommand.command("open").description("Open a new monitored terminal window
16221
16991
  });
16222
16992
 
16223
16993
  // src/cli/commands/observer.ts
16224
- import { Command as Command10 } from "commander";
16225
- var observerCommand = new Command10("observer").description(
16994
+ import { Command as Command11 } from "commander";
16995
+ var observerCommand = new Command11("observer").description(
16226
16996
  "Configure what the UI observer may capture (Layer 2 policy)"
16227
16997
  );
16228
16998
  function applyObserverListChange(current, entry, op) {
@@ -16285,7 +17055,7 @@ observerCommand.command("revoke <process>").description("Remove a process from o
16285
17055
 
16286
17056
  // src/cli/commands/profile.ts
16287
17057
  import { dirname as dirname6, resolve as resolve7 } from "path";
16288
- import { Command as Command11 } from "commander";
17058
+ import { Command as Command12 } from "commander";
16289
17059
  var C2 = {
16290
17060
  reset: "\x1B[0m",
16291
17061
  bold: "\x1B[1m",
@@ -16302,7 +17072,7 @@ function render(profile) {
16302
17072
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
16303
17073
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
16304
17074
  }
16305
- var profileCommand = new Command11("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
17075
+ var profileCommand = new Command12("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
16306
17076
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
16307
17077
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
16308
17078
  process.exit(1);
@@ -16342,8 +17112,8 @@ var profileCommand = new Command11("profile").description("Show or change this m
16342
17112
 
16343
17113
  // src/cli/commands/provider.ts
16344
17114
  import { password as password2 } from "@inquirer/prompts";
16345
- import { Command as Command12 } from "commander";
16346
- var providerCommand = new Command12("provider").description(
17115
+ import { Command as Command13 } from "commander";
17116
+ var providerCommand = new Command13("provider").description(
16347
17117
  "Configure role-based AI providers (url/model/flavor/key, per role)"
16348
17118
  );
16349
17119
  providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
@@ -16578,8 +17348,8 @@ providerCommand.command("clear-key <ref>").description("Remove a stored API key"
16578
17348
  });
16579
17349
 
16580
17350
  // src/cli/commands/review.ts
16581
- import { Command as Command13 } from "commander";
16582
- var reviewCommand = new Command13("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
17351
+ import { Command as Command14 } from "commander";
17352
+ var reviewCommand = new Command14("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
16583
17353
  let db;
16584
17354
  try {
16585
17355
  db = await openDatabase();
@@ -16617,9 +17387,8 @@ Review session: ${queue.items.length} card(s)`);
16617
17387
  });
16618
17388
  console.log(
16619
17389
  `
16620
- [${index + 1}/${queue.items.length}] ${prompt.bloomVerb} (Bloom ${prompt.bloomLevel})`
17390
+ [${index + 1}/${queue.items.length}] ${formatHeader({ bloomLevel: item.bloomLevel, domain: item.domain })}`
16621
17391
  );
16622
- console.log(`Domain: ${prompt.domain || "(none)"}`);
16623
17392
  if (prompt.sourceLink) {
16624
17393
  console.log(`Source: ${prompt.sourceLink}`);
16625
17394
  if (opts.resolve !== false) {
@@ -16694,8 +17463,8 @@ ${"\u2550".repeat(50)}`);
16694
17463
  // src/cli/commands/session.ts
16695
17464
  import { readFileSync as readFileSync13 } from "fs";
16696
17465
  import { input as input6, select as select2 } from "@inquirer/prompts";
16697
- import { Command as Command14 } from "commander";
16698
- var sessionCommand = new Command14("session").description(
17466
+ import { Command as Command15 } from "commander";
17467
+ var sessionCommand = new Command15("session").description(
16699
17468
  "Manage learning sessions"
16700
17469
  );
16701
17470
  sessionCommand.command("start").description("Start a new learning session (review \u2192 task)").option("--user <id>", "User ID (default: whoami)").option("--task <description>", "Task description (interactive if omitted)").option(
@@ -16819,9 +17588,8 @@ Time limit reached (${maxMinutes} min). Moving to task selection.`
16819
17588
  });
16820
17589
  const elapsed = Math.round((Date.now() - startTime) / 6e4);
16821
17590
  console.log(
16822
- `[${index + 1}/${queue.items.length}] ${prompt.bloomVerb} (Bloom ${prompt.bloomLevel}) \u2014 ${elapsed}/${maxMinutes} min`
17591
+ `[${index + 1}/${queue.items.length}] ${formatHeader({ bloomLevel: item.bloomLevel, domain: item.domain })} (${elapsed}/${maxMinutes} min)`
16823
17592
  );
16824
- console.log(`Domain: ${prompt.domain || "(none)"}`);
16825
17593
  console.log(`
16826
17594
  ${prompt.question}
16827
17595
  `);
@@ -17075,8 +17843,8 @@ sessionCommand.command("end").description("End a session and show summary").requ
17075
17843
 
17076
17844
  // src/cli/commands/settings.ts
17077
17845
  import { existsSync as existsSync21 } from "fs";
17078
- import { Command as Command15 } from "commander";
17079
- var settingsCommand = new Command15("settings").description(
17846
+ import { Command as Command16 } from "commander";
17847
+ var settingsCommand = new Command16("settings").description(
17080
17848
  "Manage user settings"
17081
17849
  );
17082
17850
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -17252,7 +18020,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
17252
18020
 
17253
18021
  // src/cli/commands/setup.ts
17254
18022
  import { resolve as resolve8 } from "path";
17255
- import { Command as Command16 } from "commander";
18023
+ import { Command as Command17 } from "commander";
17256
18024
  function formatDatabaseInitTarget(target) {
17257
18025
  switch (target.kind) {
17258
18026
  case "local":
@@ -17293,7 +18061,7 @@ async function activateMachineProviderConfig(db) {
17293
18061
  ` activate ${providerCount} machine-local provider(s) from ~/.zam/config.json`
17294
18062
  );
17295
18063
  }
17296
- var setupCommand = new Command16("setup").description(
18064
+ var setupCommand = new Command17("setup").description(
17297
18065
  "Link ZAM skill directories into this workspace and initialize the database"
17298
18066
  ).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
17299
18067
  "--agents <list>",
@@ -17347,8 +18115,8 @@ var setupCommand = new Command16("setup").description(
17347
18115
  );
17348
18116
 
17349
18117
  // src/cli/commands/skill.ts
17350
- import { Command as Command17 } from "commander";
17351
- var skillCommand = new Command17("skill").description(
18118
+ import { Command as Command18 } from "commander";
18119
+ var skillCommand = new Command18("skill").description(
17352
18120
  "Manage agent skill entries (task recipes)"
17353
18121
  );
17354
18122
  skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
@@ -17435,7 +18203,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
17435
18203
  // src/cli/commands/snapshot.ts
17436
18204
  import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
17437
18205
  import { dirname as dirname7, join as join21 } from "path";
17438
- import { Command as Command18 } from "commander";
18206
+ import { Command as Command19 } from "commander";
17439
18207
  function defaultOutName() {
17440
18208
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
17441
18209
  return `zam-snapshot-${stamp}.sql`;
@@ -17449,7 +18217,7 @@ function summarize(tables) {
17449
18217
  }
17450
18218
  return { total, nonEmpty };
17451
18219
  }
17452
- var exportCmd = new Command18("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
18220
+ var exportCmd = new Command19("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
17453
18221
  let db;
17454
18222
  try {
17455
18223
  db = await openDatabaseWithSync({ initialize: true });
@@ -17479,7 +18247,7 @@ var exportCmd = new Command18("export").description("Write a portable SQL-text s
17479
18247
  process.exit(1);
17480
18248
  }
17481
18249
  });
17482
- var importCmd = new Command18("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
18250
+ var importCmd = new Command19("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
17483
18251
  let db;
17484
18252
  try {
17485
18253
  if (!existsSync22(file)) {
@@ -17502,7 +18270,7 @@ var importCmd = new Command18("import").description("Restore a snapshot into the
17502
18270
  process.exit(1);
17503
18271
  }
17504
18272
  });
17505
- var verifyCmd = new Command18("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
18273
+ var verifyCmd = new Command19("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
17506
18274
  try {
17507
18275
  if (!existsSync22(file)) {
17508
18276
  console.error(`Error: Snapshot file not found: ${file}`);
@@ -17520,11 +18288,11 @@ var verifyCmd = new Command18("verify").description("Check a snapshot's manifest
17520
18288
  process.exit(1);
17521
18289
  }
17522
18290
  });
17523
- var snapshotCommand = new Command18("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
18291
+ var snapshotCommand = new Command19("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
17524
18292
 
17525
18293
  // src/cli/commands/stats.ts
17526
- import { Command as Command19 } from "commander";
17527
- var statsCommand = new Command19("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
18294
+ import { Command as Command20 } from "commander";
18295
+ var statsCommand = new Command20("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
17528
18296
  await withDb(async (db) => {
17529
18297
  const userId = await resolveUser(opts, db);
17530
18298
  const stats = await getUserStats(db, userId);
@@ -17560,8 +18328,8 @@ var statsCommand = new Command19("stats").description("Show learning dashboard f
17560
18328
  });
17561
18329
 
17562
18330
  // src/cli/commands/token.ts
17563
- import { Command as Command20 } from "commander";
17564
- var tokenCommand = new Command20("token").description(
18331
+ import { Command as Command21 } from "commander";
18332
+ var tokenCommand = new Command21("token").description(
17565
18333
  "Manage knowledge tokens"
17566
18334
  );
17567
18335
  tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--user <id>", "Owner of the personal card (default: whoami)").option("--no-card", "Register the token only; do not create a personal card").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
@@ -17612,7 +18380,11 @@ tokenCommand.command("register").description("Register a new knowledge token").r
17612
18380
  )
17613
18381
  );
17614
18382
  } else {
17615
- console.log(`Registered token: ${token.slug} (${token.id})`);
18383
+ console.log(`Registered token: ${token.title || token.slug}`);
18384
+ if (token.slug !== (token.title || token.slug)) {
18385
+ console.log(` Slug: ${token.slug}`);
18386
+ }
18387
+ console.log(` ID: ${token.id}`);
17616
18388
  console.log(` Concept: ${token.concept}`);
17617
18389
  console.log(` Domain: ${token.domain || "(none)"}`);
17618
18390
  console.log(` Bloom: ${token.bloom_level}`);
@@ -17631,8 +18403,9 @@ tokenCommand.command("register").description("Register a new knowledge token").r
17631
18403
  console.log(`
17632
18404
  WARNING: Possible duplicate tokens found:`);
17633
18405
  for (const dup of possibleDuplicates) {
18406
+ const name = dup.title || dup.slug;
17634
18407
  console.log(
17635
- ` - ${dup.slug} (similarity: ${dup.similarity.toFixed(2)})`
18408
+ ` - ${name} (slug: ${dup.slug}, similarity: ${dup.similarity.toFixed(2)})`
17636
18409
  );
17637
18410
  }
17638
18411
  }
@@ -17640,7 +18413,8 @@ WARNING: Possible duplicate tokens found:`);
17640
18413
  const queryText = embeddingContentForToken({
17641
18414
  concept: opts.concept,
17642
18415
  question: question ?? null,
17643
- domain: opts.domain ?? ""
18416
+ domain: opts.domain ?? "",
18417
+ title: opts.title ?? null
17644
18418
  });
17645
18419
  const q2 = await embedQuery(db, queryText);
17646
18420
  if (q2) {
@@ -17707,16 +18481,18 @@ tokenCommand.command("find").description("Fuzzy search for tokens").requiredOpti
17707
18481
  console.log(`Found ${results.length} token(s):
17708
18482
  `);
17709
18483
  console.log(
17710
- "Score Sim Slug Concept Domain Bloom"
18484
+ "Score Sim Title Concept Domain Bloom"
17711
18485
  );
17712
18486
  console.log("\u2500".repeat(95));
17713
18487
  for (const t2 of results) {
17714
18488
  const scoreStr = t2.score.toFixed(3).padEnd(6);
17715
18489
  const simStr = (t2.similarity?.toFixed(2) ?? "-").padEnd(4);
18490
+ const display = getDisplayTitle(t2);
17716
18491
  console.log(
17717
- `${scoreStr} ${simStr} ${t2.slug.padEnd(21)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
18492
+ `${scoreStr} ${simStr} ${display.padEnd(28)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
17718
18493
  );
17719
18494
  }
18495
+ console.log("\n(Use slug for CLI commands -- slugs are technical IDs)");
17720
18496
  });
17721
18497
  });
17722
18498
  tokenCommand.command("list").description("List all tokens").option("--domain <domain>", "Filter by domain").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
@@ -17735,16 +18511,20 @@ tokenCommand.command("list").description("List all tokens").option("--domain <do
17735
18511
  return;
17736
18512
  }
17737
18513
  console.log(
17738
- "Slug Concept Domain Bloom"
18514
+ "Title Concept Domain Bloom"
17739
18515
  );
17740
- console.log("\u2500".repeat(80));
18516
+ console.log("\u2500".repeat(85));
17741
18517
  for (const t2 of tokens) {
18518
+ const display = getDisplayTitle(t2);
17742
18519
  console.log(
17743
- `${t2.slug.padEnd(21)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
18520
+ `${display.padEnd(28)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
17744
18521
  );
17745
18522
  }
17746
18523
  console.log(`
17747
18524
  ${tokens.length} token(s) total.`);
18525
+ console.log(
18526
+ "(Use --slug <slug> for commands; slugs are technical identifiers)"
18527
+ );
17748
18528
  });
17749
18529
  });
17750
18530
  tokenCommand.command("edit").description("Edit a token's mutable fields").requiredOption("--slug <slug>", "Token slug").option("--concept <concept>", "Updated concept text").option("--domain <domain>", "Updated domain (blank allowed)").option("--bloom <level>", "Updated Bloom taxonomy level (1-5)").option("--context <context>", "Updated context (blank allowed)").option(
@@ -17753,7 +18533,10 @@ tokenCommand.command("edit").description("Edit a token's mutable fields").requir
17753
18533
  ).option(
17754
18534
  "--source-link <link>",
17755
18535
  "Updated source file path or reference URL (blank allowed)"
17756
- ).option("--question <question>", "Updated question text (blank allowed)").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
18536
+ ).option("--question <question>", "Updated question text (blank allowed)").option(
18537
+ "--title <title>",
18538
+ "Updated human-friendly title for graph display (blank allowed)"
18539
+ ).option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
17757
18540
  await withDb(async (db) => {
17758
18541
  const updates = {};
17759
18542
  if (opts.concept !== void 0) updates.concept = opts.concept;
@@ -17767,6 +18550,9 @@ tokenCommand.command("edit").description("Edit a token's mutable fields").requir
17767
18550
  if (opts.question !== void 0) {
17768
18551
  updates.question = opts.question === "" ? null : opts.question;
17769
18552
  }
18553
+ if (opts.title !== void 0) {
18554
+ updates.title = opts.title === "" ? "" : opts.title;
18555
+ }
17770
18556
  if (opts.mode !== void 0) {
17771
18557
  const validModes = ["shadowing", "copilot", "autonomy", "none"];
17772
18558
  if (!validModes.includes(opts.mode)) {
@@ -17781,11 +18567,15 @@ tokenCommand.command("edit").description("Edit a token's mutable fields").requir
17781
18567
  jsonOut(token);
17782
18568
  return;
17783
18569
  }
17784
- console.log(`Updated token: ${token.slug}`);
18570
+ console.log(`Updated token: ${token.title || token.slug}`);
18571
+ if (token.slug !== (token.title || token.slug)) {
18572
+ console.log(` Slug: ${token.slug}`);
18573
+ }
17785
18574
  console.log(` Concept: ${token.concept}`);
17786
18575
  console.log(` Domain: ${token.domain || "(none)"}`);
17787
18576
  console.log(` Bloom: ${token.bloom_level}`);
17788
18577
  console.log(` Question: ${token.question || "(none)"}`);
18578
+ console.log(` Title: ${token.title || "(none)"}`);
17789
18579
  console.log(` Context: ${token.context || "(none)"}`);
17790
18580
  console.log(` Mode: ${token.symbiosis_mode ?? "none"}`);
17791
18581
  console.log(` Source: ${token.source_link || "(none)"}`);
@@ -17904,7 +18694,11 @@ tokenCommand.command("status").description("Show full status of a token for a us
17904
18694
  console.log(JSON.stringify(status, null, 2));
17905
18695
  return;
17906
18696
  }
17907
- console.log(`Token: ${token.slug} (${token.id})`);
18697
+ console.log(`Token: ${token.title || token.slug}`);
18698
+ if (token.slug !== (token.title || token.slug)) {
18699
+ console.log(` Slug: ${token.slug}`);
18700
+ }
18701
+ console.log(` ID: ${token.id}`);
17908
18702
  console.log(` Concept: ${token.concept}`);
17909
18703
  console.log(` Question: ${token.question || "(none)"}`);
17910
18704
  console.log(` Domain: ${token.domain || "(none)"}`);
@@ -17929,7 +18723,9 @@ tokenCommand.command("status").description("Show full status of a token for a us
17929
18723
  if (prereqs.length > 0) {
17930
18724
  console.log("Prerequisites:");
17931
18725
  for (const p of prereqs) {
17932
- console.log(` - ${p.slug}: ${p.concept} (bloom ${p.bloom_level})`);
18726
+ console.log(
18727
+ ` - ${p.title || p.slug}: ${p.concept} (bloom ${p.bloom_level})`
18728
+ );
17933
18729
  }
17934
18730
  } else {
17935
18731
  console.log("No prerequisites.");
@@ -17937,7 +18733,9 @@ tokenCommand.command("status").description("Show full status of a token for a us
17937
18733
  if (dependents.length > 0) {
17938
18734
  console.log("\nDependents:");
17939
18735
  for (const d of dependents) {
17940
- console.log(` - ${d.slug}: ${d.concept} (bloom ${d.bloom_level})`);
18736
+ console.log(
18737
+ ` - ${d.title || d.slug}: ${d.concept} (bloom ${d.bloom_level})`
18738
+ );
17941
18739
  }
17942
18740
  }
17943
18741
  });
@@ -17999,7 +18797,7 @@ import { existsSync as existsSync23 } from "fs";
17999
18797
  import { homedir as homedir12 } from "os";
18000
18798
  import { dirname as dirname8, join as join22 } from "path";
18001
18799
  import { fileURLToPath as fileURLToPath3 } from "url";
18002
- import { Command as Command21 } from "commander";
18800
+ import { Command as Command22 } from "commander";
18003
18801
  var C3 = {
18004
18802
  reset: "\x1B[0m",
18005
18803
  red: "\x1B[31m",
@@ -18179,7 +18977,7 @@ function createShortcuts(appPath, repoRoot) {
18179
18977
  console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
18180
18978
  }
18181
18979
  }
18182
- var uiCommand = new Command21("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
18980
+ var uiCommand = new Command22("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
18183
18981
  "--build",
18184
18982
  "Build the native installer for your OS (needs Rust, one-time)"
18185
18983
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
@@ -18287,7 +19085,7 @@ import { existsSync as existsSync24, readFileSync as readFileSync15, realpathSyn
18287
19085
  import { dirname as dirname9, join as join23 } from "path";
18288
19086
  import { fileURLToPath as fileURLToPath4 } from "url";
18289
19087
  import { confirm as confirm3 } from "@inquirer/prompts";
18290
- import { Command as Command22 } from "commander";
19088
+ import { Command as Command23 } from "commander";
18291
19089
  var GITHUB_REPO = "zam-os/zam";
18292
19090
  var CHANNELS = [
18293
19091
  "developer",
@@ -18365,7 +19163,7 @@ function render2(decision) {
18365
19163
  );
18366
19164
  }
18367
19165
  }
18368
- var checkCmd = new Command22("check").description("Check whether a newer ZAM has been released").option(
19166
+ var checkCmd = new Command23("check").description("Check whether a newer ZAM has been released").option(
18369
19167
  "--latest <version>",
18370
19168
  "Compare against this version instead of fetching"
18371
19169
  ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
@@ -18532,7 +19330,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
18532
19330
  process.exit(1);
18533
19331
  }
18534
19332
  }
18535
- var updateCommand = new Command22("update").description(
19333
+ var updateCommand = new Command23("update").description(
18536
19334
  "Update ZAM to the latest release (use `update check` to only check)"
18537
19335
  ).option("-y, --yes", "Apply without confirmation").option(
18538
19336
  "--force",
@@ -18542,8 +19340,8 @@ var updateCommand = new Command22("update").description(
18542
19340
  }).addCommand(checkCmd);
18543
19341
 
18544
19342
  // src/cli/commands/whoami.ts
18545
- import { Command as Command23 } from "commander";
18546
- var whoamiCommand = new Command23("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
19343
+ import { Command as Command24 } from "commander";
19344
+ var whoamiCommand = new Command24("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
18547
19345
  await withDb(async (db) => {
18548
19346
  if (opts.set) {
18549
19347
  await setSetting(db, "user.id", opts.set);
@@ -18584,7 +19382,7 @@ import { existsSync as existsSync25, writeFileSync as writeFileSync11 } from "fs
18584
19382
  import { homedir as homedir13 } from "os";
18585
19383
  import { join as join24, resolve as resolve9 } from "path";
18586
19384
  import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
18587
- import { Command as Command24 } from "commander";
19385
+ import { Command as Command25 } from "commander";
18588
19386
  function runGit2(cwd, args) {
18589
19387
  try {
18590
19388
  return execFileSync4("git", args, {
@@ -18602,7 +19400,7 @@ function ghRepoCreateArgs(repoName, repoVisibility) {
18602
19400
  function gitRemoteArgs(githubUrl, hasOrigin) {
18603
19401
  return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
18604
19402
  }
18605
- var workspaceCommand = new Command24("workspace").description(
19403
+ var workspaceCommand = new Command25("workspace").description(
18606
19404
  "Manage your ZAM learning workspace"
18607
19405
  );
18608
19406
  var WORKSPACE_KINDS = [
@@ -18957,13 +19755,14 @@ var __dirname = dirname10(fileURLToPath5(import.meta.url));
18957
19755
  var pkg = JSON.parse(
18958
19756
  readFileSync16(join25(__dirname, "..", "..", "package.json"), "utf-8")
18959
19757
  );
18960
- var program = new Command25();
19758
+ var program = new Command26();
18961
19759
  program.name("zam").description(
18962
19760
  "The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
18963
19761
  ).version(pkg.version);
18964
19762
  program.addCommand(initCommand);
18965
19763
  program.addCommand(setupCommand);
18966
19764
  program.addCommand(tokenCommand);
19765
+ program.addCommand(doctorCommand);
18967
19766
  program.addCommand(cardCommand);
18968
19767
  program.addCommand(sessionCommand);
18969
19768
  program.addCommand(statsCommand);