zam-core 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 Command27 } 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),
@@ -681,6 +682,22 @@ CREATE TABLE IF NOT EXISTS agent_skills (
681
682
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
682
683
  );
683
684
 
685
+ -- Knowledge contexts: work, school, private
686
+ CREATE TABLE IF NOT EXISTS contexts (
687
+ id TEXT PRIMARY KEY,
688
+ name TEXT NOT NULL UNIQUE,
689
+ label TEXT,
690
+ language TEXT,
691
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
692
+ );
693
+
694
+ -- Join table mapping tokens to their knowledge contexts
695
+ CREATE TABLE IF NOT EXISTS token_contexts (
696
+ token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
697
+ context_id TEXT NOT NULL REFERENCES contexts(id) ON DELETE CASCADE,
698
+ PRIMARY KEY (token_id, context_id)
699
+ );
700
+
684
701
  -- Performance indexes
685
702
  CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain);
686
703
  CREATE INDEX IF NOT EXISTS idx_tokens_slug ON tokens(slug);
@@ -691,6 +708,8 @@ CREATE INDEX IF NOT EXISTS idx_cards_token_user ON cards(token_id, user_id);
691
708
  CREATE INDEX IF NOT EXISTS idx_review_logs_card ON review_logs(card_id);
692
709
  CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed_at);
693
710
  CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
711
+ CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title);
712
+ CREATE INDEX IF NOT EXISTS idx_token_contexts_context ON token_contexts(context_id);
694
713
  `;
695
714
 
696
715
  // src/kernel/db/sync-adapter.ts
@@ -1024,6 +1043,34 @@ async function runMigrations(db) {
1024
1043
  embedded_at TEXT NOT NULL DEFAULT (datetime('now'))
1025
1044
  )
1026
1045
  `);
1046
+ if (!tokenCols.some((c) => c.name === "title")) {
1047
+ await db.exec(
1048
+ `ALTER TABLE tokens ADD COLUMN title TEXT NOT NULL DEFAULT ''`
1049
+ );
1050
+ }
1051
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title)`);
1052
+ await db.exec(
1053
+ `CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain)`
1054
+ );
1055
+ await db.exec(`
1056
+ CREATE TABLE IF NOT EXISTS contexts (
1057
+ id TEXT PRIMARY KEY,
1058
+ name TEXT NOT NULL UNIQUE,
1059
+ label TEXT,
1060
+ language TEXT,
1061
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1062
+ )
1063
+ `);
1064
+ await db.exec(`
1065
+ CREATE TABLE IF NOT EXISTS token_contexts (
1066
+ token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
1067
+ context_id TEXT NOT NULL REFERENCES contexts(id) ON DELETE CASCADE,
1068
+ PRIMARY KEY (token_id, context_id)
1069
+ )
1070
+ `);
1071
+ await db.exec(`
1072
+ CREATE INDEX IF NOT EXISTS idx_token_contexts_context ON token_contexts(context_id)
1073
+ `);
1027
1074
  }
1028
1075
 
1029
1076
  // src/kernel/db/snapshot.ts
@@ -1487,41 +1534,110 @@ async function deleteCardForUser(db, tokenId, userId) {
1487
1534
  await db.prepare("DELETE FROM cards WHERE id = ?").run(card.id);
1488
1535
  return { card, impact };
1489
1536
  }
1490
- async function getDueCards(db, userId, now, domain) {
1537
+ async function getDueCards(db, userId, now, domain, knowledgeContext) {
1491
1538
  const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
1539
+ let sql = `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
1540
+ FROM cards c
1541
+ JOIN tokens t ON t.id = c.token_id
1542
+ WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ?`;
1543
+ const params = [userId, cutoff];
1492
1544
  if (domain) {
1493
- return await db.prepare(
1494
- `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
1495
- FROM cards c
1496
- JOIN tokens t ON t.id = c.token_id
1497
- WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ? AND t.domain = ?
1498
- ORDER BY t.bloom_level ASC, c.due_at ASC`
1499
- ).all(userId, cutoff, domain);
1545
+ sql += " AND t.domain = ?";
1546
+ params.push(domain);
1500
1547
  }
1501
- return await db.prepare(
1502
- `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
1503
- FROM cards c
1504
- JOIN tokens t ON t.id = c.token_id
1505
- WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ?
1506
- ORDER BY t.bloom_level ASC, c.due_at ASC`
1507
- ).all(userId, cutoff);
1548
+ if (knowledgeContext) {
1549
+ sql += ` AND EXISTS (
1550
+ SELECT 1 FROM token_contexts tc
1551
+ INNER JOIN contexts context_filter ON context_filter.id = tc.context_id
1552
+ WHERE tc.token_id = t.id AND context_filter.name = ?
1553
+ )`;
1554
+ params.push(knowledgeContext);
1555
+ }
1556
+ sql += " ORDER BY t.bloom_level ASC, c.due_at ASC";
1557
+ return await db.prepare(sql).all(...params);
1508
1558
  }
1509
1559
 
1510
- // src/kernel/models/token.ts
1560
+ // src/kernel/models/knowledge-context.ts
1511
1561
  import { ulid as ulid3 } from "ulid";
1512
- async function createToken(db, input8) {
1562
+ function normalizeContextName(name) {
1563
+ const normalized = name.trim();
1564
+ if (!normalized) {
1565
+ throw new Error("Context name cannot be empty");
1566
+ }
1567
+ return normalized;
1568
+ }
1569
+ function normalizeOptionalText(value) {
1570
+ if (value == null) return null;
1571
+ const normalized = value.trim();
1572
+ return normalized || null;
1573
+ }
1574
+ async function createKnowledgeContext(db, input8) {
1513
1575
  const id = ulid3();
1514
1576
  const now = (/* @__PURE__ */ new Date()).toISOString();
1577
+ const name = normalizeContextName(input8.name);
1578
+ const existing = await getKnowledgeContextByName(db, name);
1579
+ if (existing) {
1580
+ throw new Error(`Knowledge context with name "${name}" already exists`);
1581
+ }
1582
+ await db.prepare(
1583
+ `INSERT INTO contexts (id, name, label, language, created_at)
1584
+ VALUES (?, ?, ?, ?, ?)`
1585
+ ).run(
1586
+ id,
1587
+ name,
1588
+ normalizeOptionalText(input8.label),
1589
+ normalizeOptionalText(input8.language),
1590
+ now
1591
+ );
1592
+ const context = await getKnowledgeContextById(db, id);
1593
+ if (!context) {
1594
+ throw new Error(
1595
+ `Failed to retrieve newly created knowledge context: ${id}`
1596
+ );
1597
+ }
1598
+ return context;
1599
+ }
1600
+ async function getKnowledgeContextByName(db, name) {
1601
+ const normalized = name.trim();
1602
+ if (!normalized) return void 0;
1603
+ return await db.prepare("SELECT * FROM contexts WHERE name = ?").get(normalized);
1604
+ }
1605
+ async function getKnowledgeContextById(db, id) {
1606
+ return await db.prepare("SELECT * FROM contexts WHERE id = ?").get(id);
1607
+ }
1608
+ async function listKnowledgeContexts(db) {
1609
+ return await db.prepare("SELECT * FROM contexts ORDER BY name ASC").all();
1610
+ }
1611
+ async function deleteKnowledgeContext(db, id) {
1612
+ await db.prepare("DELETE FROM contexts WHERE id = ?").run(id);
1613
+ }
1614
+ async function assignTokenToContext(db, tokenId, contextId) {
1615
+ await db.prepare(
1616
+ `INSERT OR IGNORE INTO token_contexts (token_id, context_id)
1617
+ VALUES (?, ?)`
1618
+ ).run(tokenId, contextId);
1619
+ }
1620
+ async function unassignTokenFromContext(db, tokenId, contextId) {
1621
+ await db.prepare("DELETE FROM token_contexts WHERE token_id = ? AND context_id = ?").run(tokenId, contextId);
1622
+ }
1623
+
1624
+ // src/kernel/models/token.ts
1625
+ import { ulid as ulid4 } from "ulid";
1626
+ async function createToken(db, input8) {
1627
+ const id = ulid4();
1628
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1515
1629
  const bloom = input8.bloom_level ?? 1;
1516
1630
  if (bloom < 1 || bloom > 5) {
1517
1631
  throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1518
1632
  }
1633
+ const title = input8.title ?? "";
1519
1634
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1635
+ INSERT INTO tokens (id, slug, title, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
1636
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1522
1637
  `).run(
1523
1638
  id,
1524
1639
  input8.slug,
1640
+ title,
1525
1641
  input8.concept,
1526
1642
  input8.domain ?? "",
1527
1643
  bloom,
@@ -1564,6 +1680,10 @@ async function updateToken(db, slug, updates) {
1564
1680
  }
1565
1681
  const fields = [];
1566
1682
  const values = [];
1683
+ if (updates.title !== void 0) {
1684
+ fields.push("title = ?");
1685
+ values.push(updates.title ?? "");
1686
+ }
1567
1687
  if (updates.concept !== void 0) {
1568
1688
  fields.push("concept = ?");
1569
1689
  values.push(updates.concept);
@@ -1669,13 +1789,14 @@ async function deleteToken(db, slug) {
1669
1789
  await db.transaction(async (tx) => {
1670
1790
  const now = (/* @__PURE__ */ new Date()).toISOString();
1671
1791
  const skillRows = await tx.prepare("SELECT id, token_slugs FROM agent_skills").all();
1792
+ const skillUpdateStmt = tx.prepare(
1793
+ "UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
1794
+ );
1672
1795
  for (const row of skillRows) {
1673
1796
  const tokenSlugs = JSON.parse(row.token_slugs);
1674
1797
  const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
1675
1798
  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);
1799
+ await skillUpdateStmt.run(JSON.stringify(filtered), now, row.id);
1679
1800
  }
1680
1801
  }
1681
1802
  await tx.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
@@ -1689,10 +1810,16 @@ async function findTokens(db, query) {
1689
1810
  const shortTerms = searchTokens.filter((t2) => t2.length <= 2);
1690
1811
  const longTerms = searchTokens.filter((t2) => t2.length > 2);
1691
1812
  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 ?)`;
1813
+ 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 ?)`;
1814
+ const likeStmt = db.prepare(likeSQL);
1693
1815
  for (const term of longTerms) {
1694
1816
  const pattern = `%${term}%`;
1695
- const rows = await db.prepare(likeSQL).all(pattern, pattern, pattern);
1817
+ const rows = await likeStmt.all(
1818
+ pattern,
1819
+ pattern,
1820
+ pattern,
1821
+ pattern
1822
+ );
1696
1823
  for (const row of rows) {
1697
1824
  const entry = scoreMap.get(row.id);
1698
1825
  if (entry) {
@@ -1705,7 +1832,7 @@ async function findTokens(db, query) {
1705
1832
  if (shortTerms.length > 0 || longTerms.length === 0) {
1706
1833
  const allTokens = await db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
1707
1834
  for (const token of allTokens) {
1708
- const words = `${token.slug} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
1835
+ const words = `${token.slug} ${token.title} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
1709
1836
  let matchCount = 0;
1710
1837
  for (const term of shortTerms.length > 0 ? shortTerms : searchTokens) {
1711
1838
  for (const w of words) {
@@ -1734,23 +1861,47 @@ async function findTokens(db, query) {
1734
1861
  return scored;
1735
1862
  }
1736
1863
  async function listTokens(db, options) {
1737
- let tokens;
1864
+ const whereClauses = ["deprecated_at IS NULL"];
1865
+ const params = [];
1738
1866
  if (options?.domain) {
1739
- tokens = await db.prepare(
1740
- "SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
1741
- ).all(options.domain);
1742
- } else {
1743
- tokens = await db.prepare(
1744
- "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
1745
- ).all();
1746
- }
1867
+ whereClauses.push("domain = ?");
1868
+ params.push(options.domain);
1869
+ } else if (options?.domainPrefix) {
1870
+ const prefix = options.domainPrefix;
1871
+ whereClauses.push("(domain = ? OR domain LIKE ?)");
1872
+ params.push(prefix, `${prefix}/%`);
1873
+ }
1874
+ if (options?.knowledgeContext) {
1875
+ whereClauses.push(`EXISTS (
1876
+ SELECT 1 FROM token_contexts tc
1877
+ INNER JOIN contexts c ON c.id = tc.context_id
1878
+ WHERE tc.token_id = tokens.id AND c.name = ?
1879
+ )`);
1880
+ params.push(options.knowledgeContext);
1881
+ }
1882
+ const orderBy = options?.domain || options?.domainPrefix ? "ORDER BY bloom_level, slug" : "ORDER BY bloom_level, domain, slug";
1883
+ const sql = `SELECT * FROM tokens WHERE ${whereClauses.join(" AND ")} ${orderBy}`;
1884
+ const tokens = await db.prepare(sql).all(...params);
1747
1885
  for (const token of tokens) {
1748
1886
  parseTokenFallback(token);
1749
1887
  }
1750
1888
  return tokens;
1751
1889
  }
1752
1890
  function slugify(text) {
1753
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1891
+ return text.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1892
+ }
1893
+ function getShortSlug(slug, domainPrefix) {
1894
+ if (domainPrefix) {
1895
+ const folded = domainPrefix.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1896
+ if (folded && slug.startsWith(`${folded}-`)) {
1897
+ return slug.substring(folded.length + 1);
1898
+ }
1899
+ }
1900
+ return slug;
1901
+ }
1902
+ function getDisplayTitle(t2, activeDomainScope) {
1903
+ if (t2.title?.trim()) return t2.title.trim();
1904
+ return getShortSlug(t2.slug, activeDomainScope);
1754
1905
  }
1755
1906
  async function generateTokenSlug(db, domain, concept, question) {
1756
1907
  const baseText = question && question.trim().length > 0 ? question : concept;
@@ -1780,6 +1931,7 @@ async function listPersonalCards(db, userId, options) {
1780
1931
  SELECT
1781
1932
  t.id AS tokenId,
1782
1933
  t.slug,
1934
+ t.title,
1783
1935
  t.concept,
1784
1936
  t.domain,
1785
1937
  t.bloom_level AS bloomLevel,
@@ -1820,6 +1972,14 @@ async function listPersonalCards(db, userId, options) {
1820
1972
  sql += " AND t.domain = ?";
1821
1973
  values.push(options.domain);
1822
1974
  }
1975
+ if (options?.knowledgeContext) {
1976
+ sql += ` AND EXISTS (
1977
+ SELECT 1 FROM token_contexts tc
1978
+ INNER JOIN contexts kc ON kc.id = tc.context_id
1979
+ WHERE tc.token_id = t.id AND kc.name = ?
1980
+ )`;
1981
+ values.push(options.knowledgeContext);
1982
+ }
1823
1983
  if (options?.query) {
1824
1984
  const terms = options.query.toLowerCase().split(/\s+/).filter(Boolean);
1825
1985
  for (const term of terms) {
@@ -1881,6 +2041,7 @@ async function importCurriculumCards(db, userId, cards) {
1881
2041
  );
1882
2042
  token = await createToken(tx, {
1883
2043
  slug: finalSlug,
2044
+ title: card.title,
1884
2045
  concept: card.concept,
1885
2046
  domain: card.domain,
1886
2047
  bloom_level: bloom,
@@ -1998,23 +2159,28 @@ async function confirmCardSplit(db, userId, originalSlug, action, originalQuesti
1998
2159
  (/* @__PURE__ */ new Date()).toISOString(),
1999
2160
  originalToken.id
2000
2161
  );
2162
+ const insertPrereqStmt = tx.prepare(
2163
+ "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
2164
+ );
2001
2165
  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);
2166
+ await insertPrereqStmt.run(originalToken.id, propToken.id);
2005
2167
  }
2006
2168
  await tx.prepare(
2007
2169
  "UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
2008
2170
  ).run(originalToken.id, userId);
2171
+ const checkPrereqsStmt = tx.prepare(
2172
+ "SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
2173
+ );
2174
+ const unblockCardStmt = tx.prepare(
2175
+ "UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?"
2176
+ );
2009
2177
  for (const propToken of proposalTokens) {
2010
2178
  const card = await ensureCard(tx, propToken.id, userId);
2011
2179
  if (card.blocked === 1) {
2012
- const prereqOfPrereq = await tx.prepare(
2013
- "SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
2014
- ).get(propToken.id);
2180
+ const prereqOfPrereq = await checkPrereqsStmt.get(propToken.id);
2015
2181
  if (prereqOfPrereq.n === 0) {
2016
2182
  const now = (/* @__PURE__ */ new Date()).toISOString();
2017
- await tx.prepare("UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?").run(now, card.id);
2183
+ await unblockCardStmt.run(now, card.id);
2018
2184
  }
2019
2185
  }
2020
2186
  }
@@ -2085,6 +2251,7 @@ async function confirmFoundations(db, userId, originalSlug, proposals) {
2085
2251
  );
2086
2252
  token = await createToken(tx, {
2087
2253
  slug: finalSlug,
2254
+ title: card.title,
2088
2255
  concept: card.concept,
2089
2256
  domain: card.domain,
2090
2257
  bloom_level: bloom,
@@ -2151,6 +2318,7 @@ async function confirmSourceImport(db, userId, sourceId, proposals) {
2151
2318
  }
2152
2319
  token = await createToken(tx, {
2153
2320
  slug: finalSlug,
2321
+ title: card.title,
2154
2322
  concept: card.concept,
2155
2323
  domain: card.domain,
2156
2324
  bloom_level: bloom,
@@ -2253,7 +2421,7 @@ async function addPrerequisite(db, tokenId, requiresId) {
2253
2421
  }
2254
2422
  async function getPrerequisites(db, tokenId) {
2255
2423
  return await db.prepare(
2256
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
2424
+ `SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
2257
2425
  FROM prerequisites p
2258
2426
  JOIN tokens t ON t.id = p.requires_id
2259
2427
  WHERE p.token_id = ?`
@@ -2261,7 +2429,7 @@ async function getPrerequisites(db, tokenId) {
2261
2429
  }
2262
2430
  async function getDependents(db, tokenId) {
2263
2431
  return await db.prepare(
2264
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
2432
+ `SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
2265
2433
  FROM prerequisites p
2266
2434
  JOIN tokens t ON t.id = p.token_id
2267
2435
  WHERE p.requires_id = ?`
@@ -2292,6 +2460,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2292
2460
  const toNode = (t2, card) => ({
2293
2461
  id: t2.id,
2294
2462
  slug: t2.slug,
2463
+ title: t2.title ?? t2.slug,
2295
2464
  concept: t2.concept,
2296
2465
  domain: t2.domain,
2297
2466
  bloom_level: t2.bloom_level,
@@ -2311,6 +2480,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2311
2480
  {
2312
2481
  id: p.requires_id,
2313
2482
  slug: p.slug,
2483
+ title: p.title,
2314
2484
  concept: p.concept,
2315
2485
  domain: p.domain,
2316
2486
  bloom_level: p.bloom_level
@@ -2323,6 +2493,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2323
2493
  {
2324
2494
  id: d.token_id,
2325
2495
  slug: d.slug,
2496
+ title: d.title,
2326
2497
  concept: d.concept,
2327
2498
  domain: d.domain,
2328
2499
  bloom_level: d.bloom_level
@@ -2334,12 +2505,12 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2334
2505
  }
2335
2506
 
2336
2507
  // src/kernel/models/review.ts
2337
- import { ulid as ulid4 } from "ulid";
2508
+ import { ulid as ulid5 } from "ulid";
2338
2509
 
2339
2510
  // src/kernel/models/session.ts
2340
- import { ulid as ulid5 } from "ulid";
2511
+ import { ulid as ulid6 } from "ulid";
2341
2512
  async function startSession(db, input8) {
2342
- const id = ulid5();
2513
+ const id = ulid6();
2343
2514
  const now = (/* @__PURE__ */ new Date()).toISOString();
2344
2515
  const ctx = input8.execution_context ?? "shell";
2345
2516
  await db.prepare(
@@ -2373,7 +2544,7 @@ async function logStep(db, input8) {
2373
2544
  if (!session) {
2374
2545
  throw new Error(`Session not found: ${input8.session_id}`);
2375
2546
  }
2376
- const id = ulid5();
2547
+ const id = ulid6();
2377
2548
  const now = (/* @__PURE__ */ new Date()).toISOString();
2378
2549
  await db.prepare(
2379
2550
  `INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
@@ -2437,7 +2608,8 @@ import { createHash as createHash2 } from "crypto";
2437
2608
  function embeddingContentForToken(t2) {
2438
2609
  return `${t2.concept}
2439
2610
  ${t2.question ?? ""}
2440
- ${t2.domain}`;
2611
+ ${t2.domain}
2612
+ ${t2.title ?? ""}`;
2441
2613
  }
2442
2614
  function computeContentHash(text) {
2443
2615
  return createHash2("sha256").update(text, "utf8").digest("hex");
@@ -3254,10 +3426,10 @@ async function syncObserverSidecarPolicy(db, dir = getUiObserverDir()) {
3254
3426
  }
3255
3427
 
3256
3428
  // src/kernel/observation/session-synthesis.ts
3257
- import { ulid as ulid7 } from "ulid";
3429
+ import { ulid as ulid8 } from "ulid";
3258
3430
 
3259
3431
  // src/kernel/recall/evaluator.ts
3260
- import { ulid as ulid6 } from "ulid";
3432
+ import { ulid as ulid7 } from "ulid";
3261
3433
 
3262
3434
  // src/kernel/scheduler/fsrs.ts
3263
3435
  var DEFAULT_W = [
@@ -3436,7 +3608,7 @@ async function evaluateRatingWithinTransaction(db, input8) {
3436
3608
  due_at: updated.dueAt.toISOString(),
3437
3609
  last_review_at: now.toISOString()
3438
3610
  });
3439
- const reviewLogId = input8.reviewLogId ?? ulid6();
3611
+ const reviewLogId = input8.reviewLogId ?? ulid7();
3440
3612
  await db.prepare(
3441
3613
  `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
3442
3614
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
@@ -3770,7 +3942,7 @@ async function applySessionSynthesis(db, input8) {
3770
3942
  return { applied: false, record: parseSynthesisRow(existing) };
3771
3943
  }
3772
3944
  const card = await ensureCard(tx, token.id, session.user_id);
3773
- const reviewLogId = ulid7();
3945
+ const reviewLogId = ulid8();
3774
3946
  await evaluateRatingWithinTransaction(tx, {
3775
3947
  cardId: card.id,
3776
3948
  tokenId: token.id,
@@ -4596,11 +4768,11 @@ async function buildReviewQueue(db, options) {
4596
4768
  const maxReviews = options.maxReviews ?? 50;
4597
4769
  const now = options.now ?? /* @__PURE__ */ new Date();
4598
4770
  const nowISO = now.toISOString();
4599
- const dueRows = await db.prepare(
4600
- `SELECT
4771
+ let dueSql = `SELECT
4601
4772
  c.id AS card_id,
4602
4773
  c.token_id AS token_id,
4603
4774
  t.slug AS slug,
4775
+ t.title AS title,
4604
4776
  t.concept AS concept,
4605
4777
  t.domain AS domain,
4606
4778
  t.bloom_level AS bloom_level,
@@ -4614,14 +4786,23 @@ async function buildReviewQueue(db, options) {
4614
4786
  AND c.blocked = 0
4615
4787
  AND c.due_at <= ?
4616
4788
  AND c.state IN ('review', 'relearning', 'learning')
4617
- AND t.deprecated_at IS NULL
4618
- ORDER BY c.due_at ASC`
4619
- ).all(options.userId, nowISO);
4620
- const newRows = await db.prepare(
4621
- `SELECT
4789
+ AND t.deprecated_at IS NULL`;
4790
+ const dueParams = [options.userId, nowISO];
4791
+ if (options.knowledgeContext) {
4792
+ dueSql += ` AND EXISTS (
4793
+ SELECT 1 FROM token_contexts tc
4794
+ INNER JOIN contexts ctx ON ctx.id = tc.context_id
4795
+ WHERE tc.token_id = t.id AND ctx.name = ?
4796
+ )`;
4797
+ dueParams.push(options.knowledgeContext);
4798
+ }
4799
+ dueSql += ` ORDER BY c.due_at ASC`;
4800
+ const dueRows = await db.prepare(dueSql).all(...dueParams);
4801
+ let newSql = `SELECT
4622
4802
  c.id AS card_id,
4623
4803
  c.token_id AS token_id,
4624
4804
  t.slug AS slug,
4805
+ t.title AS title,
4625
4806
  t.concept AS concept,
4626
4807
  t.domain AS domain,
4627
4808
  t.bloom_level AS bloom_level,
@@ -4634,10 +4815,19 @@ async function buildReviewQueue(db, options) {
4634
4815
  WHERE c.user_id = ?
4635
4816
  AND c.blocked = 0
4636
4817
  AND c.state = 'new'
4637
- AND t.deprecated_at IS NULL
4638
- ORDER BY t.bloom_level ASC, t.slug ASC
4639
- LIMIT ?`
4640
- ).all(options.userId, maxNew);
4818
+ AND t.deprecated_at IS NULL`;
4819
+ const newParams = [options.userId];
4820
+ if (options.knowledgeContext) {
4821
+ newSql += ` AND EXISTS (
4822
+ SELECT 1 FROM token_contexts tc
4823
+ INNER JOIN contexts ctx ON ctx.id = tc.context_id
4824
+ WHERE tc.token_id = t.id AND ctx.name = ?
4825
+ )`;
4826
+ newParams.push(options.knowledgeContext);
4827
+ }
4828
+ newSql += ` ORDER BY t.bloom_level ASC, t.slug ASC LIMIT ?`;
4829
+ newParams.push(maxNew);
4830
+ const newRows = await db.prepare(newSql).all(...newParams);
4641
4831
  const nowMs = now.getTime();
4642
4832
  const sortedDue = [...dueRows].sort((a, b) => {
4643
4833
  const overdueA = nowMs - new Date(a.due_at).getTime();
@@ -4681,6 +4871,7 @@ function rowToItem(row) {
4681
4871
  cardId: row.card_id,
4682
4872
  tokenId: row.token_id,
4683
4873
  slug: row.slug,
4874
+ title: getDisplayTitle(row),
4684
4875
  concept: row.concept,
4685
4876
  domain: row.domain,
4686
4877
  bloomLevel: row.bloom_level,
@@ -5367,6 +5558,27 @@ function detectSyncProvider(dir) {
5367
5558
  }
5368
5559
  return null;
5369
5560
  }
5561
+ function getActiveWorkspaceContext(path = defaultConfigPath()) {
5562
+ const activeWorkspace = getActiveWorkspace(path);
5563
+ return activeWorkspace?.activeKnowledgeContext;
5564
+ }
5565
+ function setActiveWorkspaceContext(contextName, path = defaultConfigPath()) {
5566
+ const config = loadInstallConfig(path);
5567
+ const activeId = config.activeWorkspaceId;
5568
+ if (activeId && config.workspaces) {
5569
+ const workspace = config.workspaces.find((w) => w.id === activeId);
5570
+ if (workspace) {
5571
+ if (contextName) {
5572
+ workspace.activeKnowledgeContext = contextName;
5573
+ } else {
5574
+ delete workspace.activeKnowledgeContext;
5575
+ }
5576
+ saveInstallConfig(config, path);
5577
+ return true;
5578
+ }
5579
+ }
5580
+ return false;
5581
+ }
5370
5582
 
5371
5583
  // src/kernel/system/installer.ts
5372
5584
  import { execFileSync, execSync } from "child_process";
@@ -6233,7 +6445,7 @@ import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync a
6233
6445
  import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
6234
6446
  import { join as join18, resolve as resolve4 } from "path";
6235
6447
  import { Command as Command2 } from "commander";
6236
- import { ulid as ulid8 } from "ulid";
6448
+ import { ulid as ulid9 } from "ulid";
6237
6449
 
6238
6450
  // src/cli/adapters/source-reader.ts
6239
6451
  import dns from "dns";
@@ -6640,6 +6852,11 @@ function parseGeneratedCardArray(responseText, label, limits) {
6640
6852
  );
6641
6853
  }
6642
6854
  }
6855
+ if (card.title !== void 0 && (typeof card.title !== "string" || card.title.trim().length === 0)) {
6856
+ throw new Error(
6857
+ `Invalid ${label} card at index ${index}: title must be a non-empty string if provided`
6858
+ );
6859
+ }
6643
6860
  if (typeof card.bloom_level !== "number" || !Number.isInteger(card.bloom_level) || card.bloom_level < 1 || card.bloom_level > 5) {
6644
6861
  throw new Error(
6645
6862
  `Invalid ${label} card at index ${index}: bloom_level must be an integer from 1 to 5`
@@ -6653,6 +6870,7 @@ function parseGeneratedCardArray(responseText, label, limits) {
6653
6870
  return {
6654
6871
  question: card.question,
6655
6872
  concept: card.concept,
6873
+ title: card.title ? card.title : void 0,
6656
6874
  domain: card.domain,
6657
6875
  context: card.context,
6658
6876
  bloom_level: card.bloom_level,
@@ -6703,10 +6921,11 @@ Your task is to analyze curriculum objectives, syllabus requirements, or textboo
6703
6921
  For each extracted learning card, you MUST generate:
6704
6922
  1. "question": A clear, concise active-recall question testing the concept. The question must not reveal the answer itself.
6705
6923
  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).
6924
+ 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.
6925
+ 4. "domain": The category of the card. Use "${targetCategory}" as the default, but you may refine it if a specific sub-topic is evident.
6926
+ 5. "context": The exact sentence or short excerpt from the source text that this card is based on.
6927
+ 6. "bloom_level": Initial Bloom cognitive level (1 = Remember, 2 = Understand, 3 = Apply, 4 = Analyze, 5 = Synthesize).
6928
+ 7. "symbiosis_mode": ZAM agent symbiosis level: "shadowing" (reading/monitoring), "copilot" (interactive helper), or "autonomy" (autonomous tasks).
6710
6929
 
6711
6930
  Guidelines:
6712
6931
  - Break complex requirements into multiple separate, atomic cards.
@@ -6753,20 +6972,28 @@ JSON Array Output:`;
6753
6972
  );
6754
6973
  return readChatContent(res, "LLM curriculum import");
6755
6974
  }
6756
- async function importCurriculumViaLLM(db, text, targetCategory, sourceUrl) {
6975
+ async function importCurriculumViaLLM(db, text, targetCategory, sourceUrl, options) {
6757
6976
  if (text.length > MAX_IMPORT_TEXT_CHARS) {
6758
6977
  throw new Error(
6759
6978
  `Curriculum text exceeds the ${MAX_IMPORT_TEXT_CHARS.toLocaleString()} character limit`
6760
6979
  );
6761
6980
  }
6762
6981
  const cfg = await getProviderForRole(db, "text");
6982
+ let locale = cfg.locale;
6983
+ const contextName = options?.knowledgeContext || getActiveWorkspaceContext();
6984
+ if (contextName) {
6985
+ const context = await getKnowledgeContextByName(db, contextName);
6986
+ if (context?.language) {
6987
+ locale = normalizeLocale(context.language);
6988
+ }
6989
+ }
6763
6990
  const endpoint = await resolveUsableTextEndpoint(db);
6764
- const langName = LANGUAGE_NAMES[cfg.locale] || "English";
6991
+ const langName = LANGUAGE_NAMES[locale] || "English";
6765
6992
  let responseText;
6766
6993
  try {
6767
6994
  responseText = await requestCurriculumCards(
6768
6995
  endpoint,
6769
- cfg.locale,
6996
+ locale,
6770
6997
  langName,
6771
6998
  text,
6772
6999
  targetCategory,
@@ -6783,7 +7010,7 @@ async function importCurriculumViaLLM(db, text, targetCategory, sourceUrl) {
6783
7010
  try {
6784
7011
  chunkResponse = await requestCurriculumCards(
6785
7012
  endpoint,
6786
- cfg.locale,
7013
+ locale,
6787
7014
  langName,
6788
7015
  chunk,
6789
7016
  targetCategory,
@@ -7635,6 +7862,122 @@ async function ensureHighQualityQuestion(db, token) {
7635
7862
  }
7636
7863
  return null;
7637
7864
  }
7865
+ async function generateTitleViaLLM(db, input8, opts = {}) {
7866
+ const cfg = await getProviderForRole(db, "text");
7867
+ let locale = cfg.locale;
7868
+ const contextName = opts.knowledgeContext || getActiveWorkspaceContext();
7869
+ if (contextName) {
7870
+ const context = await getKnowledgeContextByName(db, contextName);
7871
+ if (context?.language) {
7872
+ locale = normalizeLocale(context.language);
7873
+ }
7874
+ }
7875
+ const endpoint = await resolveUsableTextEndpoint(db);
7876
+ const systemPrompt = `You are an expert at naming knowledge items for a personal knowledge graph.
7877
+ Your task: given a knowledge token's concept (the full reference answer), question, domain, and optional context, produce a short, descriptive, human-friendly TITLE.
7878
+
7879
+ Strict rules from the project ADR:
7880
+ - Concise name for the concept (\u2264 80 chars ideal).
7881
+ - Thoughtful, memorable name \u2014 NOT a definition or the first sentence of the concept.
7882
+ - NEVER echo the domain name (e.g. do not say "Workflow Engine Node Drain" if domain is "workflow-engine"; just "Node Drain Protection").
7883
+ - Prefer a name over spoiling the full concept.
7884
+ - Support Unicode (umlauts, etc.).
7885
+ - 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).
7886
+ - Output ONLY the raw title text. No preamble, quotes, explanations, or markdown.
7887
+
7888
+ Example good titles:
7889
+ - "RAG Retrieval Quality Evaluation" (for RAG failure modes focused on retrieval)
7890
+ - "Pythagorean Theorem Converse Proof"
7891
+ - "macOS Applications Directory Layout"
7892
+ - "Chronotype Sleep Preference"`;
7893
+ const userPrompt = `Domain: ${input8.domain}
7894
+ Slug: ${input8.slug}
7895
+ Question: ${input8.question || "(none)"}
7896
+ Concept: ${input8.concept}
7897
+ Context: ${input8.context || "(none)"}
7898
+
7899
+ Title:`;
7900
+ const url = `${endpoint.url}/chat/completions`;
7901
+ const apiKey = endpoint.apiKey;
7902
+ const model = endpoint.model;
7903
+ try {
7904
+ const res = await fetchWithInteractiveTimeout(url, {
7905
+ method: "POST",
7906
+ headers: {
7907
+ "Content-Type": "application/json",
7908
+ Authorization: `Bearer ${apiKey}`
7909
+ },
7910
+ body: JSON.stringify({
7911
+ model,
7912
+ messages: [
7913
+ { role: "system", content: systemPrompt },
7914
+ { role: "user", content: userPrompt }
7915
+ ],
7916
+ temperature: 0.2,
7917
+ max_tokens: 100
7918
+ }),
7919
+ locale,
7920
+ timeoutMs: opts.timeoutMs,
7921
+ hardTimeoutMs: opts.timeoutMs
7922
+ });
7923
+ const text = await readChatContent(res, "title generation");
7924
+ return {
7925
+ text: text.trim(),
7926
+ model,
7927
+ providerName: endpoint.providerName
7928
+ };
7929
+ } catch (_e) {
7930
+ return {
7931
+ text: input8.concept.split(/[.;]/)[0].trim().substring(0, 80),
7932
+ model: "fallback",
7933
+ providerName: "heuristic"
7934
+ };
7935
+ }
7936
+ }
7937
+ async function repairUmlautsViaLLM(db, input8, opts = {}) {
7938
+ const cfg = await getProviderForRole(db, "text");
7939
+ const endpoint = await resolveUsableTextEndpoint(db);
7940
+ const systemPrompt = `You are an expert editor specializing in German language orthography.
7941
+ 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.
7942
+
7943
+ Strict rules:
7944
+ - 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").
7945
+ - 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").
7946
+ - Output ONLY the repaired text. No preamble, no explanation, no markdown formatting.`;
7947
+ const userPrompt = `Text to repair:
7948
+ """
7949
+ ${input8.text}
7950
+ """
7951
+
7952
+ Repaired text:`;
7953
+ const url = `${endpoint.url}/chat/completions`;
7954
+ const apiKey = endpoint.apiKey;
7955
+ const model = endpoint.model;
7956
+ try {
7957
+ const res = await fetchWithInteractiveTimeout(url, {
7958
+ method: "POST",
7959
+ headers: {
7960
+ "Content-Type": "application/json",
7961
+ Authorization: `Bearer ${apiKey}`
7962
+ },
7963
+ body: JSON.stringify({
7964
+ model,
7965
+ messages: [
7966
+ { role: "system", content: systemPrompt },
7967
+ { role: "user", content: userPrompt }
7968
+ ],
7969
+ temperature: 0.1
7970
+ }),
7971
+ locale: cfg.locale,
7972
+ timeoutMs: opts.timeoutMs,
7973
+ hardTimeoutMs: opts.timeoutMs
7974
+ });
7975
+ const text = await readChatContent(res, "umlaut repair");
7976
+ return text.trim();
7977
+ } catch (_e) {
7978
+ return input8.text;
7979
+ }
7980
+ }
7638
7981
 
7639
7982
  // src/cli/adapters/source-reader.ts
7640
7983
  var MAX_SOURCE_BYTES = 2 * 1024 * 1024;
@@ -7709,7 +8052,7 @@ async function readWebLink(url) {
7709
8052
  redirect: "manual",
7710
8053
  signal: controller.signal,
7711
8054
  headers: {
7712
- "User-Agent": "ZAM-Content-Studio/0.6.3"
8055
+ "User-Agent": "ZAM-Content-Studio/0.8.0"
7713
8056
  }
7714
8057
  });
7715
8058
  if (res.status >= 300 && res.status < 400) {
@@ -7946,7 +8289,7 @@ function cleanHtmlText(html) {
7946
8289
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
7947
8290
  }
7948
8291
  function normalizeForComparison(str) {
7949
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8292
+ 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
8293
  }
7951
8294
 
7952
8295
  // src/cli/curriculum/providers/bildungsplan-bw/manifest.ts
@@ -8117,7 +8460,7 @@ function cleanHtmlText2(html) {
8117
8460
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8118
8461
  }
8119
8462
  function normalizeForComparison2(str) {
8120
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8463
+ 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
8464
  }
8122
8465
 
8123
8466
  // src/cli/curriculum/providers/bildungsplan-hamburg/manifest.ts
@@ -8275,7 +8618,7 @@ function cleanHtmlText3(html) {
8275
8618
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8276
8619
  }
8277
8620
  function normalizeForComparison3(str) {
8278
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8621
+ 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
8622
  }
8280
8623
 
8281
8624
  // src/cli/curriculum/providers/fachanforderungen-sh/manifest.ts
@@ -8432,7 +8775,7 @@ function cleanHtmlText4(html) {
8432
8775
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8433
8776
  }
8434
8777
  function normalizeForComparison4(str) {
8435
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8778
+ 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
8779
  }
8437
8780
 
8438
8781
  // src/cli/curriculum/providers/kerncurriculum-hessen/manifest.ts
@@ -8626,7 +8969,7 @@ function cleanHtmlText5(html) {
8626
8969
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8627
8970
  }
8628
8971
  function normalizeForComparison5(str) {
8629
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8972
+ 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
8973
  }
8631
8974
 
8632
8975
  // src/cli/curriculum/providers/kerncurriculum-niedersachsen/manifest.ts
@@ -8798,7 +9141,7 @@ function cleanHtmlText6(html) {
8798
9141
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8799
9142
  }
8800
9143
  function normalizeForComparison6(str) {
8801
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9144
+ 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
9145
  }
8803
9146
 
8804
9147
  // src/cli/curriculum/providers/kernlehrplan-nrw/manifest.ts
@@ -9013,7 +9356,7 @@ function cleanHtmlText7(html) {
9013
9356
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9014
9357
  }
9015
9358
  function normalizeForComparison7(str) {
9016
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9359
+ 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
9360
  }
9018
9361
 
9019
9362
  // src/cli/curriculum/providers/lehrplaene-rp/manifest.ts
@@ -9170,7 +9513,7 @@ function cleanHtmlText8(html) {
9170
9513
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9171
9514
  }
9172
9515
  function normalizeForComparison8(str) {
9173
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9516
+ 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
9517
  }
9175
9518
 
9176
9519
  // src/cli/curriculum/providers/lehrplan-saarland/manifest.ts
@@ -9327,7 +9670,7 @@ function cleanHtmlText9(html) {
9327
9670
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9328
9671
  }
9329
9672
  function normalizeForComparison9(str) {
9330
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9673
+ 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
9674
  }
9332
9675
 
9333
9676
  // src/cli/curriculum/providers/lehrplan-sachsen/manifest.ts
@@ -9512,7 +9855,7 @@ function cleanHtmlText10(html) {
9512
9855
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9513
9856
  }
9514
9857
  function normalizeForComparison10(str) {
9515
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9858
+ 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
9859
  }
9517
9860
 
9518
9861
  // src/cli/curriculum/providers/lehrplan-thueringen/manifest.ts
@@ -9667,7 +10010,7 @@ function cleanHtmlText11(html) {
9667
10010
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9668
10011
  }
9669
10012
  function normalizeForComparison11(str) {
9670
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10013
+ 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
10014
  }
9672
10015
 
9673
10016
  // src/cli/curriculum/providers/lehrplanplus-bayern/manifest.ts
@@ -9911,7 +10254,7 @@ function cleanHtmlText12(html) {
9911
10254
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9912
10255
  }
9913
10256
  function normalizeForComparison12(str) {
9914
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10257
+ 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
10258
  }
9916
10259
 
9917
10260
  // src/cli/curriculum/providers/rahmenlehrplan-berlin-brandenburg/manifest.ts
@@ -10096,7 +10439,7 @@ function cleanHtmlText13(html) {
10096
10439
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
10097
10440
  }
10098
10441
  function normalizeForComparison13(str) {
10099
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10442
+ 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
10443
  }
10101
10444
 
10102
10445
  // src/cli/curriculum/providers/rahmenplan-mv/manifest.ts
@@ -10253,7 +10596,7 @@ function cleanHtmlText14(html) {
10253
10596
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
10254
10597
  }
10255
10598
  function normalizeForComparison14(str) {
10256
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10599
+ 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
10600
  }
10258
10601
 
10259
10602
  // src/cli/curriculum/providers/rahmenrichtlinien-st/manifest.ts
@@ -10408,7 +10751,7 @@ function cleanHtmlText15(html) {
10408
10751
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
10409
10752
  }
10410
10753
  function normalizeForComparison15(str) {
10411
- return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10754
+ 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
10755
  }
10413
10756
 
10414
10757
  // src/cli/curriculum/registry.ts
@@ -10433,6 +10776,25 @@ function getCurriculumProvider(id) {
10433
10776
  return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
10434
10777
  }
10435
10778
 
10779
+ // src/cli/knowledge-contexts.ts
10780
+ async function resolveOperationKnowledgeContexts(db, requestedNames) {
10781
+ const explicitNames = [
10782
+ ...new Set(requestedNames.map((name) => name.trim()).filter(Boolean))
10783
+ ];
10784
+ const activeDefault = getActiveWorkspaceContext();
10785
+ const selectedNames = explicitNames.length > 0 ? explicitNames : activeDefault ? [activeDefault] : [];
10786
+ const contexts = [];
10787
+ for (const name of selectedNames) {
10788
+ const context = await getKnowledgeContextByName(db, name);
10789
+ if (!context) {
10790
+ const subject = explicitNames.length > 0 ? "Knowledge" : "Active knowledge";
10791
+ throw new Error(`${subject} context not found: ${name}`);
10792
+ }
10793
+ contexts.push(context);
10794
+ }
10795
+ return contexts;
10796
+ }
10797
+
10436
10798
  // src/cli/llm/embedder.ts
10437
10799
  var CANONICAL_EMBEDDING_MODEL_ID = "embeddinggemma-300m";
10438
10800
  var EMBEDDINGGEMMA_ALIASES = /* @__PURE__ */ new Set([
@@ -10606,7 +10968,8 @@ async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
10606
10968
  const queryText = embeddingContentForToken({
10607
10969
  concept: candidate.concept,
10608
10970
  question: candidate.question ?? null,
10609
- domain: candidate.domain ?? ""
10971
+ domain: candidate.domain ?? "",
10972
+ title: candidate.title ?? null
10610
10973
  });
10611
10974
  const q2 = await embed(db, queryText);
10612
10975
  if (!q2) {
@@ -10637,6 +11000,7 @@ async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
10637
11000
  results.push({
10638
11001
  slug: hit.slug,
10639
11002
  concept: hit.concept,
11003
+ title: hit.title,
10640
11004
  similarity: hit.similarity
10641
11005
  });
10642
11006
  }
@@ -11745,6 +12109,20 @@ function jsonError(message) {
11745
12109
  console.log(JSON.stringify({ error: msg }, null, 2));
11746
12110
  process.exit(1);
11747
12111
  }
12112
+ function parseKnowledgeContextNames(value) {
12113
+ if (value == null) return [];
12114
+ if (!Array.isArray(value) || value.some((name) => typeof name !== "string" || !name.trim())) {
12115
+ jsonError("knowledgeContexts must be an array of non-empty context names");
12116
+ }
12117
+ return [...new Set(value.map((name) => name.trim()))];
12118
+ }
12119
+ async function resolveKnowledgeContexts(db, names) {
12120
+ try {
12121
+ return await resolveOperationKnowledgeContexts(db, names);
12122
+ } catch (error) {
12123
+ jsonError(error.message);
12124
+ }
12125
+ }
11748
12126
  function parseNonNegativeIntegerOption(name, value) {
11749
12127
  const parsed = Number(value);
11750
12128
  if (!Number.isInteger(parsed) || parsed < 0) {
@@ -11798,16 +12176,23 @@ function parseTokenUpdates(opts) {
11798
12176
  var bridgeCommand = new Command2("bridge").description(
11799
12177
  "Machine-readable JSON protocol for AI integration"
11800
12178
  );
11801
- bridgeCommand.command("check-due").description("Check due cards for a user (JSON)").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").action(async (opts) => {
12179
+ bridgeCommand.command("check-due").description("Check due cards for a user (JSON)").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").option("--knowledge-context <context>", "Filter by knowledge context").action(async (opts) => {
11802
12180
  await withDb2(async (db) => {
11803
12181
  const userId = await resolveUser(opts, db, { json: true });
11804
- const dueCards = await getDueCards(db, userId, void 0, opts.domain);
12182
+ const dueCards = await getDueCards(
12183
+ db,
12184
+ userId,
12185
+ void 0,
12186
+ opts.domain,
12187
+ opts.knowledgeContext
12188
+ );
11805
12189
  const domains = [
11806
12190
  ...new Set(dueCards.map((c) => c.domain).filter(Boolean))
11807
12191
  ].sort();
11808
12192
  jsonOut2({
11809
12193
  userId,
11810
12194
  domain: opts.domain ?? null,
12195
+ knowledgeContext: opts.knowledgeContext ?? null,
11811
12196
  dueCount: dueCards.length,
11812
12197
  domains,
11813
12198
  cards: dueCards.map((c) => ({
@@ -12068,13 +12453,14 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
12068
12453
  bridgeCommand.command("get-review").description("Get next review card with prompt (JSON)").option("--user <id>", "User ID (default: whoami)").option("--no-resolve", "Skip resolving the token's source_link into context").option(
12069
12454
  "--no-dynamic-question",
12070
12455
  "Use the stored question without generating a fresh LLM question"
12071
- ).action(async (opts) => {
12456
+ ).option("--knowledge-context <context>", "Filter cards by knowledge context").action(async (opts) => {
12072
12457
  await withDb2(async (db) => {
12073
12458
  const userId = await resolveUser(opts, db, { json: true });
12074
12459
  const queue = await buildReviewQueue(db, {
12075
12460
  userId,
12076
12461
  maxReviews: 1,
12077
- maxNew: 1
12462
+ maxNew: 1,
12463
+ knowledgeContext: opts.knowledgeContext || void 0
12078
12464
  });
12079
12465
  if (queue.items.length === 0) {
12080
12466
  jsonOut2({
@@ -12129,7 +12515,10 @@ bridgeCommand.command("get-review").description("Get next review card with promp
12129
12515
  resolvedContext = null;
12130
12516
  }
12131
12517
  }
12132
- const fullQueue = await buildReviewQueue(db, { userId });
12518
+ const fullQueue = await buildReviewQueue(db, {
12519
+ userId,
12520
+ knowledgeContext: opts.knowledgeContext || void 0
12521
+ });
12133
12522
  jsonOut2({
12134
12523
  userId,
12135
12524
  hasReview: true,
@@ -12400,10 +12789,16 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
12400
12789
  const possibleDuplicates = await findPossibleDuplicates(db, {
12401
12790
  concept: data?.concept,
12402
12791
  question: data?.question ?? null,
12403
- domain: data?.domain
12792
+ domain: data?.domain,
12793
+ title: data?.title ?? null
12404
12794
  });
12795
+ const ctxNames = parseKnowledgeContextNames(
12796
+ data?.knowledgeContexts ?? data?.knowledge_contexts
12797
+ );
12798
+ const assignedContexts = await resolveKnowledgeContexts(db, ctxNames);
12405
12799
  const token = await createToken(db, {
12406
12800
  slug: data?.slug,
12801
+ title: data?.title,
12407
12802
  concept: data?.concept,
12408
12803
  domain: data?.domain,
12409
12804
  bloom_level: data?.bloom_level ?? 1,
@@ -12412,6 +12807,9 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
12412
12807
  source_link: data?.source_link ?? null,
12413
12808
  question: data?.question ?? null
12414
12809
  });
12810
+ for (const context of assignedContexts) {
12811
+ await assignTokenToContext(db, token.id, context.id);
12812
+ }
12415
12813
  const card = await ensureCard(db, token.id, userId);
12416
12814
  try {
12417
12815
  await ensureTokenEmbeddings(db, { limit: 8 });
@@ -12419,7 +12817,14 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
12419
12817
  }
12420
12818
  jsonOut2({
12421
12819
  success: true,
12422
- token,
12820
+ token: {
12821
+ ...token,
12822
+ knowledgeContexts: assignedContexts.map((c) => ({
12823
+ name: c.name,
12824
+ label: c.label,
12825
+ language: c.language
12826
+ }))
12827
+ },
12423
12828
  card: {
12424
12829
  id: card.id,
12425
12830
  tokenId: card.token_id,
@@ -12479,15 +12884,34 @@ bridgeCommand.command("relevant-tokens").description("Find tokens relevant to a
12479
12884
  limit
12480
12885
  });
12481
12886
  const tokens = [];
12887
+ const contextMap = /* @__PURE__ */ new Map();
12888
+ if (results.length > 0) {
12889
+ const ids = results.map((t2) => t2.id);
12890
+ const placeholders = ids.map(() => "?").join(",");
12891
+ const mappings = await db.prepare(
12892
+ `SELECT tc.token_id, c.name, c.label, c.language
12893
+ FROM token_contexts tc
12894
+ INNER JOIN contexts c ON c.id = tc.context_id
12895
+ WHERE tc.token_id IN (${placeholders})`
12896
+ ).all(...ids);
12897
+ for (const m of mappings) {
12898
+ const list = contextMap.get(m.token_id) ?? [];
12899
+ list.push({ name: m.name, label: m.label, language: m.language });
12900
+ contextMap.set(m.token_id, list);
12901
+ }
12902
+ }
12482
12903
  for (const t2 of results) {
12483
12904
  const card = await getCard(db, t2.id, userId);
12484
12905
  tokens.push({
12485
12906
  slug: t2.slug,
12907
+ title: t2.title,
12908
+ display_title: getDisplayTitle(t2),
12486
12909
  concept: t2.concept,
12487
12910
  domain: t2.domain,
12488
12911
  bloom_level: t2.bloom_level,
12489
12912
  score: t2.score,
12490
12913
  similarity: t2.similarity,
12914
+ knowledgeContexts: contextMap.get(t2.id) ?? [],
12491
12915
  card: card ? {
12492
12916
  state: card.state,
12493
12917
  due_at: card.due_at,
@@ -12545,7 +12969,8 @@ bridgeCommand.command("suggest-foundations").description("Propose existing token
12545
12969
  queryText = embeddingContentForToken({
12546
12970
  concept: data.concept,
12547
12971
  question: typeof data.question === "string" ? data.question : null,
12548
- domain: typeof data.domain === "string" ? data.domain : ""
12972
+ domain: typeof data.domain === "string" ? data.domain : "",
12973
+ title: typeof data.title === "string" ? data.title : null
12549
12974
  });
12550
12975
  if (data.bloom_level !== void 0) {
12551
12976
  if (typeof data.bloom_level !== "number" || !Number.isInteger(data.bloom_level) || data.bloom_level < 1 || data.bloom_level > 5) {
@@ -13777,12 +14202,20 @@ bridgeCommand.command("list-tokens").description(
13777
14202
  ).option(
13778
14203
  "--user <id>",
13779
14204
  "User ID (default: whoami) \u2014 when provided, includes personal card info"
13780
- ).option("--domain <domain>", "Filter by domain").action(async (opts) => {
14205
+ ).option("--domain <domain>", "Filter by exact domain").option(
14206
+ "--domain-prefix <prefix>",
14207
+ "Filter by domain prefix (e.g. company-team) \u2014 uses / separator for hierarchy"
14208
+ ).option("--knowledge-context <context>", "Filter by knowledge context").action(async (opts) => {
13781
14209
  await withDb2(async (db) => {
13782
14210
  const userId = opts.user ? await resolveUser(opts, db, { json: true }) : void 0;
14211
+ const listOpts = {};
14212
+ if (opts.domain) listOpts.domain = opts.domain;
14213
+ if (opts.domainPrefix) listOpts.domainPrefix = opts.domainPrefix;
14214
+ if (opts.knowledgeContext)
14215
+ listOpts.knowledgeContext = opts.knowledgeContext;
13783
14216
  const tokens = await listTokens(
13784
14217
  db,
13785
- opts.domain ? { domain: opts.domain } : void 0
14218
+ Object.keys(listOpts).length ? listOpts : void 0
13786
14219
  );
13787
14220
  const cardMap = /* @__PURE__ */ new Map();
13788
14221
  if (userId && tokens.length > 0) {
@@ -13794,14 +14227,33 @@ bridgeCommand.command("list-tokens").description(
13794
14227
  ).all(...ids, userId);
13795
14228
  for (const c of cards) cardMap.set(c.token_id, c);
13796
14229
  }
14230
+ const contextMap = /* @__PURE__ */ new Map();
14231
+ if (tokens.length > 0) {
14232
+ const ids = tokens.map((t2) => t2.id);
14233
+ const placeholders = ids.map(() => "?").join(",");
14234
+ const mappings = await db.prepare(
14235
+ `SELECT tc.token_id, c.name, c.label, c.language
14236
+ FROM token_contexts tc
14237
+ INNER JOIN contexts c ON c.id = tc.context_id
14238
+ WHERE tc.token_id IN (${placeholders})`
14239
+ ).all(...ids);
14240
+ for (const m of mappings) {
14241
+ const list = contextMap.get(m.token_id) ?? [];
14242
+ list.push({ name: m.name, label: m.label, language: m.language });
14243
+ contextMap.set(m.token_id, list);
14244
+ }
14245
+ }
13797
14246
  const out = tokens.map((t2) => {
13798
14247
  const c = cardMap.get(t2.id);
13799
14248
  return {
13800
14249
  id: t2.id,
13801
14250
  slug: t2.slug,
14251
+ title: t2.title,
14252
+ display_title: getDisplayTitle(t2),
13802
14253
  concept: t2.concept,
13803
14254
  domain: t2.domain,
13804
14255
  bloomLevel: t2.bloom_level,
14256
+ knowledgeContexts: contextMap.get(t2.id) ?? [],
13805
14257
  card: c ? {
13806
14258
  state: c.state,
13807
14259
  reps: c.reps,
@@ -13829,12 +14281,32 @@ bridgeCommand.command("get-neighborhood").description(
13829
14281
  jsonError(`Token not found: ${opts.focus}`);
13830
14282
  }
13831
14283
  const nb = await getTokenNeighborhood(db, token.id, userId);
14284
+ const allTokens = [nb.center, ...nb.prerequisites, ...nb.dependents];
14285
+ const contextMap = /* @__PURE__ */ new Map();
14286
+ if (allTokens.length > 0) {
14287
+ const ids = allTokens.map((t2) => t2.id);
14288
+ const placeholders = ids.map(() => "?").join(",");
14289
+ const mappings = await db.prepare(
14290
+ `SELECT tc.token_id, c.name, c.label, c.language
14291
+ FROM token_contexts tc
14292
+ INNER JOIN contexts c ON c.id = tc.context_id
14293
+ WHERE tc.token_id IN (${placeholders})`
14294
+ ).all(...ids);
14295
+ for (const m of mappings) {
14296
+ const list = contextMap.get(m.token_id) ?? [];
14297
+ list.push({ name: m.name, label: m.label, language: m.language });
14298
+ contextMap.set(m.token_id, list);
14299
+ }
14300
+ }
13832
14301
  const mapToken = (nt) => ({
13833
14302
  id: nt.id,
13834
14303
  slug: nt.slug,
14304
+ title: nt.title,
14305
+ display_title: getDisplayTitle(nt),
13835
14306
  concept: nt.concept,
13836
14307
  domain: nt.domain,
13837
14308
  bloomLevel: nt.bloom_level,
14309
+ knowledgeContexts: contextMap.get(nt.id) ?? [],
13838
14310
  card: nt.card ? {
13839
14311
  state: nt.card.state,
13840
14312
  reps: nt.card.reps,
@@ -13853,23 +14325,62 @@ bridgeCommand.command("get-neighborhood").description(
13853
14325
  });
13854
14326
  });
13855
14327
  });
13856
- bridgeCommand.command("personal-card-list").description("List and search personal learning cards (JSON)").option("--user <id>", "User ID (default: whoami)").option("--query <query>", "Text search query").option("--domain <domain>", "Filter by category/domain").action(async (opts) => {
14328
+ bridgeCommand.command("personal-card-list").description("List and search personal learning cards (JSON)").option("--user <id>", "User ID (default: whoami)").option("--query <query>", "Text search query").option("--domain <domain>", "Filter by category/domain").option("--knowledge-context <context>", "Filter by knowledge context").action(async (opts) => {
13857
14329
  await withDb2(async (db) => {
13858
14330
  const userId = await resolveUser(opts, db, { json: true });
13859
14331
  const cards = await listPersonalCards(db, userId, {
13860
14332
  query: opts.query,
13861
- domain: opts.domain
14333
+ domain: opts.domain,
14334
+ knowledgeContext: opts.knowledgeContext
14335
+ });
14336
+ const contextMap = /* @__PURE__ */ new Map();
14337
+ if (cards.length > 0) {
14338
+ const tokenIds = cards.map((card) => card.tokenId);
14339
+ const placeholders = tokenIds.map(() => "?").join(",");
14340
+ const mappings = await db.prepare(
14341
+ `SELECT tc.token_id, c.name, c.label, c.language
14342
+ FROM token_contexts tc
14343
+ INNER JOIN contexts c ON c.id = tc.context_id
14344
+ WHERE tc.token_id IN (${placeholders})
14345
+ ORDER BY c.name`
14346
+ ).all(...tokenIds);
14347
+ for (const mapping of mappings) {
14348
+ const contexts = contextMap.get(mapping.token_id) ?? [];
14349
+ contexts.push({
14350
+ name: mapping.name,
14351
+ label: mapping.label,
14352
+ language: mapping.language
14353
+ });
14354
+ contextMap.set(mapping.token_id, contexts);
14355
+ }
14356
+ }
14357
+ jsonOut2({
14358
+ cards: cards.map((card) => ({
14359
+ ...card,
14360
+ knowledgeContexts: contextMap.get(card.tokenId) ?? []
14361
+ }))
13862
14362
  });
13863
- jsonOut2({ cards });
13864
14363
  });
13865
14364
  });
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(
14365
+ 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
14366
  "--mode <mode>",
13868
14367
  "Symbiosis mode: shadowing | copilot | autonomy | none",
13869
14368
  "none"
13870
- ).option("--context <context>", "Context description", "").action(async (opts) => {
14369
+ ).option("--context <context>", "Context description", "").option(
14370
+ "--knowledge-context <context>",
14371
+ "Assign token to a knowledge context (repeatable)",
14372
+ (val, memo) => {
14373
+ memo.push(val);
14374
+ return memo;
14375
+ },
14376
+ []
14377
+ ).action(async (opts) => {
13871
14378
  await withDb2(async (db) => {
13872
14379
  const userId = await resolveUser(opts, db, { json: true });
14380
+ const contextNames = parseKnowledgeContextNames(
14381
+ opts.knowledgeContext || []
14382
+ );
14383
+ const contexts = await resolveKnowledgeContexts(db, contextNames);
13873
14384
  const bloom = Number(opts.bloom);
13874
14385
  if (bloom < 1 || bloom > 5) {
13875
14386
  jsonError("bloom must be between 1 and 5");
@@ -13891,6 +14402,7 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
13891
14402
  const { token, card } = await db.transaction(async (tx) => {
13892
14403
  const createdToken = await createToken(tx, {
13893
14404
  slug,
14405
+ title: opts.title,
13894
14406
  concept: opts.concept,
13895
14407
  domain: opts.domain,
13896
14408
  bloom_level: bloom,
@@ -13899,6 +14411,9 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
13899
14411
  source_link: opts.sourceLink || null,
13900
14412
  question
13901
14413
  });
14414
+ for (const context of contexts) {
14415
+ await assignTokenToContext(tx, createdToken.id, context.id);
14416
+ }
13902
14417
  const createdCard = await ensureCard(tx, createdToken.id, userId);
13903
14418
  return { token: createdToken, card: createdCard };
13904
14419
  });
@@ -13907,6 +14422,8 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
13907
14422
  token: {
13908
14423
  id: token.id,
13909
14424
  slug: token.slug,
14425
+ title: token.title,
14426
+ display_title: getDisplayTitle(token),
13910
14427
  concept: token.concept,
13911
14428
  domain: token.domain,
13912
14429
  bloomLevel: token.bloom_level,
@@ -13915,7 +14432,12 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
13915
14432
  sourceLink: token.source_link,
13916
14433
  question: token.question,
13917
14434
  createdAt: token.created_at,
13918
- updatedAt: token.updated_at
14435
+ updatedAt: token.updated_at,
14436
+ knowledgeContexts: contexts.map((context) => ({
14437
+ name: context.name,
14438
+ label: context.label,
14439
+ language: context.language
14440
+ }))
13919
14441
  },
13920
14442
  card: {
13921
14443
  id: card.id,
@@ -13928,13 +14450,14 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
13928
14450
  });
13929
14451
  });
13930
14452
  });
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(
14453
+ 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
14454
  "--mode <mode>",
13933
14455
  "Updated symbiosis mode: shadowing | copilot | autonomy | none"
13934
14456
  ).option("--source-link <link>", "Updated source link").option("--question <question>", "Updated question text").action(async (opts) => {
13935
14457
  await withDb2(async (db) => {
13936
14458
  const userId = await resolveUser(opts, db, { json: true });
13937
14459
  const updates = {};
14460
+ if (opts.title !== void 0) updates.title = opts.title;
13938
14461
  if (opts.concept !== void 0) updates.concept = opts.concept;
13939
14462
  if (opts.domain !== void 0) updates.domain = opts.domain;
13940
14463
  if (opts.bloom !== void 0) {
@@ -13976,6 +14499,8 @@ bridgeCommand.command("personal-card-update").description("Update the mutable to
13976
14499
  token: {
13977
14500
  id: token.id,
13978
14501
  slug: token.slug,
14502
+ title: token.title,
14503
+ display_title: getDisplayTitle(token),
13979
14504
  concept: token.concept,
13980
14505
  domain: token.domain,
13981
14506
  bloomLevel: token.bloom_level,
@@ -14008,7 +14533,12 @@ bridgeCommand.command("personal-card-remove").description(
14008
14533
  success: true,
14009
14534
  preview: true,
14010
14535
  requiresConfirmation: true,
14011
- token: { id: token.id, slug: token.slug },
14536
+ token: {
14537
+ id: token.id,
14538
+ slug: token.slug,
14539
+ title: token.title,
14540
+ display_title: getDisplayTitle(token)
14541
+ },
14012
14542
  impact
14013
14543
  });
14014
14544
  return;
@@ -14037,7 +14567,12 @@ bridgeCommand.command("personal-card-delete").description("Hard-delete a token a
14037
14567
  success: true,
14038
14568
  preview: true,
14039
14569
  requiresConfirmation: true,
14040
- token: { id: token.id, slug: token.slug },
14570
+ token: {
14571
+ id: token.id,
14572
+ slug: token.slug,
14573
+ title: token.title,
14574
+ display_title: getDisplayTitle(token)
14575
+ },
14041
14576
  impact
14042
14577
  });
14043
14578
  return;
@@ -14056,14 +14591,28 @@ bridgeCommand.command("personal-card-delete").description("Hard-delete a token a
14056
14591
  bridgeCommand.command("personal-card-import-curriculum").description("Parse curriculum text using LLM and import cards (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--text <text>", "Curriculum syllabus/content text").requiredOption(
14057
14592
  "--domain <domain>",
14058
14593
  "Default category/domain for imported cards"
14059
- ).option("--source <url>", "Provenance source link or URL").option("--preview", "Return parsed cards without saving them").action(async (opts) => {
14594
+ ).option("--source <url>", "Provenance source link or URL").option("--preview", "Return parsed cards without saving them").option(
14595
+ "--knowledge-context <context>",
14596
+ "Assign imported tokens to a knowledge context (repeatable)",
14597
+ (val, memo) => {
14598
+ memo.push(val);
14599
+ return memo;
14600
+ },
14601
+ []
14602
+ ).action(async (opts) => {
14060
14603
  await withDb2(async (db) => {
14061
14604
  const userId = await resolveUser(opts, db, { json: true });
14605
+ const contextNames = parseKnowledgeContextNames(
14606
+ opts.knowledgeContext || []
14607
+ );
14608
+ const contexts = await resolveKnowledgeContexts(db, contextNames);
14609
+ const firstContext = contexts[0]?.name;
14062
14610
  const cards = await importCurriculumViaLLM(
14063
14611
  db,
14064
14612
  opts.text,
14065
14613
  opts.domain,
14066
- opts.source || null
14614
+ opts.source || null,
14615
+ { knowledgeContext: firstContext }
14067
14616
  );
14068
14617
  if (opts.preview) {
14069
14618
  jsonOut2({
@@ -14073,6 +14622,24 @@ bridgeCommand.command("personal-card-import-curriculum").description("Parse curr
14073
14622
  return;
14074
14623
  }
14075
14624
  const result = await importCurriculumCards(db, userId, cards);
14625
+ for (const card of cards) {
14626
+ const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
14627
+ const cleanDomain = slugify(card.domain || "");
14628
+ const cleanBase = slugify(baseText);
14629
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
14630
+ if (baseSlug.length > 60) {
14631
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
14632
+ }
14633
+ if (!baseSlug) {
14634
+ baseSlug = "token";
14635
+ }
14636
+ const token = await getTokenBySlug(db, baseSlug);
14637
+ if (token) {
14638
+ for (const context of contexts) {
14639
+ await assignTokenToContext(db, token.id, context.id);
14640
+ }
14641
+ }
14642
+ }
14076
14643
  jsonOut2({
14077
14644
  success: true,
14078
14645
  createdCount: result.createdCount,
@@ -14211,7 +14778,7 @@ bridgeCommand.command("personal-source-import").description(
14211
14778
  } else {
14212
14779
  throw new Error(`Invalid source type: ${opts.type}`);
14213
14780
  }
14214
- const sourceId = ulid8();
14781
+ const sourceId = ulid9();
14215
14782
  await db.prepare(
14216
14783
  `INSERT INTO sources (id, type, uri, content)
14217
14784
  VALUES (?, ?, ?, ?)
@@ -14233,16 +14800,46 @@ bridgeCommand.command("personal-source-confirm-import").description(
14233
14800
  ).option("--user <id>", "User ID (default: whoami)").requiredOption("--sourceId <id>", "Source database ID").requiredOption(
14234
14801
  "--proposals <json>",
14235
14802
  "JSON string representing proposals array"
14803
+ ).option(
14804
+ "--knowledge-context <context>",
14805
+ "Assign confirmed tokens to a knowledge context (repeatable)",
14806
+ (val, memo) => {
14807
+ memo.push(val);
14808
+ return memo;
14809
+ },
14810
+ []
14236
14811
  ).action(async (opts) => {
14237
14812
  await withDb2(async (db) => {
14238
14813
  const userId = await resolveUser(opts, db, { json: true });
14239
14814
  const proposals = JSON.parse(opts.proposals);
14815
+ const contextNames = parseKnowledgeContextNames(
14816
+ opts.knowledgeContext || []
14817
+ );
14818
+ const contexts = await resolveKnowledgeContexts(db, contextNames);
14240
14819
  const result = await confirmSourceImport(
14241
14820
  db,
14242
14821
  userId,
14243
14822
  opts.sourceId,
14244
14823
  proposals
14245
14824
  );
14825
+ for (const p of proposals) {
14826
+ const baseText = p.question && p.question.trim().length > 0 ? p.question : p.concept;
14827
+ const cleanDomain = slugify(p.domain || "");
14828
+ const cleanBase = slugify(baseText);
14829
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
14830
+ if (baseSlug.length > 60) {
14831
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
14832
+ }
14833
+ if (!baseSlug) {
14834
+ baseSlug = "token";
14835
+ }
14836
+ const token = await getTokenBySlug(db, baseSlug);
14837
+ if (token) {
14838
+ for (const context of contexts) {
14839
+ await assignTokenToContext(db, token.id, context.id);
14840
+ }
14841
+ }
14842
+ }
14246
14843
  jsonOut2({
14247
14844
  success: true,
14248
14845
  createdCount: result.createdCount,
@@ -14400,7 +14997,7 @@ bridgeCommand.command("curriculum-extract-topics").description(
14400
14997
  }
14401
14998
  }
14402
14999
  const pageCleanedText = cleanHtml(rawHtml);
14403
- const sourceId = ulid8();
15000
+ const sourceId = ulid9();
14404
15001
  await db.prepare(
14405
15002
  `INSERT INTO sources (id, type, uri, content)
14406
15003
  VALUES (?, 'web', ?, ?)
@@ -14449,6 +15046,71 @@ bridgeCommand.command("curriculum-set-last-selection").description("Persist the
14449
15046
  jsonOut2({ success: true });
14450
15047
  });
14451
15048
  });
15049
+ bridgeCommand.command("list-knowledge-contexts").description("List all knowledge contexts (JSON)").action(async () => {
15050
+ await withDb2(async (db) => {
15051
+ const contexts = await listKnowledgeContexts(db);
15052
+ jsonOut2({ success: true, contexts });
15053
+ });
15054
+ });
15055
+ bridgeCommand.command("assign-knowledge-context").description("Assign a token to a knowledge context (JSON)").requiredOption("--token <slug>", "Token slug").requiredOption("--context <name>", "Context name").action(async (opts) => {
15056
+ await withDb2(async (db) => {
15057
+ const context = await getKnowledgeContextByName(db, opts.context);
15058
+ if (!context) {
15059
+ jsonError(`Knowledge context not found: ${opts.context}`);
15060
+ }
15061
+ const token = await getTokenBySlug(db, opts.token);
15062
+ if (!token) {
15063
+ jsonError(`Token not found: ${opts.token}`);
15064
+ }
15065
+ await assignTokenToContext(db, token.id, context.id);
15066
+ jsonOut2({ success: true, token: token.slug, context: context.name });
15067
+ });
15068
+ });
15069
+ bridgeCommand.command("unassign-knowledge-context").description("Remove a token from a knowledge context (JSON)").requiredOption("--token <slug>", "Token slug").requiredOption("--context <name>", "Context name").action(async (opts) => {
15070
+ await withDb2(async (db) => {
15071
+ const context = await getKnowledgeContextByName(db, opts.context);
15072
+ if (!context) {
15073
+ jsonError(`Knowledge context not found: ${opts.context}`);
15074
+ }
15075
+ const token = await getTokenBySlug(db, opts.token);
15076
+ if (!token) {
15077
+ jsonError(`Token not found: ${opts.token}`);
15078
+ }
15079
+ await unassignTokenFromContext(db, token.id, context.id);
15080
+ jsonOut2({ success: true, token: token.slug, context: context.name });
15081
+ });
15082
+ });
15083
+ bridgeCommand.command("get-active-knowledge-context").description("Get the active knowledge context name (JSON)").action(async () => {
15084
+ await withDb2(async (db) => {
15085
+ const configured = getActiveWorkspaceContext();
15086
+ const active = configured ? await getKnowledgeContextByName(db, configured) : void 0;
15087
+ jsonOut2({
15088
+ success: true,
15089
+ activeContext: active?.name ?? null,
15090
+ staleContext: configured && !active ? configured : null
15091
+ });
15092
+ });
15093
+ });
15094
+ bridgeCommand.command("set-active-knowledge-context").description("Set the active knowledge context name (JSON)").argument("[name]", "Context name to use (use empty/none to clear)").action(async (name) => {
15095
+ await withDb2(async (db) => {
15096
+ if (!name || name === "none" || name === "null" || name === "undefined") {
15097
+ if (!setActiveWorkspaceContext(void 0)) {
15098
+ jsonError("No active workspace configured");
15099
+ }
15100
+ jsonOut2({ success: true, activeContext: null });
15101
+ return;
15102
+ }
15103
+ const context = await getKnowledgeContextByName(db, name);
15104
+ if (!context) {
15105
+ jsonError(`Knowledge context not found: ${name}`);
15106
+ return;
15107
+ }
15108
+ if (!setActiveWorkspaceContext(context.name)) {
15109
+ jsonError("No active workspace configured");
15110
+ }
15111
+ jsonOut2({ success: true, activeContext: context.name });
15112
+ });
15113
+ });
14452
15114
  bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/stdout server").option("--stdin", "Use stdin/stdout for communication").action(async (_opts) => {
14453
15115
  isServeMode = true;
14454
15116
  const {
@@ -14583,13 +15245,19 @@ import { Command as Command3 } from "commander";
14583
15245
  var cardCommand = new Command3("card").description(
14584
15246
  "Manage spaced-repetition cards"
14585
15247
  );
14586
- cardCommand.command("due").description("Show due tokens for a user").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").option("--json", "Output as JSON").option("--summary", "Show only counts per domain (no slugs or concepts)").action(async (opts) => {
15248
+ cardCommand.command("due").description("Show due tokens for a user").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").option("--knowledge-context <context>", "Filter by knowledge context").option("--json", "Output as JSON").option("--summary", "Show only counts per domain (no slugs or concepts)").action(async (opts) => {
14587
15249
  await withDb(async (db) => {
14588
15250
  const userId = await resolveUser(opts, db);
14589
- const dueCards = await getDueCards(db, userId, void 0, opts.domain);
14590
- if (opts.json) {
14591
- console.log(JSON.stringify(dueCards, null, 2));
14592
- return;
15251
+ const dueCards = await getDueCards(
15252
+ db,
15253
+ userId,
15254
+ void 0,
15255
+ opts.domain,
15256
+ opts.knowledgeContext
15257
+ );
15258
+ if (opts.json) {
15259
+ console.log(JSON.stringify(dueCards, null, 2));
15260
+ return;
14593
15261
  }
14594
15262
  if (dueCards.length === 0) {
14595
15263
  console.log("No cards due for review.");
@@ -14929,11 +15597,921 @@ async function setupTurso(urlArg, tokenArg, mode) {
14929
15597
  }
14930
15598
  }
14931
15599
 
15600
+ // src/cli/commands/doctor.ts
15601
+ import { Command as Command5 } from "commander";
15602
+ function shouldApplyFixes(opts) {
15603
+ return opts.fix === true && opts.dryRun !== true;
15604
+ }
15605
+ function stringifyDoctorOutput(value) {
15606
+ if (typeof value === "string") return value;
15607
+ try {
15608
+ return JSON.stringify(value);
15609
+ } catch {
15610
+ return String(value);
15611
+ }
15612
+ }
15613
+ async function captureDoctorTaskOutput(task, db, opts) {
15614
+ const chunks = [];
15615
+ const append = (value) => {
15616
+ for (const line of value.split(/[\r\n]+/)) {
15617
+ const trimmed = line.trim();
15618
+ if (trimmed) chunks.push(trimmed);
15619
+ }
15620
+ };
15621
+ const capture = (...args) => {
15622
+ append(args.map(stringifyDoctorOutput).join(" "));
15623
+ };
15624
+ const originalLog = console.log;
15625
+ const originalWarn = console.warn;
15626
+ const originalError = console.error;
15627
+ const originalWrite = process.stdout.write;
15628
+ console.log = capture;
15629
+ console.warn = capture;
15630
+ console.error = capture;
15631
+ process.stdout.write = ((chunk) => {
15632
+ append(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString());
15633
+ return true;
15634
+ });
15635
+ try {
15636
+ await task.run(db, opts);
15637
+ return { lines: chunks };
15638
+ } catch (error) {
15639
+ return { lines: chunks, error: error.message };
15640
+ } finally {
15641
+ console.log = originalLog;
15642
+ console.warn = originalWarn;
15643
+ console.error = originalError;
15644
+ process.stdout.write = originalWrite;
15645
+ }
15646
+ }
15647
+ async function confirmDeterministicChanges(opts, message) {
15648
+ if (opts.yes) return true;
15649
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
15650
+ console.error(
15651
+ "Cannot request confirmation in a non-interactive terminal. Re-run with --yes to apply deterministic fixes."
15652
+ );
15653
+ return false;
15654
+ }
15655
+ const { confirm: confirm5 } = await import("@inquirer/prompts");
15656
+ return confirm5({ message, default: false });
15657
+ }
15658
+ function canRunInteractiveChoices(opts, taskName) {
15659
+ if (opts.yes) {
15660
+ console.error(
15661
+ `--yes cannot choose how to resolve ${taskName}. Re-run without --yes in an interactive terminal.`
15662
+ );
15663
+ return false;
15664
+ }
15665
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
15666
+ console.error(
15667
+ `${taskName} fixes require an interactive terminal because each change needs an explicit choice.`
15668
+ );
15669
+ return false;
15670
+ }
15671
+ return true;
15672
+ }
15673
+ function getWeakTitleReason(t2) {
15674
+ const title = (t2.title || "").trim();
15675
+ if (!title) {
15676
+ return "Missing or empty title";
15677
+ }
15678
+ if (title === t2.slug) {
15679
+ return "Title is equal to technical slug";
15680
+ }
15681
+ if (title.length < 3) {
15682
+ return "Title is suspiciously short";
15683
+ }
15684
+ if (title.length > 100) {
15685
+ return "Title is too long (> 100 chars)";
15686
+ }
15687
+ if (t2.domain) {
15688
+ const domParts = t2.domain.split(/[-/]/).filter((p) => p.length > 2);
15689
+ const titleLower2 = title.toLowerCase();
15690
+ for (const part of domParts) {
15691
+ if (titleLower2.includes(part.toLowerCase())) {
15692
+ return `Domain echo (contains '${part}' from domain '${t2.domain}')`;
15693
+ }
15694
+ }
15695
+ }
15696
+ const conceptClean = (t2.concept || "").trim().toLowerCase();
15697
+ const titleLower = title.toLowerCase();
15698
+ if (conceptClean.startsWith(titleLower) && t2.concept.length > title.length + 5 && !/[.!?]/.test(title)) {
15699
+ const titleWords = titleLower.split(/\s+/).length;
15700
+ if (titleWords <= 5) {
15701
+ return "Concept-prefix copy";
15702
+ }
15703
+ }
15704
+ if (title.endsWith("?") || /^(what|how|why|who|where|when|which|is|are|was|were|can|do|does|did|ueber|fuer|nahe)\b/i.test(
15705
+ title
15706
+ )) {
15707
+ return "Question stump or interrogative structure";
15708
+ }
15709
+ if (title.length > 3 && /[a-zA-Z]/.test(title)) {
15710
+ if (title === title.toUpperCase()) {
15711
+ return "All uppercase";
15712
+ }
15713
+ if (title === title.toLowerCase()) {
15714
+ return "All lowercase";
15715
+ }
15716
+ }
15717
+ return null;
15718
+ }
15719
+ function stripLeadingDomainWords(value, domain) {
15720
+ let result = value.trim();
15721
+ for (const word of domain.split(/[-/]/).filter(Boolean)) {
15722
+ const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15723
+ result = result.replace(new RegExp(`^${escaped}\\s+`, "i"), "");
15724
+ }
15725
+ return result.trim();
15726
+ }
15727
+ function truncateTitle(value) {
15728
+ const trimmed = value.trim().replace(/^["']+|["']+$/g, "");
15729
+ return trimmed.length <= 80 ? trimmed : `${trimmed.substring(0, 77).trimEnd()}...`;
15730
+ }
15731
+ function titleFromSlug(token) {
15732
+ return getShortSlug(token.slug, token.domain).split(/[-_]+/).filter(Boolean).map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join(" ");
15733
+ }
15734
+ function validatedTitleProposal(token, raw) {
15735
+ const candidates = [
15736
+ raw,
15737
+ (token.concept || "").split(/[.;]/)[0] || "",
15738
+ titleFromSlug(token)
15739
+ ];
15740
+ for (const candidate of candidates) {
15741
+ const normalized = truncateTitle(
15742
+ stripLeadingDomainWords(candidate, token.domain || "")
15743
+ );
15744
+ if (normalized.length > 0 && getWeakTitleReason({ ...token, title: normalized }) === null) {
15745
+ return normalized;
15746
+ }
15747
+ }
15748
+ return null;
15749
+ }
15750
+ var LANGUAGE_MARKERS = {
15751
+ de: ["der", "die", "das", "und", "ist", "sind", "f\xFCr", "mit", "nicht"],
15752
+ en: ["the", "and", "is", "are", "for", "with", "not", "from", "that"],
15753
+ es: ["el", "la", "los", "las", "y", "es", "son", "para", "con"],
15754
+ fr: ["le", "la", "les", "et", "est", "sont", "pour", "avec"],
15755
+ pt: ["o", "a", "os", "as", "e", "\xE9", "s\xE3o", "para", "com"]
15756
+ };
15757
+ function inferEstablishedLanguage(token) {
15758
+ const text = [token.title, token.concept, token.question, token.context].filter(Boolean).join(" ").toLowerCase();
15759
+ if (/[぀-ヿ]/u.test(text)) return "ja";
15760
+ if (/[㐀-鿿]/u.test(text)) return "zh";
15761
+ const words = new Set(text.match(/\p{L}+/gu) ?? []);
15762
+ const scores = Object.entries(LANGUAGE_MARKERS).map(([language, markers]) => ({
15763
+ language,
15764
+ score: markers.filter((marker) => words.has(marker)).length
15765
+ })).sort((a, b) => b.score - a.score);
15766
+ return scores[0].score >= 2 && scores[0].score > scores[1].score ? scores[0].language : null;
15767
+ }
15768
+ function primaryLanguage(language) {
15769
+ return language?.trim().toLowerCase().split(/[-_]/)[0] || null;
15770
+ }
15771
+ var doctorTasks = [
15772
+ {
15773
+ name: "titles",
15774
+ description: "Backfill missing/weak human-friendly titles (no domain echoes, descriptive names).",
15775
+ run: async (db, opts) => {
15776
+ const tokens = await listTokens(db, {});
15777
+ const needing = tokens.map((t2) => {
15778
+ const reason = getWeakTitleReason(t2);
15779
+ return reason ? { token: t2, reason } : null;
15780
+ }).filter((x) => x !== null);
15781
+ console.log(
15782
+ `Found ${needing.length} tokens needing title work (of ${tokens.length}).`
15783
+ );
15784
+ if (needing.length === 0) return;
15785
+ const cfg = await getLlmConfig(db);
15786
+ const useLLM = cfg.enabled && !opts.noLlm;
15787
+ console.log(
15788
+ `Title generation mode: ${useLLM ? "LLM" : "Heuristic fallback"}${opts.timeoutMs ? ` (timeout: ${opts.timeoutMs}ms)` : ""}`
15789
+ );
15790
+ console.log("Generating proposed titles...");
15791
+ const proposals = [];
15792
+ let rejected = 0;
15793
+ for (let i = 0; i < needing.length; i++) {
15794
+ const item = needing[i];
15795
+ if (!opts.json) {
15796
+ process.stdout.write(
15797
+ `\r [${i + 1}/${needing.length}] ${item.token.slug.slice(0, 40)}...`
15798
+ );
15799
+ }
15800
+ let rawProposal = "";
15801
+ if (useLLM) {
15802
+ try {
15803
+ const gen = await generateTitleViaLLM(
15804
+ db,
15805
+ {
15806
+ slug: item.token.slug,
15807
+ concept: item.token.concept,
15808
+ domain: item.token.domain,
15809
+ question: item.token.question,
15810
+ context: item.token.context
15811
+ },
15812
+ {
15813
+ timeoutMs: opts.timeoutMs,
15814
+ knowledgeContext: opts.knowledgeContext
15815
+ }
15816
+ );
15817
+ rawProposal = gen.text;
15818
+ } catch (_e) {
15819
+ rawProposal = (item.token.concept || "").split(/[.;]/)[0]?.trim() || item.token.slug.replace(/-/g, " ");
15820
+ }
15821
+ } else {
15822
+ rawProposal = (item.token.concept || "").split(/[.;]/)[0]?.trim() || item.token.slug.replace(/-/g, " ");
15823
+ }
15824
+ const proposed = validatedTitleProposal(item.token, rawProposal);
15825
+ if (!proposed) {
15826
+ rejected++;
15827
+ continue;
15828
+ }
15829
+ proposals.push({
15830
+ token: item.token,
15831
+ oldTitle: item.token.title,
15832
+ newTitle: proposed,
15833
+ reason: item.reason
15834
+ });
15835
+ }
15836
+ if (!opts.json) process.stdout.write("\n");
15837
+ if (rejected > 0) {
15838
+ console.warn(
15839
+ `Skipped ${rejected} token(s) because no proposal passed title quality validation.`
15840
+ );
15841
+ }
15842
+ if (proposals.length === 0) return;
15843
+ if (!shouldApplyFixes(opts)) {
15844
+ console.log("\nProposed changes (Dry run):");
15845
+ for (const prop of proposals) {
15846
+ console.log(
15847
+ ` ${prop.token.slug}: "${prop.oldTitle}" -> "${prop.newTitle}" (Reason: ${prop.reason})`
15848
+ );
15849
+ }
15850
+ console.log("\nRun with --fix to apply these changes.");
15851
+ return;
15852
+ }
15853
+ if (!opts.yes) {
15854
+ console.log("\nProposed changes:");
15855
+ for (const prop of proposals) {
15856
+ console.log(
15857
+ ` ${prop.token.slug}: "${prop.oldTitle}" -> "${prop.newTitle}" (Reason: ${prop.reason})`
15858
+ );
15859
+ }
15860
+ }
15861
+ const confirmed = await confirmDeterministicChanges(
15862
+ opts,
15863
+ `Apply these ${proposals.length} proposed titles?`
15864
+ );
15865
+ if (!confirmed) {
15866
+ console.log("Cancelled. No titles were updated.");
15867
+ return;
15868
+ }
15869
+ let fixed = 0;
15870
+ for (const prop of proposals) {
15871
+ await updateToken(db, prop.token.slug, { title: prop.newTitle });
15872
+ fixed++;
15873
+ console.log(`Set title for ${prop.token.slug} -> ${prop.newTitle}`);
15874
+ }
15875
+ console.log(`Successfully fixed ${fixed} titles.`);
15876
+ }
15877
+ },
15878
+ {
15879
+ name: "texts",
15880
+ description: "Repair legacy ASCII umlauts in prose fields (question/concept/context).",
15881
+ run: async (db, opts) => {
15882
+ const tokens = await listTokens(db, {});
15883
+ const cfg = await getLlmConfig(db);
15884
+ const useLLM = cfg.enabled && !opts.noLlm;
15885
+ const replacements = [
15886
+ [/\bUeber\b/g, "\xDCber"],
15887
+ [/\bueber\b/g, "\xFCber"],
15888
+ [/\bFuer\b/g, "F\xFCr"],
15889
+ [/\bfuer\b/g, "f\xFCr"],
15890
+ [/\bMuehe\b/g, "M\xFChe"],
15891
+ [/\bmuehe\b/g, "m\xFChe"],
15892
+ [/\bKuer\b/g, "K\xFCr"],
15893
+ [/\bkuer\b/g, "k\xFCr"],
15894
+ [/\bUebung\b/g, "\xDCbung"],
15895
+ [/\buebung\b/g, "\xFCbung"],
15896
+ [/\bMoeglich\b/g, "M\xF6glich"],
15897
+ [/\bmoeglich\b/g, "m\xF6glich"],
15898
+ [/\bMoeglichkeiten\b/g, "M\xF6glichkeiten"],
15899
+ [/\bmoeglichkeiten\b/g, "m\xF6glichkeiten"],
15900
+ [/\bSchoen\b/g, "Sch\xF6n"],
15901
+ [/\bschoen\b/g, "sch\xF6n"],
15902
+ [/\bKoennen\b/g, "K\xF6nnen"],
15903
+ [/\bkoennen\b/g, "k\xF6nnen"],
15904
+ [/\bMuessen\b/g, "M\xFCssen"],
15905
+ [/\bmuessen\b/g, "m\xFCssen"],
15906
+ [/\bDuerfen\b/g, "D\xFCrfen"],
15907
+ [/\bduerfen\b/g, "d\xFCrfen"],
15908
+ [/\bAerger\b/g, "\xC4rger"],
15909
+ [/\baerger\b/g, "\xE4rger"],
15910
+ [/\bGruende\b/g, "Gr\xFCnde"],
15911
+ [/\bgruende\b/g, "gr\xFCnde"]
15912
+ ];
15913
+ const toFix = [];
15914
+ console.log("Analyzing prose text fields for legacy umlauts...");
15915
+ for (const t2 of tokens) {
15916
+ const fields = [
15917
+ { key: "question", val: t2.question },
15918
+ { key: "concept", val: t2.concept },
15919
+ { key: "context", val: t2.context },
15920
+ { key: "title", val: t2.title }
15921
+ ];
15922
+ for (const f of fields) {
15923
+ if (!f.val || typeof f.val !== "string") continue;
15924
+ let repaired = f.val;
15925
+ if (useLLM) {
15926
+ const hasPossibleUmlautFold = /[aou]e/i.test(f.val);
15927
+ if (hasPossibleUmlautFold) {
15928
+ try {
15929
+ repaired = await repairUmlautsViaLLM(
15930
+ db,
15931
+ { text: f.val },
15932
+ { timeoutMs: opts.timeoutMs }
15933
+ );
15934
+ if (repaired === f.val) {
15935
+ for (const [from, to] of replacements) {
15936
+ repaired = repaired.replace(from, to);
15937
+ }
15938
+ }
15939
+ } catch (_e) {
15940
+ for (const [from, to] of replacements) {
15941
+ repaired = repaired.replace(from, to);
15942
+ }
15943
+ }
15944
+ }
15945
+ } else {
15946
+ for (const [from, to] of replacements) {
15947
+ repaired = repaired.replace(from, to);
15948
+ }
15949
+ }
15950
+ if (repaired !== f.val) {
15951
+ toFix.push({ token: t2, field: f.key, old: f.val, new: repaired });
15952
+ }
15953
+ }
15954
+ }
15955
+ console.log(`Found ${toFix.length} prose fields with legacy umlauts.`);
15956
+ if (toFix.length === 0) return;
15957
+ if (!shouldApplyFixes(opts)) {
15958
+ console.log("\nProposed changes (Dry run):");
15959
+ for (const fix of toFix) {
15960
+ console.log(` ${fix.token.slug} [${fix.field}]:`);
15961
+ console.log(` Old: "${fix.old}"`);
15962
+ console.log(` New: "${fix.new}"`);
15963
+ }
15964
+ console.log("\nRun with --fix to apply these changes.");
15965
+ return;
15966
+ }
15967
+ if (!opts.yes) {
15968
+ console.log("\nProposed changes:");
15969
+ for (const fix of toFix) {
15970
+ console.log(
15971
+ ` ${fix.token.slug} [${fix.field}]: "${fix.old}" -> "${fix.new}"`
15972
+ );
15973
+ }
15974
+ }
15975
+ const confirmed = await confirmDeterministicChanges(
15976
+ opts,
15977
+ `Apply these ${toFix.length} prose fixes?`
15978
+ );
15979
+ if (!confirmed) {
15980
+ console.log("Cancelled. No changes made.");
15981
+ return;
15982
+ }
15983
+ let fixed = 0;
15984
+ for (const fix of toFix) {
15985
+ const updates = {};
15986
+ updates[fix.field] = fix.new;
15987
+ await updateToken(db, fix.token.slug, updates);
15988
+ fixed++;
15989
+ }
15990
+ console.log(
15991
+ `Fixed ${fixed} fields. (Re-embed will happen automatically for changed content.)`
15992
+ );
15993
+ }
15994
+ },
15995
+ {
15996
+ name: "duplicates",
15997
+ description: "List semantic duplicates for review/merge.",
15998
+ run: async (db, opts) => {
15999
+ const applyingFixes = shouldApplyFixes(opts);
16000
+ if (applyingFixes && !canRunInteractiveChoices(opts, "duplicate fixes")) {
16001
+ return;
16002
+ }
16003
+ const modelSetting = await getSetting(db, "llm.embedding.model");
16004
+ if (!modelSetting) {
16005
+ console.log(
16006
+ "No embedding model configured. Please configure an embedding model to scan duplicates."
16007
+ );
16008
+ return;
16009
+ }
16010
+ const model = canonicalEmbeddingModelId(modelSetting);
16011
+ if (model !== modelSetting.trim().toLowerCase()) {
16012
+ console.log(
16013
+ `Using canonical embedding model "${model}" for configured alias "${modelSetting}".`
16014
+ );
16015
+ }
16016
+ let coverage = await getEmbeddingCoverage(db, model);
16017
+ const initiallyIncomplete = coverage.missing + coverage.stale;
16018
+ if (initiallyIncomplete > 0) {
16019
+ if (applyingFixes) {
16020
+ console.log(
16021
+ `Embedding coverage is incomplete (${coverage.missing} missing, ${coverage.stale} stale). Attempting backfill...`
16022
+ );
16023
+ const result = await ensureTokenEmbeddings(db, {
16024
+ limit: coverage.tokens
16025
+ });
16026
+ coverage = await getEmbeddingCoverage(db, model);
16027
+ const remaining = coverage.missing + coverage.stale;
16028
+ if (remaining > 0) {
16029
+ console.warn(
16030
+ `Warning: Duplicate scan is incomplete: ${remaining} of ${coverage.tokens} active tokens still lack a fresh "${model}" embedding.${result.reason ? ` ${result.reason}` : ""}`
16031
+ );
16032
+ } else {
16033
+ console.log(
16034
+ `Embedding backfill complete (${result.embedded} updated).`
16035
+ );
16036
+ }
16037
+ } else {
16038
+ console.warn(
16039
+ `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.`
16040
+ );
16041
+ }
16042
+ }
16043
+ console.log(`Listing embedded tokens for model "${model}"...`);
16044
+ const embedded = await listEmbeddedTokens(db, model);
16045
+ const threshold = await resolveDedupThreshold(db);
16046
+ console.log(
16047
+ `Scanning ${embedded.length} tokens for semantic duplicates (threshold: ${threshold})...`
16048
+ );
16049
+ const duplicates = [];
16050
+ const seenPairs = /* @__PURE__ */ new Set();
16051
+ for (let i = 0; i < embedded.length; i++) {
16052
+ for (let j = i + 1; j < embedded.length; j++) {
16053
+ const sim = cosineSimilarity(
16054
+ embedded[i].embedding,
16055
+ embedded[j].embedding
16056
+ );
16057
+ if (sim >= threshold) {
16058
+ const idA = embedded[i].token.id;
16059
+ const idB = embedded[j].token.id;
16060
+ const pairKey = idA < idB ? `${idA}:${idB}` : `${idB}:${idA}`;
16061
+ if (!seenPairs.has(pairKey)) {
16062
+ seenPairs.add(pairKey);
16063
+ duplicates.push({
16064
+ a: embedded[i].token,
16065
+ b: embedded[j].token,
16066
+ similarity: sim
16067
+ });
16068
+ }
16069
+ }
16070
+ }
16071
+ }
16072
+ console.log(`Found ${duplicates.length} duplicate pairs.`);
16073
+ if (duplicates.length === 0) return;
16074
+ if (!applyingFixes) {
16075
+ console.log("\nDuplicate pairs found (Dry run):");
16076
+ for (const pair of duplicates) {
16077
+ console.log(
16078
+ ` - "${pair.a.slug}" and "${pair.b.slug}" (similarity: ${pair.similarity.toFixed(3)})`
16079
+ );
16080
+ }
16081
+ console.log("\nRun with --fix to resolve duplicates interactively.");
16082
+ return;
16083
+ }
16084
+ const { select: select3 } = await import("@inquirer/prompts");
16085
+ let deprecatedCount = 0;
16086
+ for (const pair of duplicates) {
16087
+ console.log(
16088
+ `
16089
+ Duplicate pair found (similarity: ${pair.similarity.toFixed(3)}):`
16090
+ );
16091
+ console.log(
16092
+ ` 1. ${pair.a.slug}: "${pair.a.concept.substring(0, 100)}..."`
16093
+ );
16094
+ console.log(
16095
+ ` 2. ${pair.b.slug}: "${pair.b.concept.substring(0, 100)}..."`
16096
+ );
16097
+ const choice = await select3({
16098
+ message: "Select action:",
16099
+ choices: [
16100
+ { name: "Keep both", value: "keep" },
16101
+ { name: `Deprecate ${pair.a.slug}`, value: "deprecate_a" },
16102
+ { name: `Deprecate ${pair.b.slug}`, value: "deprecate_b" }
16103
+ ]
16104
+ });
16105
+ if (choice === "deprecate_a") {
16106
+ await deprecateToken(db, pair.a.slug);
16107
+ console.log(` Deprecated ${pair.a.slug}`);
16108
+ deprecatedCount++;
16109
+ } else if (choice === "deprecate_b") {
16110
+ await deprecateToken(db, pair.b.slug);
16111
+ console.log(` Deprecated ${pair.b.slug}`);
16112
+ deprecatedCount++;
16113
+ }
16114
+ }
16115
+ console.log(
16116
+ `
16117
+ Duplicates scan complete. Deprecated ${deprecatedCount} token(s).`
16118
+ );
16119
+ }
16120
+ },
16121
+ {
16122
+ name: "domains",
16123
+ description: "Help rename/unify domains into / hierarchy (slugs untouched).",
16124
+ run: async (db, opts) => {
16125
+ const tokens = await listTokens(db, {});
16126
+ const domains = Array.from(
16127
+ new Set(tokens.map((t2) => t2.domain).filter(Boolean))
16128
+ ).sort();
16129
+ console.log(`Current domains (${domains.length}):`);
16130
+ for (const d of domains) {
16131
+ const count2 = tokens.filter((t2) => t2.domain === d).length;
16132
+ const status = d.includes("/") ? "(hierarchical)" : "(flat, valid)";
16133
+ console.log(` ${d} \u2014 ${count2} token(s) ${status}`);
16134
+ }
16135
+ if (!shouldApplyFixes(opts)) {
16136
+ console.log("\nUse --fix to interactively rename/unify domains.");
16137
+ return;
16138
+ }
16139
+ if (!canRunInteractiveChoices(opts, "domain renames")) return;
16140
+ const { confirm: confirm5, input: input8 } = await import("@inquirer/prompts");
16141
+ const changes = [];
16142
+ for (const d of domains) {
16143
+ const count2 = tokens.filter((t2) => t2.domain === d).length;
16144
+ const wantRename = await confirm5({
16145
+ message: `Rename domain "${d}" (${count2} tokens)?`,
16146
+ default: false
16147
+ });
16148
+ if (wantRename) {
16149
+ const newDomain = await input8({
16150
+ message: `Enter new domain name for "${d}":`,
16151
+ validate: (val) => val.trim().length > 0 ? true : "Domain name cannot be empty"
16152
+ });
16153
+ const trimmed = newDomain.trim();
16154
+ if (trimmed === d) {
16155
+ console.log(" New name matches old name. Skipped.");
16156
+ continue;
16157
+ }
16158
+ changes.push({ oldDomain: d, newDomain: trimmed });
16159
+ }
16160
+ }
16161
+ if (changes.length > 0) {
16162
+ await db.transaction(async (tx) => {
16163
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16164
+ const stmt = tx.prepare(
16165
+ "UPDATE tokens SET domain = ?, updated_at = ? WHERE domain = ?"
16166
+ );
16167
+ for (const change of changes) {
16168
+ const res = await stmt.run(change.newDomain, now, change.oldDomain);
16169
+ console.log(
16170
+ ` Renamed "${change.oldDomain}" to "${change.newDomain}" for ${res.changes} token(s).`
16171
+ );
16172
+ }
16173
+ });
16174
+ }
16175
+ console.log(
16176
+ `Domains rename complete. Renamed ${changes.length} domain(s).`
16177
+ );
16178
+ }
16179
+ },
16180
+ {
16181
+ name: "contexts",
16182
+ description: "Backfill contexts on old/unassigned tokens using heuristic rules.",
16183
+ run: async (db, opts) => {
16184
+ const tokensWithoutContext = await db.prepare(
16185
+ `SELECT t.* FROM tokens t
16186
+ WHERE NOT EXISTS (
16187
+ SELECT 1 FROM token_contexts tc WHERE tc.token_id = t.id
16188
+ )`
16189
+ ).all();
16190
+ const allContexts = await listKnowledgeContexts(db);
16191
+ const contexts = opts.knowledgeContext ? allContexts.filter(
16192
+ (context) => context.name === opts.knowledgeContext
16193
+ ) : allContexts;
16194
+ if (opts.knowledgeContext && contexts.length === 0) {
16195
+ throw new Error(
16196
+ `Knowledge context not found: ${opts.knowledgeContext}`
16197
+ );
16198
+ }
16199
+ if (contexts.length === 0) {
16200
+ if (opts.json) {
16201
+ console.log(
16202
+ JSON.stringify({
16203
+ success: false,
16204
+ error: "No knowledge contexts defined."
16205
+ })
16206
+ );
16207
+ } else {
16208
+ console.log(
16209
+ "No knowledge contexts defined. Run `zam kc create` first."
16210
+ );
16211
+ }
16212
+ return;
16213
+ }
16214
+ const proposals = [];
16215
+ for (const t2 of tokensWithoutContext) {
16216
+ const domain = (t2.domain || "").toLowerCase();
16217
+ const slug = t2.slug.toLowerCase();
16218
+ const title = (t2.title || "").toLowerCase();
16219
+ const source = (t2.source_link || "").toLowerCase();
16220
+ const establishedLanguage = inferEstablishedLanguage(t2);
16221
+ const languageMatches = establishedLanguage ? contexts.filter(
16222
+ (context) => primaryLanguage(context.language) === establishedLanguage
16223
+ ) : [];
16224
+ for (const ctx of contexts) {
16225
+ const ctxName = ctx.name.toLowerCase();
16226
+ const ctxLabel = (ctx.label || "").toLowerCase();
16227
+ let match = false;
16228
+ let highConfidence = false;
16229
+ if (domain) {
16230
+ if (domain === ctxName) {
16231
+ match = true;
16232
+ highConfidence = true;
16233
+ } else if (ctxName.includes(domain) || domain.includes(ctxName)) {
16234
+ match = true;
16235
+ highConfidence = domain.length >= 2;
16236
+ } else if (ctxLabel && (ctxLabel.includes(domain) || domain.includes(ctxLabel))) {
16237
+ match = true;
16238
+ highConfidence = domain.length >= 2;
16239
+ }
16240
+ }
16241
+ if (slug.includes(ctxName) || title.includes(ctxName)) {
16242
+ match = true;
16243
+ }
16244
+ if (ctxLabel) {
16245
+ const labelWords = ctxLabel.split(/\s+/).filter((w) => w.length > 3);
16246
+ for (const word of labelWords) {
16247
+ if (domain.includes(word) || slug.includes(word) || title.includes(word)) {
16248
+ match = true;
16249
+ }
16250
+ }
16251
+ }
16252
+ if (source && (source.includes(ctxName) || ctxLabel.split(/\s+/).filter((word) => word.length > 3).some((word) => source.includes(word)))) {
16253
+ match = true;
16254
+ highConfidence = true;
16255
+ }
16256
+ if (languageMatches.length === 1 && languageMatches[0].id === ctx.id) {
16257
+ match = true;
16258
+ }
16259
+ if (match) {
16260
+ proposals.push({
16261
+ token: t2,
16262
+ context: ctx,
16263
+ highConfidence
16264
+ });
16265
+ }
16266
+ }
16267
+ }
16268
+ const applying = shouldApplyFixes(opts);
16269
+ if (opts.json) {
16270
+ if (!applying) {
16271
+ const formatted = proposals.map((p) => ({
16272
+ tokenSlug: p.token.slug,
16273
+ contextName: p.context.name,
16274
+ highConfidence: p.highConfidence
16275
+ }));
16276
+ console.log(
16277
+ JSON.stringify({
16278
+ success: true,
16279
+ task: "contexts",
16280
+ proposals: formatted,
16281
+ totalUnassigned: tokensWithoutContext.length
16282
+ })
16283
+ );
16284
+ return;
16285
+ }
16286
+ } else {
16287
+ console.log(
16288
+ `Found ${proposals.length} proposed context assignments for ${tokensWithoutContext.length} unassigned tokens.`
16289
+ );
16290
+ }
16291
+ if (proposals.length === 0) {
16292
+ if (opts.json && applying) {
16293
+ console.log(
16294
+ JSON.stringify({
16295
+ success: true,
16296
+ task: "contexts",
16297
+ applied: [],
16298
+ totalApplied: 0
16299
+ })
16300
+ );
16301
+ }
16302
+ return;
16303
+ }
16304
+ if (!applying) {
16305
+ console.log("\nProposed context assignments (Dry run):");
16306
+ for (const prop of proposals) {
16307
+ const confStr = prop.highConfidence ? " [High Confidence]" : "";
16308
+ console.log(
16309
+ ` - Assign token "${prop.token.slug}" to context "${prop.context.name}"${confStr}`
16310
+ );
16311
+ }
16312
+ console.log("\nRun with --fix to apply these assignments.");
16313
+ return;
16314
+ }
16315
+ if (opts.yes) {
16316
+ let applied = 0;
16317
+ const appliedProposals = [];
16318
+ for (const prop of proposals) {
16319
+ if (prop.highConfidence) {
16320
+ await assignTokenToContext(db, prop.token.id, prop.context.id);
16321
+ if (!opts.json) {
16322
+ console.log(
16323
+ `Auto-assigned: "${prop.token.slug}" -> context "${prop.context.name}"`
16324
+ );
16325
+ }
16326
+ appliedProposals.push({
16327
+ tokenSlug: prop.token.slug,
16328
+ contextName: prop.context.name
16329
+ });
16330
+ applied++;
16331
+ }
16332
+ }
16333
+ if (opts.json) {
16334
+ console.log(
16335
+ JSON.stringify({
16336
+ success: true,
16337
+ task: "contexts",
16338
+ applied: appliedProposals,
16339
+ totalApplied: applied
16340
+ })
16341
+ );
16342
+ } else {
16343
+ console.log(`Auto-assigned ${applied} high-confidence contexts.`);
16344
+ }
16345
+ return;
16346
+ }
16347
+ const { confirm: confirm5 } = await import("@inquirer/prompts");
16348
+ let assignedCount = 0;
16349
+ for (const prop of proposals) {
16350
+ const confStr = prop.highConfidence ? " (high confidence)" : "";
16351
+ const ans = await confirm5({
16352
+ message: `Assign token "${prop.token.slug}" to context "${prop.context.name}"${confStr}?`,
16353
+ default: prop.highConfidence
16354
+ });
16355
+ if (ans) {
16356
+ await assignTokenToContext(db, prop.token.id, prop.context.id);
16357
+ console.log(
16358
+ `Assigned "${prop.token.slug}" -> context "${prop.context.name}"`
16359
+ );
16360
+ assignedCount++;
16361
+ }
16362
+ }
16363
+ console.log(
16364
+ `Completed contexts backfill. Assigned ${assignedCount} tokens.`
16365
+ );
16366
+ }
16367
+ }
16368
+ ];
16369
+ function parseDoctorTimeout(value) {
16370
+ const parsed = Number(value);
16371
+ if (!Number.isInteger(parsed) || parsed <= 0) {
16372
+ throw new Error("--timeout must be a positive integer in milliseconds");
16373
+ }
16374
+ return parsed;
16375
+ }
16376
+ var doctorCommand = new Command5("doctor").description(
16377
+ "Diagnose and repair the knowledge base (titles, legacy data, duplicates, domains)."
16378
+ ).option("--fix", "Apply changes (default is dry-run)").option("--dry-run", "Explicit dry run (default)").option(
16379
+ "--yes",
16380
+ "Auto-confirm deterministic title/text fixes (duplicate/domain choices remain interactive)"
16381
+ ).option("--no-llm", "Skip LLM calls, use heuristic fallback only").option(
16382
+ "--timeout <ms>",
16383
+ "LLM timeout in ms per call (default: 20000)",
16384
+ "20000"
16385
+ ).option(
16386
+ "--knowledge-context <context>",
16387
+ "Knowledge context to limit/guide doctor task"
16388
+ ).option("--json", "Emit report in JSON format").argument(
16389
+ "[task]",
16390
+ "Specific task: titles, texts, duplicates, domains, contexts"
16391
+ ).action(async (taskName, opts) => {
16392
+ await withDb(async (db) => {
16393
+ const fix = !!opts.fix;
16394
+ const dryRun = !fix || !!opts.dryRun;
16395
+ const yes = !!opts.yes;
16396
+ const noLlm = opts.llm === false;
16397
+ const timeoutMs = parseDoctorTimeout(opts.timeout);
16398
+ const knowledgeContext = opts.knowledgeContext;
16399
+ const json = !!opts.json;
16400
+ if (!taskName) {
16401
+ const diagnosisOptions = {
16402
+ fix: false,
16403
+ dryRun: true,
16404
+ yes: false,
16405
+ noLlm: true,
16406
+ timeoutMs,
16407
+ knowledgeContext,
16408
+ json: false
16409
+ };
16410
+ if (json) {
16411
+ const reports = [];
16412
+ for (const task2 of doctorTasks) {
16413
+ const report = await captureDoctorTaskOutput(
16414
+ task2,
16415
+ db,
16416
+ diagnosisOptions
16417
+ );
16418
+ reports.push({
16419
+ name: task2.name,
16420
+ description: task2.description,
16421
+ ...report
16422
+ });
16423
+ }
16424
+ const success = reports.every((report) => !report.error);
16425
+ console.log(
16426
+ JSON.stringify({ success, readOnly: true, tasks: reports })
16427
+ );
16428
+ if (!success) process.exitCode = 1;
16429
+ } else {
16430
+ console.log("ZAM doctor read-only diagnosis (LLM disabled):");
16431
+ for (const task2 of doctorTasks) {
16432
+ console.log(`
16433
+ [${task2.name}] ${task2.description}`);
16434
+ await task2.run(db, diagnosisOptions);
16435
+ }
16436
+ }
16437
+ return;
16438
+ }
16439
+ const task = doctorTasks.find((t2) => t2.name === taskName);
16440
+ if (!task) {
16441
+ if (json) {
16442
+ console.log(
16443
+ JSON.stringify({
16444
+ success: false,
16445
+ error: `Unknown task: ${taskName}`
16446
+ })
16447
+ );
16448
+ } else {
16449
+ console.error(`Unknown task: ${taskName}`);
16450
+ }
16451
+ process.exit(1);
16452
+ }
16453
+ if (!json) {
16454
+ console.log(
16455
+ `Running doctor task: ${task.name} (fix=${fix}, dryRun=${dryRun}${noLlm ? ", noLlm=true" : ""})`
16456
+ );
16457
+ }
16458
+ const taskOptions = {
16459
+ fix,
16460
+ dryRun,
16461
+ yes,
16462
+ noLlm,
16463
+ timeoutMs,
16464
+ knowledgeContext,
16465
+ json
16466
+ };
16467
+ if (json && fix && !yes) {
16468
+ console.log(
16469
+ JSON.stringify({
16470
+ success: false,
16471
+ error: "--json --fix requires --yes; interactive prompts are not machine-readable"
16472
+ })
16473
+ );
16474
+ process.exitCode = 1;
16475
+ return;
16476
+ }
16477
+ if (json && fix && yes && (task.name === "duplicates" || task.name === "domains")) {
16478
+ console.log(
16479
+ JSON.stringify({
16480
+ success: false,
16481
+ error: `${task.name} fixes require interactive choices and cannot use --json --yes`
16482
+ })
16483
+ );
16484
+ process.exitCode = 1;
16485
+ return;
16486
+ }
16487
+ if (json) {
16488
+ const report = await captureDoctorTaskOutput(task, db, taskOptions);
16489
+ let payload = {
16490
+ success: report.error === void 0,
16491
+ task: task.name,
16492
+ ...report
16493
+ };
16494
+ if (task.name === "contexts" && !report.error && report.lines.length === 1) {
16495
+ try {
16496
+ payload = JSON.parse(report.lines[0]);
16497
+ } catch {
16498
+ }
16499
+ }
16500
+ console.log(JSON.stringify(payload));
16501
+ if (report.error || payload.success === false) {
16502
+ process.exitCode = 1;
16503
+ }
16504
+ return;
16505
+ }
16506
+ await task.run(db, taskOptions);
16507
+ });
16508
+ });
16509
+
14932
16510
  // src/cli/commands/git-sync.ts
14933
16511
  import { execSync as execSync5 } from "child_process";
14934
16512
  import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync8 } from "fs";
14935
16513
  import { join as join19 } from "path";
14936
- import { Command as Command5 } from "commander";
16514
+ import { Command as Command6 } from "commander";
14937
16515
  function installHook2() {
14938
16516
  const gitDir = join19(process.cwd(), ".git");
14939
16517
  if (!existsSync18(gitDir)) {
@@ -14963,7 +16541,7 @@ zam git-sync --commit HEAD --quiet
14963
16541
  process.exit(1);
14964
16542
  }
14965
16543
  }
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) => {
16544
+ 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
16545
  if (opts.install) {
14968
16546
  installHook2();
14969
16547
  return;
@@ -15055,7 +16633,7 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
15055
16633
  import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
15056
16634
  import { resolve as resolve5 } from "path";
15057
16635
  import { input as input2 } from "@inquirer/prompts";
15058
- import { Command as Command6 } from "commander";
16636
+ import { Command as Command7 } from "commander";
15059
16637
  async function resolveGoalsDir() {
15060
16638
  let goalsDir;
15061
16639
  let db;
@@ -15068,7 +16646,7 @@ async function resolveGoalsDir() {
15068
16646
  }
15069
16647
  return goalsDir ? resolve5(goalsDir) : resolve5("goals");
15070
16648
  }
15071
- var goalCommand = new Command6("goal").description(
16649
+ var goalCommand = new Command7("goal").description(
15072
16650
  "Manage learning goals (markdown files)"
15073
16651
  );
15074
16652
  goalCommand.command("list").description("List all goals").option(
@@ -15241,7 +16819,7 @@ import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as
15241
16819
  import { homedir as homedir11 } from "os";
15242
16820
  import { join as join20, resolve as resolve6 } from "path";
15243
16821
  import { confirm, input as input3 } from "@inquirer/prompts";
15244
- import { Command as Command7 } from "commander";
16822
+ import { Command as Command8 } from "commander";
15245
16823
  var HOME2 = homedir11();
15246
16824
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
15247
16825
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
@@ -15278,7 +16856,7 @@ Here, I declare the core concepts and principles I want to master.
15278
16856
  );
15279
16857
  }
15280
16858
  }
15281
- var initCommand = new Command7("init").description("Launch the guided interactive onboarding wizard").action(async () => {
16859
+ var initCommand = new Command8("init").description("Launch the guided interactive onboarding wizard").action(async () => {
15282
16860
  printLine();
15283
16861
  console.log(
15284
16862
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -15453,9 +17031,202 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
15453
17031
  printLine();
15454
17032
  });
15455
17033
 
17034
+ // src/cli/commands/knowledge-context.ts
17035
+ import { Command as Command9 } from "commander";
17036
+ var knowledgeContextCommand = new Command9("knowledge-context").alias("kc").description("Manage knowledge contexts (work, school, private)");
17037
+ function commandError(message, json) {
17038
+ if (json) {
17039
+ jsonOut({ success: false, error: message });
17040
+ } else {
17041
+ console.error(message);
17042
+ }
17043
+ process.exit(1);
17044
+ }
17045
+ knowledgeContextCommand.command("list").description("List all knowledge contexts").option("--json", "Output as JSON").action(async (opts) => {
17046
+ await withDb(async (db) => {
17047
+ const contexts = await listKnowledgeContexts(db);
17048
+ if (opts.json) {
17049
+ jsonOut(contexts);
17050
+ return;
17051
+ }
17052
+ if (contexts.length === 0) {
17053
+ console.log("No knowledge contexts registered yet.");
17054
+ return;
17055
+ }
17056
+ console.log(`Knowledge Contexts (${contexts.length})`);
17057
+ console.log("\u2500".repeat(60));
17058
+ for (const c of contexts) {
17059
+ const lang = c.language ? ` (${c.language})` : "";
17060
+ const label = c.label ? ` - ${c.label}` : "";
17061
+ console.log(` ${c.name.padEnd(20)}${label}${lang}`);
17062
+ }
17063
+ });
17064
+ });
17065
+ knowledgeContextCommand.command("create").description("Create a new knowledge context").requiredOption(
17066
+ "--name <name>",
17067
+ "Unique context identifier (e.g. work-company)"
17068
+ ).option("--label <label>", "Display name").option(
17069
+ "--language <language>",
17070
+ "Default language (BCP-47 code, e.g. en, de)"
17071
+ ).option("--json", "Output as JSON").action(async (opts) => {
17072
+ await withDb(async (db) => {
17073
+ const context = await createKnowledgeContext(db, {
17074
+ name: opts.name,
17075
+ label: opts.label || null,
17076
+ language: opts.language || null
17077
+ });
17078
+ if (opts.json) {
17079
+ jsonOut({ success: true, context });
17080
+ return;
17081
+ }
17082
+ console.log(`Created knowledge context: ${context.name}`);
17083
+ if (context.label) console.log(` Label: ${context.label}`);
17084
+ if (context.language) console.log(` Language: ${context.language}`);
17085
+ });
17086
+ });
17087
+ knowledgeContextCommand.command("delete").description("Delete a knowledge context").requiredOption("--name <name>", "Context name").option(
17088
+ "--confirm",
17089
+ "Confirm removal of the context and its token assignments"
17090
+ ).option("--json", "Output as JSON").action(async (opts) => {
17091
+ await withDb(async (db) => {
17092
+ const context = await getKnowledgeContextByName(db, opts.name);
17093
+ if (!context) {
17094
+ commandError(`Knowledge context not found: ${opts.name}`, !!opts.json);
17095
+ }
17096
+ const assignmentCount = (await db.prepare(
17097
+ "SELECT COUNT(*) AS n FROM token_contexts WHERE context_id = ?"
17098
+ ).get(context.id)).n;
17099
+ if (!opts.confirm) {
17100
+ if (opts.json) {
17101
+ jsonOut({
17102
+ success: true,
17103
+ preview: true,
17104
+ requiresConfirmation: true,
17105
+ name: context.name,
17106
+ tokenAssignments: assignmentCount
17107
+ });
17108
+ } else {
17109
+ console.log(
17110
+ `Deleting "${context.name}" removes ${assignmentCount} token assignment(s). Re-run with --confirm.`
17111
+ );
17112
+ }
17113
+ return;
17114
+ }
17115
+ await deleteKnowledgeContext(db, context.id);
17116
+ if (getActiveWorkspaceContext() === context.name) {
17117
+ setActiveWorkspaceContext(void 0);
17118
+ }
17119
+ if (opts.json) {
17120
+ jsonOut({
17121
+ success: true,
17122
+ name: context.name,
17123
+ tokenAssignmentsRemoved: assignmentCount
17124
+ });
17125
+ return;
17126
+ }
17127
+ console.log(`Deleted knowledge context: ${context.name}`);
17128
+ });
17129
+ });
17130
+ knowledgeContextCommand.command("assign").description("Assign a token to a knowledge context").requiredOption("--token <slug>", "Token slug").requiredOption("--context <name>", "Context name").option("--json", "Output as JSON").action(async (opts) => {
17131
+ await withDb(async (db) => {
17132
+ const context = await getKnowledgeContextByName(db, opts.context);
17133
+ if (!context) {
17134
+ commandError(
17135
+ `Knowledge context not found: ${opts.context}`,
17136
+ !!opts.json
17137
+ );
17138
+ }
17139
+ const token = await getTokenBySlug(db, opts.token);
17140
+ if (!token) {
17141
+ commandError(`Token not found: ${opts.token}`, !!opts.json);
17142
+ }
17143
+ await assignTokenToContext(db, token.id, context.id);
17144
+ if (opts.json) {
17145
+ jsonOut({ success: true, token: token.slug, context: context.name });
17146
+ return;
17147
+ }
17148
+ console.log(
17149
+ `Assigned token "${token.slug}" to context "${context.name}"`
17150
+ );
17151
+ });
17152
+ });
17153
+ knowledgeContextCommand.command("unassign").description("Remove a token from a knowledge context").requiredOption("--token <slug>", "Token slug").requiredOption("--context <name>", "Context name").option("--json", "Output as JSON").action(async (opts) => {
17154
+ await withDb(async (db) => {
17155
+ const context = await getKnowledgeContextByName(db, opts.context);
17156
+ if (!context) {
17157
+ commandError(
17158
+ `Knowledge context not found: ${opts.context}`,
17159
+ !!opts.json
17160
+ );
17161
+ }
17162
+ const token = await getTokenBySlug(db, opts.token);
17163
+ if (!token) {
17164
+ commandError(`Token not found: ${opts.token}`, !!opts.json);
17165
+ }
17166
+ await unassignTokenFromContext(db, token.id, context.id);
17167
+ if (opts.json) {
17168
+ jsonOut({ success: true, token: token.slug, context: context.name });
17169
+ return;
17170
+ }
17171
+ console.log(
17172
+ `Unassigned token "${token.slug}" from context "${context.name}"`
17173
+ );
17174
+ });
17175
+ });
17176
+ knowledgeContextCommand.command("use").description("Set the active knowledge context default for this device").argument("[name]", "Context name to use (use without argument to clear)").option("--json", "Output as JSON").action(async (name, opts) => {
17177
+ await withDb(async (db) => {
17178
+ if (!name) {
17179
+ if (!setActiveWorkspaceContext(void 0)) {
17180
+ commandError("No active workspace configured", !!opts.json);
17181
+ }
17182
+ if (opts.json) {
17183
+ jsonOut({ success: true, activeContext: null });
17184
+ return;
17185
+ }
17186
+ console.log("Cleared active knowledge context default.");
17187
+ return;
17188
+ }
17189
+ const context = await getKnowledgeContextByName(db, name);
17190
+ if (!context) {
17191
+ commandError(`Knowledge context not found: ${name}`, !!opts.json);
17192
+ }
17193
+ if (!setActiveWorkspaceContext(context.name)) {
17194
+ commandError("No active workspace configured", !!opts.json);
17195
+ }
17196
+ if (opts.json) {
17197
+ jsonOut({ success: true, activeContext: context.name });
17198
+ return;
17199
+ }
17200
+ console.log(`Active knowledge context set to: ${context.name}`);
17201
+ });
17202
+ });
17203
+ knowledgeContextCommand.command("show").description("Show the active knowledge context default for this device").option("--json", "Output as JSON").action(async (opts) => {
17204
+ await withDb(async (db) => {
17205
+ const configured = getActiveWorkspaceContext();
17206
+ const active = configured ? await getKnowledgeContextByName(db, configured) : void 0;
17207
+ if (opts.json) {
17208
+ jsonOut({
17209
+ success: true,
17210
+ activeContext: active?.name ?? null,
17211
+ staleContext: configured && !active ? configured : null
17212
+ });
17213
+ return;
17214
+ }
17215
+ if (active) {
17216
+ console.log(`Active knowledge context: ${active.name}`);
17217
+ } else if (configured) {
17218
+ console.warn(
17219
+ `Configured knowledge context no longer exists: ${configured}`
17220
+ );
17221
+ } else {
17222
+ console.log("No active knowledge context default set.");
17223
+ }
17224
+ });
17225
+ });
17226
+
15456
17227
  // src/cli/commands/learn.ts
15457
17228
  import { input as input5 } from "@inquirer/prompts";
15458
- import { Command as Command8 } from "commander";
17229
+ import { Command as Command10 } from "commander";
15459
17230
 
15460
17231
  // src/cli/learn-format.ts
15461
17232
  var BLOOM_VERBS3 = {
@@ -15471,16 +17242,22 @@ function clampBloom(n) {
15471
17242
  function formatHeader(input8) {
15472
17243
  const lvl = clampBloom(input8.bloomLevel);
15473
17244
  const parts = [`${BLOOM_VERBS3[lvl]} (Bloom ${lvl})`];
15474
- if (input8.domain?.trim()) {
17245
+ const name = input8.title || input8.slug;
17246
+ if (name) {
17247
+ parts.push(name);
17248
+ } else if (input8.domain?.trim()) {
15475
17249
  parts.push(input8.domain.trim());
15476
17250
  }
15477
17251
  return parts.join(" \xB7 ");
15478
17252
  }
15479
17253
  function formatReveal(input8) {
15480
17254
  const lines = [
15481
- `Token: #${input8.slug}`,
17255
+ `Token: ${input8.title || input8.slug}`,
15482
17256
  `Concept: ${input8.concept}`
15483
17257
  ];
17258
+ if (input8.title) {
17259
+ lines.push(`ID: #${input8.slug}`);
17260
+ }
15484
17261
  if (input8.context?.trim()) {
15485
17262
  lines.push("", `Context: ${input8.context.trim()}`);
15486
17263
  }
@@ -15793,7 +17570,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
15793
17570
  function isExitPrompt(err) {
15794
17571
  return err instanceof Error && err.name === "ExitPromptError";
15795
17572
  }
15796
- var learnCommand = new Command8("learn").description(
17573
+ var learnCommand = new Command10("learn").description(
15797
17574
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
15798
17575
  ).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
17576
  let db;
@@ -15867,7 +17644,7 @@ ${t(locale, "welcome", { count: queue.items.length })}`);
15867
17644
  console.log(`
15868
17645
  ${"\u2500".repeat(50)}`);
15869
17646
  console.log(
15870
- `[${index + 1}/${queue.items.length}] ${formatHeader(item)}`
17647
+ `[${index + 1}/${queue.items.length}] ${formatHeader({ bloomLevel: item.bloomLevel, domain: item.domain })}`
15871
17648
  );
15872
17649
  console.log(`
15873
17650
  ${prompt.question}`);
@@ -15931,6 +17708,7 @@ ${t(locale, "eval_skipped", { reason: err.message })}`
15931
17708
  );
15932
17709
  const reveal = formatReveal({
15933
17710
  slug: item.slug,
17711
+ title: item.title,
15934
17712
  concept: item.concept,
15935
17713
  context: token?.context,
15936
17714
  resolved
@@ -16038,8 +17816,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
16038
17816
  });
16039
17817
 
16040
17818
  // src/cli/commands/monitor.ts
16041
- import { Command as Command9 } from "commander";
16042
- var monitorCommand = new Command9("monitor").description(
17819
+ import { Command as Command11 } from "commander";
17820
+ var monitorCommand = new Command11("monitor").description(
16043
17821
  "Shell observation for real-time task monitoring"
16044
17822
  );
16045
17823
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -16221,8 +17999,8 @@ monitorCommand.command("open").description("Open a new monitored terminal window
16221
17999
  });
16222
18000
 
16223
18001
  // src/cli/commands/observer.ts
16224
- import { Command as Command10 } from "commander";
16225
- var observerCommand = new Command10("observer").description(
18002
+ import { Command as Command12 } from "commander";
18003
+ var observerCommand = new Command12("observer").description(
16226
18004
  "Configure what the UI observer may capture (Layer 2 policy)"
16227
18005
  );
16228
18006
  function applyObserverListChange(current, entry, op) {
@@ -16285,7 +18063,7 @@ observerCommand.command("revoke <process>").description("Remove a process from o
16285
18063
 
16286
18064
  // src/cli/commands/profile.ts
16287
18065
  import { dirname as dirname6, resolve as resolve7 } from "path";
16288
- import { Command as Command11 } from "commander";
18066
+ import { Command as Command13 } from "commander";
16289
18067
  var C2 = {
16290
18068
  reset: "\x1B[0m",
16291
18069
  bold: "\x1B[1m",
@@ -16302,7 +18080,7 @@ function render(profile) {
16302
18080
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
16303
18081
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
16304
18082
  }
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) => {
18083
+ var profileCommand = new Command13("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
18084
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
16307
18085
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
16308
18086
  process.exit(1);
@@ -16342,8 +18120,8 @@ var profileCommand = new Command11("profile").description("Show or change this m
16342
18120
 
16343
18121
  // src/cli/commands/provider.ts
16344
18122
  import { password as password2 } from "@inquirer/prompts";
16345
- import { Command as Command12 } from "commander";
16346
- var providerCommand = new Command12("provider").description(
18123
+ import { Command as Command14 } from "commander";
18124
+ var providerCommand = new Command14("provider").description(
16347
18125
  "Configure role-based AI providers (url/model/flavor/key, per role)"
16348
18126
  );
16349
18127
  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,16 +18356,33 @@ providerCommand.command("clear-key <ref>").description("Remove a stored API key"
16578
18356
  });
16579
18357
 
16580
18358
  // 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) => {
18359
+ import { Command as Command15 } from "commander";
18360
+ var reviewCommand = new Command15("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").option(
18361
+ "--knowledge-context <context>",
18362
+ "Filter review queue by knowledge context"
18363
+ ).action(async (opts) => {
16583
18364
  let db;
16584
18365
  try {
16585
18366
  db = await openDatabase();
16586
18367
  const userId = await resolveUser(opts, db);
18368
+ let resolvedContext;
18369
+ if (opts.knowledgeContext) {
18370
+ const context = await getKnowledgeContextByName(
18371
+ db,
18372
+ opts.knowledgeContext
18373
+ );
18374
+ if (!context) {
18375
+ throw new Error(
18376
+ `Knowledge context not found: ${opts.knowledgeContext}`
18377
+ );
18378
+ }
18379
+ resolvedContext = context.name;
18380
+ }
16587
18381
  const queue = await buildReviewQueue(db, {
16588
18382
  userId,
16589
18383
  maxNew: Number(opts.maxNew),
16590
- maxReviews: Number(opts.maxReviews)
18384
+ maxReviews: Number(opts.maxReviews),
18385
+ knowledgeContext: resolvedContext
16591
18386
  });
16592
18387
  if (queue.items.length === 0) {
16593
18388
  console.log("No cards due for review. You're all caught up!");
@@ -16596,6 +18391,9 @@ var reviewCommand = new Command13("review").description("Start an interactive re
16596
18391
  }
16597
18392
  console.log(`
16598
18393
  Review session: ${queue.items.length} card(s)`);
18394
+ if (resolvedContext) {
18395
+ console.log(` Context: ${resolvedContext}`);
18396
+ }
16599
18397
  console.log(
16600
18398
  ` New: ${queue.newCount} Review: ${queue.reviewCount} Relearn: ${queue.relearnCount}`
16601
18399
  );
@@ -16617,9 +18415,8 @@ Review session: ${queue.items.length} card(s)`);
16617
18415
  });
16618
18416
  console.log(
16619
18417
  `
16620
- [${index + 1}/${queue.items.length}] ${prompt.bloomVerb} (Bloom ${prompt.bloomLevel})`
18418
+ [${index + 1}/${queue.items.length}] ${formatHeader({ bloomLevel: item.bloomLevel, domain: item.domain })}`
16621
18419
  );
16622
- console.log(`Domain: ${prompt.domain || "(none)"}`);
16623
18420
  if (prompt.sourceLink) {
16624
18421
  console.log(`Source: ${prompt.sourceLink}`);
16625
18422
  if (opts.resolve !== false) {
@@ -16694,8 +18491,8 @@ ${"\u2550".repeat(50)}`);
16694
18491
  // src/cli/commands/session.ts
16695
18492
  import { readFileSync as readFileSync13 } from "fs";
16696
18493
  import { input as input6, select as select2 } from "@inquirer/prompts";
16697
- import { Command as Command14 } from "commander";
16698
- var sessionCommand = new Command14("session").description(
18494
+ import { Command as Command16 } from "commander";
18495
+ var sessionCommand = new Command16("session").description(
16699
18496
  "Manage learning sessions"
16700
18497
  );
16701
18498
  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 +18616,8 @@ Time limit reached (${maxMinutes} min). Moving to task selection.`
16819
18616
  });
16820
18617
  const elapsed = Math.round((Date.now() - startTime) / 6e4);
16821
18618
  console.log(
16822
- `[${index + 1}/${queue.items.length}] ${prompt.bloomVerb} (Bloom ${prompt.bloomLevel}) \u2014 ${elapsed}/${maxMinutes} min`
18619
+ `[${index + 1}/${queue.items.length}] ${formatHeader({ bloomLevel: item.bloomLevel, domain: item.domain })} (${elapsed}/${maxMinutes} min)`
16823
18620
  );
16824
- console.log(`Domain: ${prompt.domain || "(none)"}`);
16825
18621
  console.log(`
16826
18622
  ${prompt.question}
16827
18623
  `);
@@ -17075,8 +18871,8 @@ sessionCommand.command("end").description("End a session and show summary").requ
17075
18871
 
17076
18872
  // src/cli/commands/settings.ts
17077
18873
  import { existsSync as existsSync21 } from "fs";
17078
- import { Command as Command15 } from "commander";
17079
- var settingsCommand = new Command15("settings").description(
18874
+ import { Command as Command17 } from "commander";
18875
+ var settingsCommand = new Command17("settings").description(
17080
18876
  "Manage user settings"
17081
18877
  );
17082
18878
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -17252,7 +19048,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
17252
19048
 
17253
19049
  // src/cli/commands/setup.ts
17254
19050
  import { resolve as resolve8 } from "path";
17255
- import { Command as Command16 } from "commander";
19051
+ import { Command as Command18 } from "commander";
17256
19052
  function formatDatabaseInitTarget(target) {
17257
19053
  switch (target.kind) {
17258
19054
  case "local":
@@ -17293,7 +19089,7 @@ async function activateMachineProviderConfig(db) {
17293
19089
  ` activate ${providerCount} machine-local provider(s) from ~/.zam/config.json`
17294
19090
  );
17295
19091
  }
17296
- var setupCommand = new Command16("setup").description(
19092
+ var setupCommand = new Command18("setup").description(
17297
19093
  "Link ZAM skill directories into this workspace and initialize the database"
17298
19094
  ).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
19095
  "--agents <list>",
@@ -17347,8 +19143,8 @@ var setupCommand = new Command16("setup").description(
17347
19143
  );
17348
19144
 
17349
19145
  // src/cli/commands/skill.ts
17350
- import { Command as Command17 } from "commander";
17351
- var skillCommand = new Command17("skill").description(
19146
+ import { Command as Command19 } from "commander";
19147
+ var skillCommand = new Command19("skill").description(
17352
19148
  "Manage agent skill entries (task recipes)"
17353
19149
  );
17354
19150
  skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
@@ -17435,7 +19231,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
17435
19231
  // src/cli/commands/snapshot.ts
17436
19232
  import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
17437
19233
  import { dirname as dirname7, join as join21 } from "path";
17438
- import { Command as Command18 } from "commander";
19234
+ import { Command as Command20 } from "commander";
17439
19235
  function defaultOutName() {
17440
19236
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
17441
19237
  return `zam-snapshot-${stamp}.sql`;
@@ -17449,7 +19245,7 @@ function summarize(tables) {
17449
19245
  }
17450
19246
  return { total, nonEmpty };
17451
19247
  }
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) => {
19248
+ var exportCmd = new Command20("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
17453
19249
  let db;
17454
19250
  try {
17455
19251
  db = await openDatabaseWithSync({ initialize: true });
@@ -17479,7 +19275,7 @@ var exportCmd = new Command18("export").description("Write a portable SQL-text s
17479
19275
  process.exit(1);
17480
19276
  }
17481
19277
  });
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) => {
19278
+ var importCmd = new Command20("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
19279
  let db;
17484
19280
  try {
17485
19281
  if (!existsSync22(file)) {
@@ -17502,7 +19298,7 @@ var importCmd = new Command18("import").description("Restore a snapshot into the
17502
19298
  process.exit(1);
17503
19299
  }
17504
19300
  });
17505
- var verifyCmd = new Command18("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
19301
+ var verifyCmd = new Command20("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
17506
19302
  try {
17507
19303
  if (!existsSync22(file)) {
17508
19304
  console.error(`Error: Snapshot file not found: ${file}`);
@@ -17520,11 +19316,11 @@ var verifyCmd = new Command18("verify").description("Check a snapshot's manifest
17520
19316
  process.exit(1);
17521
19317
  }
17522
19318
  });
17523
- var snapshotCommand = new Command18("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
19319
+ var snapshotCommand = new Command20("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
17524
19320
 
17525
19321
  // 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) => {
19322
+ import { Command as Command21 } from "commander";
19323
+ var statsCommand = new Command21("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
17528
19324
  await withDb(async (db) => {
17529
19325
  const userId = await resolveUser(opts, db);
17530
19326
  const stats = await getUserStats(db, userId);
@@ -17560,11 +19356,19 @@ var statsCommand = new Command19("stats").description("Show learning dashboard f
17560
19356
  });
17561
19357
 
17562
19358
  // src/cli/commands/token.ts
17563
- import { Command as Command20 } from "commander";
17564
- var tokenCommand = new Command20("token").description(
19359
+ import { Command as Command22 } from "commander";
19360
+ var tokenCommand = new Command22("token").description(
17565
19361
  "Manage knowledge tokens"
17566
19362
  );
17567
- 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) => {
19363
+ 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(
19364
+ "--knowledge-context <context>",
19365
+ "Assign token to a knowledge context (repeatable)",
19366
+ (val, memo) => {
19367
+ memo.push(val);
19368
+ return memo;
19369
+ },
19370
+ []
19371
+ ).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) => {
17568
19372
  await withDb(async (db) => {
17569
19373
  let question = opts.question || null;
17570
19374
  if (!question) {
@@ -17574,6 +19378,10 @@ tokenCommand.command("register").description("Register a new knowledge token").r
17574
19378
  opts.domain
17575
19379
  );
17576
19380
  }
19381
+ const assignedContexts = await resolveOperationKnowledgeContexts(
19382
+ db,
19383
+ opts.knowledgeContext || []
19384
+ );
17577
19385
  const possibleDuplicates = await findPossibleDuplicates(db, {
17578
19386
  concept: opts.concept,
17579
19387
  question,
@@ -17587,6 +19395,9 @@ tokenCommand.command("register").description("Register a new knowledge token").r
17587
19395
  source_link: opts.sourceLink || null,
17588
19396
  question
17589
19397
  });
19398
+ for (const context of assignedContexts) {
19399
+ await assignTokenToContext(db, token.id, context.id);
19400
+ }
17590
19401
  let cardUserId = null;
17591
19402
  if (opts.card !== false) {
17592
19403
  cardUserId = opts.user ?? await getSetting(db, "user.id");
@@ -17605,18 +19416,32 @@ tokenCommand.command("register").description("Register a new knowledge token").r
17605
19416
  {
17606
19417
  ...token,
17607
19418
  card: cardUserId ? { userId: cardUserId } : null,
17608
- possible_duplicates: possibleDuplicates
19419
+ possible_duplicates: possibleDuplicates,
19420
+ knowledgeContexts: assignedContexts.map((c) => ({
19421
+ name: c.name,
19422
+ label: c.label,
19423
+ language: c.language
19424
+ }))
17609
19425
  },
17610
19426
  null,
17611
19427
  2
17612
19428
  )
17613
19429
  );
17614
19430
  } else {
17615
- console.log(`Registered token: ${token.slug} (${token.id})`);
19431
+ console.log(`Registered token: ${token.title || token.slug}`);
19432
+ if (token.slug !== (token.title || token.slug)) {
19433
+ console.log(` Slug: ${token.slug}`);
19434
+ }
19435
+ console.log(` ID: ${token.id}`);
17616
19436
  console.log(` Concept: ${token.concept}`);
17617
19437
  console.log(` Domain: ${token.domain || "(none)"}`);
17618
19438
  console.log(` Bloom: ${token.bloom_level}`);
17619
19439
  console.log(` Question: ${token.question}`);
19440
+ if (assignedContexts.length > 0) {
19441
+ console.log(
19442
+ ` Contexts: ${assignedContexts.map((c) => c.name).join(", ")}`
19443
+ );
19444
+ }
17620
19445
  if (token.source_link) {
17621
19446
  console.log(` Source: ${token.source_link}`);
17622
19447
  }
@@ -17631,8 +19456,9 @@ tokenCommand.command("register").description("Register a new knowledge token").r
17631
19456
  console.log(`
17632
19457
  WARNING: Possible duplicate tokens found:`);
17633
19458
  for (const dup of possibleDuplicates) {
19459
+ const name = dup.title || dup.slug;
17634
19460
  console.log(
17635
- ` - ${dup.slug} (similarity: ${dup.similarity.toFixed(2)})`
19461
+ ` - ${name} (slug: ${dup.slug}, similarity: ${dup.similarity.toFixed(2)})`
17636
19462
  );
17637
19463
  }
17638
19464
  }
@@ -17640,7 +19466,8 @@ WARNING: Possible duplicate tokens found:`);
17640
19466
  const queryText = embeddingContentForToken({
17641
19467
  concept: opts.concept,
17642
19468
  question: question ?? null,
17643
- domain: opts.domain ?? ""
19469
+ domain: opts.domain ?? "",
19470
+ title: opts.title ?? null
17644
19471
  });
17645
19472
  const q2 = await embedQuery(db, queryText);
17646
19473
  if (q2) {
@@ -17707,23 +19534,29 @@ tokenCommand.command("find").description("Fuzzy search for tokens").requiredOpti
17707
19534
  console.log(`Found ${results.length} token(s):
17708
19535
  `);
17709
19536
  console.log(
17710
- "Score Sim Slug Concept Domain Bloom"
19537
+ "Score Sim Title Concept Domain Bloom"
17711
19538
  );
17712
19539
  console.log("\u2500".repeat(95));
17713
19540
  for (const t2 of results) {
17714
19541
  const scoreStr = t2.score.toFixed(3).padEnd(6);
17715
19542
  const simStr = (t2.similarity?.toFixed(2) ?? "-").padEnd(4);
19543
+ const display = getDisplayTitle(t2);
17716
19544
  console.log(
17717
- `${scoreStr} ${simStr} ${t2.slug.padEnd(21)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
19545
+ `${scoreStr} ${simStr} ${display.padEnd(28)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
17718
19546
  );
17719
19547
  }
19548
+ console.log("\n(Use slug for CLI commands -- slugs are technical IDs)");
17720
19549
  });
17721
19550
  });
17722
- 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) => {
19551
+ tokenCommand.command("list").description("List all tokens").option("--domain <domain>", "Filter by domain").option("--knowledge-context <context>", "Filter by knowledge context").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
17723
19552
  await withDb(async (db) => {
19553
+ const filterOpts = {};
19554
+ if (opts.domain) filterOpts.domain = opts.domain;
19555
+ if (opts.knowledgeContext)
19556
+ filterOpts.knowledgeContext = opts.knowledgeContext;
17724
19557
  const tokens = await listTokens(
17725
19558
  db,
17726
- opts.domain ? { domain: opts.domain } : void 0
19559
+ Object.keys(filterOpts).length ? filterOpts : void 0
17727
19560
  );
17728
19561
  if (opts.quiet) return;
17729
19562
  if (opts.json) {
@@ -17735,16 +19568,20 @@ tokenCommand.command("list").description("List all tokens").option("--domain <do
17735
19568
  return;
17736
19569
  }
17737
19570
  console.log(
17738
- "Slug Concept Domain Bloom"
19571
+ "Title Concept Domain Bloom"
17739
19572
  );
17740
- console.log("\u2500".repeat(80));
19573
+ console.log("\u2500".repeat(85));
17741
19574
  for (const t2 of tokens) {
19575
+ const display = getDisplayTitle(t2);
17742
19576
  console.log(
17743
- `${t2.slug.padEnd(21)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
19577
+ `${display.padEnd(28)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
17744
19578
  );
17745
19579
  }
17746
19580
  console.log(`
17747
19581
  ${tokens.length} token(s) total.`);
19582
+ console.log(
19583
+ "(Use --slug <slug> for commands; slugs are technical identifiers)"
19584
+ );
17748
19585
  });
17749
19586
  });
17750
19587
  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 +19590,10 @@ tokenCommand.command("edit").description("Edit a token's mutable fields").requir
17753
19590
  ).option(
17754
19591
  "--source-link <link>",
17755
19592
  "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) => {
19593
+ ).option("--question <question>", "Updated question text (blank allowed)").option(
19594
+ "--title <title>",
19595
+ "Updated human-friendly title for graph display (blank allowed)"
19596
+ ).option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
17757
19597
  await withDb(async (db) => {
17758
19598
  const updates = {};
17759
19599
  if (opts.concept !== void 0) updates.concept = opts.concept;
@@ -17767,6 +19607,9 @@ tokenCommand.command("edit").description("Edit a token's mutable fields").requir
17767
19607
  if (opts.question !== void 0) {
17768
19608
  updates.question = opts.question === "" ? null : opts.question;
17769
19609
  }
19610
+ if (opts.title !== void 0) {
19611
+ updates.title = opts.title === "" ? "" : opts.title;
19612
+ }
17770
19613
  if (opts.mode !== void 0) {
17771
19614
  const validModes = ["shadowing", "copilot", "autonomy", "none"];
17772
19615
  if (!validModes.includes(opts.mode)) {
@@ -17781,11 +19624,15 @@ tokenCommand.command("edit").description("Edit a token's mutable fields").requir
17781
19624
  jsonOut(token);
17782
19625
  return;
17783
19626
  }
17784
- console.log(`Updated token: ${token.slug}`);
19627
+ console.log(`Updated token: ${token.title || token.slug}`);
19628
+ if (token.slug !== (token.title || token.slug)) {
19629
+ console.log(` Slug: ${token.slug}`);
19630
+ }
17785
19631
  console.log(` Concept: ${token.concept}`);
17786
19632
  console.log(` Domain: ${token.domain || "(none)"}`);
17787
19633
  console.log(` Bloom: ${token.bloom_level}`);
17788
19634
  console.log(` Question: ${token.question || "(none)"}`);
19635
+ console.log(` Title: ${token.title || "(none)"}`);
17789
19636
  console.log(` Context: ${token.context || "(none)"}`);
17790
19637
  console.log(` Mode: ${token.symbiosis_mode ?? "none"}`);
17791
19638
  console.log(` Source: ${token.source_link || "(none)"}`);
@@ -17904,7 +19751,11 @@ tokenCommand.command("status").description("Show full status of a token for a us
17904
19751
  console.log(JSON.stringify(status, null, 2));
17905
19752
  return;
17906
19753
  }
17907
- console.log(`Token: ${token.slug} (${token.id})`);
19754
+ console.log(`Token: ${token.title || token.slug}`);
19755
+ if (token.slug !== (token.title || token.slug)) {
19756
+ console.log(` Slug: ${token.slug}`);
19757
+ }
19758
+ console.log(` ID: ${token.id}`);
17908
19759
  console.log(` Concept: ${token.concept}`);
17909
19760
  console.log(` Question: ${token.question || "(none)"}`);
17910
19761
  console.log(` Domain: ${token.domain || "(none)"}`);
@@ -17929,7 +19780,9 @@ tokenCommand.command("status").description("Show full status of a token for a us
17929
19780
  if (prereqs.length > 0) {
17930
19781
  console.log("Prerequisites:");
17931
19782
  for (const p of prereqs) {
17932
- console.log(` - ${p.slug}: ${p.concept} (bloom ${p.bloom_level})`);
19783
+ console.log(
19784
+ ` - ${p.title || p.slug}: ${p.concept} (bloom ${p.bloom_level})`
19785
+ );
17933
19786
  }
17934
19787
  } else {
17935
19788
  console.log("No prerequisites.");
@@ -17937,7 +19790,9 @@ tokenCommand.command("status").description("Show full status of a token for a us
17937
19790
  if (dependents.length > 0) {
17938
19791
  console.log("\nDependents:");
17939
19792
  for (const d of dependents) {
17940
- console.log(` - ${d.slug}: ${d.concept} (bloom ${d.bloom_level})`);
19793
+ console.log(
19794
+ ` - ${d.title || d.slug}: ${d.concept} (bloom ${d.bloom_level})`
19795
+ );
17941
19796
  }
17942
19797
  }
17943
19798
  });
@@ -17999,7 +19854,7 @@ import { existsSync as existsSync23 } from "fs";
17999
19854
  import { homedir as homedir12 } from "os";
18000
19855
  import { dirname as dirname8, join as join22 } from "path";
18001
19856
  import { fileURLToPath as fileURLToPath3 } from "url";
18002
- import { Command as Command21 } from "commander";
19857
+ import { Command as Command23 } from "commander";
18003
19858
  var C3 = {
18004
19859
  reset: "\x1B[0m",
18005
19860
  red: "\x1B[31m",
@@ -18179,7 +20034,7 @@ function createShortcuts(appPath, repoRoot) {
18179
20034
  console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
18180
20035
  }
18181
20036
  }
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(
20037
+ var uiCommand = new Command23("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
18183
20038
  "--build",
18184
20039
  "Build the native installer for your OS (needs Rust, one-time)"
18185
20040
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
@@ -18287,7 +20142,7 @@ import { existsSync as existsSync24, readFileSync as readFileSync15, realpathSyn
18287
20142
  import { dirname as dirname9, join as join23 } from "path";
18288
20143
  import { fileURLToPath as fileURLToPath4 } from "url";
18289
20144
  import { confirm as confirm3 } from "@inquirer/prompts";
18290
- import { Command as Command22 } from "commander";
20145
+ import { Command as Command24 } from "commander";
18291
20146
  var GITHUB_REPO = "zam-os/zam";
18292
20147
  var CHANNELS = [
18293
20148
  "developer",
@@ -18365,7 +20220,7 @@ function render2(decision) {
18365
20220
  );
18366
20221
  }
18367
20222
  }
18368
- var checkCmd = new Command22("check").description("Check whether a newer ZAM has been released").option(
20223
+ var checkCmd = new Command24("check").description("Check whether a newer ZAM has been released").option(
18369
20224
  "--latest <version>",
18370
20225
  "Compare against this version instead of fetching"
18371
20226
  ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
@@ -18532,7 +20387,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
18532
20387
  process.exit(1);
18533
20388
  }
18534
20389
  }
18535
- var updateCommand = new Command22("update").description(
20390
+ var updateCommand = new Command24("update").description(
18536
20391
  "Update ZAM to the latest release (use `update check` to only check)"
18537
20392
  ).option("-y, --yes", "Apply without confirmation").option(
18538
20393
  "--force",
@@ -18542,8 +20397,8 @@ var updateCommand = new Command22("update").description(
18542
20397
  }).addCommand(checkCmd);
18543
20398
 
18544
20399
  // 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) => {
20400
+ import { Command as Command25 } from "commander";
20401
+ var whoamiCommand = new Command25("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
20402
  await withDb(async (db) => {
18548
20403
  if (opts.set) {
18549
20404
  await setSetting(db, "user.id", opts.set);
@@ -18584,7 +20439,7 @@ import { existsSync as existsSync25, writeFileSync as writeFileSync11 } from "fs
18584
20439
  import { homedir as homedir13 } from "os";
18585
20440
  import { join as join24, resolve as resolve9 } from "path";
18586
20441
  import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
18587
- import { Command as Command24 } from "commander";
20442
+ import { Command as Command26 } from "commander";
18588
20443
  function runGit2(cwd, args) {
18589
20444
  try {
18590
20445
  return execFileSync4("git", args, {
@@ -18602,7 +20457,7 @@ function ghRepoCreateArgs(repoName, repoVisibility) {
18602
20457
  function gitRemoteArgs(githubUrl, hasOrigin) {
18603
20458
  return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
18604
20459
  }
18605
- var workspaceCommand = new Command24("workspace").description(
20460
+ var workspaceCommand = new Command26("workspace").description(
18606
20461
  "Manage your ZAM learning workspace"
18607
20462
  );
18608
20463
  var WORKSPACE_KINDS = [
@@ -18957,13 +20812,15 @@ var __dirname = dirname10(fileURLToPath5(import.meta.url));
18957
20812
  var pkg = JSON.parse(
18958
20813
  readFileSync16(join25(__dirname, "..", "..", "package.json"), "utf-8")
18959
20814
  );
18960
- var program = new Command25();
20815
+ var program = new Command27();
18961
20816
  program.name("zam").description(
18962
20817
  "The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
18963
20818
  ).version(pkg.version);
18964
20819
  program.addCommand(initCommand);
18965
20820
  program.addCommand(setupCommand);
18966
20821
  program.addCommand(tokenCommand);
20822
+ program.addCommand(knowledgeContextCommand);
20823
+ program.addCommand(doctorCommand);
18967
20824
  program.addCommand(cardCommand);
18968
20825
  program.addCommand(sessionCommand);
18969
20826
  program.addCommand(statsCommand);