zam-core 0.7.2 → 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 Command26 } 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";
@@ -682,6 +682,22 @@ CREATE TABLE IF NOT EXISTS agent_skills (
682
682
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
683
683
  );
684
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
+
685
701
  -- Performance indexes
686
702
  CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain);
687
703
  CREATE INDEX IF NOT EXISTS idx_tokens_slug ON tokens(slug);
@@ -693,7 +709,7 @@ CREATE INDEX IF NOT EXISTS idx_review_logs_card ON review_logs(card_id);
693
709
  CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed_at);
694
710
  CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
695
711
  CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title);
696
- CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain);
712
+ CREATE INDEX IF NOT EXISTS idx_token_contexts_context ON token_contexts(context_id);
697
713
  `;
698
714
 
699
715
  // src/kernel/db/sync-adapter.ts
@@ -1036,6 +1052,25 @@ async function runMigrations(db) {
1036
1052
  await db.exec(
1037
1053
  `CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain)`
1038
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
+ `);
1039
1074
  }
1040
1075
 
1041
1076
  // src/kernel/db/snapshot.ts
@@ -1499,31 +1534,98 @@ async function deleteCardForUser(db, tokenId, userId) {
1499
1534
  await db.prepare("DELETE FROM cards WHERE id = ?").run(card.id);
1500
1535
  return { card, impact };
1501
1536
  }
1502
- async function getDueCards(db, userId, now, domain) {
1537
+ async function getDueCards(db, userId, now, domain, knowledgeContext) {
1503
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];
1504
1544
  if (domain) {
1505
- return await db.prepare(
1506
- `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
1507
- FROM cards c
1508
- JOIN tokens t ON t.id = c.token_id
1509
- WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ? AND t.domain = ?
1510
- ORDER BY t.bloom_level ASC, c.due_at ASC`
1511
- ).all(userId, cutoff, domain);
1545
+ sql += " AND t.domain = ?";
1546
+ params.push(domain);
1512
1547
  }
1513
- return await db.prepare(
1514
- `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
1515
- FROM cards c
1516
- JOIN tokens t ON t.id = c.token_id
1517
- WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ?
1518
- ORDER BY t.bloom_level ASC, c.due_at ASC`
1519
- ).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);
1520
1558
  }
1521
1559
 
1522
- // src/kernel/models/token.ts
1560
+ // src/kernel/models/knowledge-context.ts
1523
1561
  import { ulid as ulid3 } from "ulid";
1524
- 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) {
1525
1575
  const id = ulid3();
1526
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();
1527
1629
  const bloom = input8.bloom_level ?? 1;
1528
1630
  if (bloom < 1 || bloom > 5) {
1529
1631
  throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
@@ -1759,21 +1861,27 @@ async function findTokens(db, query) {
1759
1861
  return scored;
1760
1862
  }
1761
1863
  async function listTokens(db, options) {
1762
- let tokens;
1864
+ const whereClauses = ["deprecated_at IS NULL"];
1865
+ const params = [];
1763
1866
  if (options?.domain) {
1764
- tokens = await db.prepare(
1765
- "SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
1766
- ).all(options.domain);
1867
+ whereClauses.push("domain = ?");
1868
+ params.push(options.domain);
1767
1869
  } else if (options?.domainPrefix) {
1768
1870
  const prefix = options.domainPrefix;
1769
- tokens = await db.prepare(
1770
- `SELECT * FROM tokens WHERE (domain = ? OR domain LIKE ?) AND deprecated_at IS NULL ORDER BY bloom_level, slug`
1771
- ).all(prefix, `${prefix}/%`);
1772
- } else {
1773
- tokens = await db.prepare(
1774
- "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
1775
- ).all();
1776
- }
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);
1777
1885
  for (const token of tokens) {
1778
1886
  parseTokenFallback(token);
1779
1887
  }
@@ -1864,6 +1972,14 @@ async function listPersonalCards(db, userId, options) {
1864
1972
  sql += " AND t.domain = ?";
1865
1973
  values.push(options.domain);
1866
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
+ }
1867
1983
  if (options?.query) {
1868
1984
  const terms = options.query.toLowerCase().split(/\s+/).filter(Boolean);
1869
1985
  for (const term of terms) {
@@ -2389,12 +2505,12 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2389
2505
  }
2390
2506
 
2391
2507
  // src/kernel/models/review.ts
2392
- import { ulid as ulid4 } from "ulid";
2508
+ import { ulid as ulid5 } from "ulid";
2393
2509
 
2394
2510
  // src/kernel/models/session.ts
2395
- import { ulid as ulid5 } from "ulid";
2511
+ import { ulid as ulid6 } from "ulid";
2396
2512
  async function startSession(db, input8) {
2397
- const id = ulid5();
2513
+ const id = ulid6();
2398
2514
  const now = (/* @__PURE__ */ new Date()).toISOString();
2399
2515
  const ctx = input8.execution_context ?? "shell";
2400
2516
  await db.prepare(
@@ -2428,7 +2544,7 @@ async function logStep(db, input8) {
2428
2544
  if (!session) {
2429
2545
  throw new Error(`Session not found: ${input8.session_id}`);
2430
2546
  }
2431
- const id = ulid5();
2547
+ const id = ulid6();
2432
2548
  const now = (/* @__PURE__ */ new Date()).toISOString();
2433
2549
  await db.prepare(
2434
2550
  `INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
@@ -3310,10 +3426,10 @@ async function syncObserverSidecarPolicy(db, dir = getUiObserverDir()) {
3310
3426
  }
3311
3427
 
3312
3428
  // src/kernel/observation/session-synthesis.ts
3313
- import { ulid as ulid7 } from "ulid";
3429
+ import { ulid as ulid8 } from "ulid";
3314
3430
 
3315
3431
  // src/kernel/recall/evaluator.ts
3316
- import { ulid as ulid6 } from "ulid";
3432
+ import { ulid as ulid7 } from "ulid";
3317
3433
 
3318
3434
  // src/kernel/scheduler/fsrs.ts
3319
3435
  var DEFAULT_W = [
@@ -3492,7 +3608,7 @@ async function evaluateRatingWithinTransaction(db, input8) {
3492
3608
  due_at: updated.dueAt.toISOString(),
3493
3609
  last_review_at: now.toISOString()
3494
3610
  });
3495
- const reviewLogId = input8.reviewLogId ?? ulid6();
3611
+ const reviewLogId = input8.reviewLogId ?? ulid7();
3496
3612
  await db.prepare(
3497
3613
  `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
3498
3614
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
@@ -3826,7 +3942,7 @@ async function applySessionSynthesis(db, input8) {
3826
3942
  return { applied: false, record: parseSynthesisRow(existing) };
3827
3943
  }
3828
3944
  const card = await ensureCard(tx, token.id, session.user_id);
3829
- const reviewLogId = ulid7();
3945
+ const reviewLogId = ulid8();
3830
3946
  await evaluateRatingWithinTransaction(tx, {
3831
3947
  cardId: card.id,
3832
3948
  tokenId: token.id,
@@ -4652,8 +4768,7 @@ async function buildReviewQueue(db, options) {
4652
4768
  const maxReviews = options.maxReviews ?? 50;
4653
4769
  const now = options.now ?? /* @__PURE__ */ new Date();
4654
4770
  const nowISO = now.toISOString();
4655
- const dueRows = await db.prepare(
4656
- `SELECT
4771
+ let dueSql = `SELECT
4657
4772
  c.id AS card_id,
4658
4773
  c.token_id AS token_id,
4659
4774
  t.slug AS slug,
@@ -4671,11 +4786,19 @@ async function buildReviewQueue(db, options) {
4671
4786
  AND c.blocked = 0
4672
4787
  AND c.due_at <= ?
4673
4788
  AND c.state IN ('review', 'relearning', 'learning')
4674
- AND t.deprecated_at IS NULL
4675
- ORDER BY c.due_at ASC`
4676
- ).all(options.userId, nowISO);
4677
- const newRows = await db.prepare(
4678
- `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
4679
4802
  c.id AS card_id,
4680
4803
  c.token_id AS token_id,
4681
4804
  t.slug AS slug,
@@ -4692,10 +4815,19 @@ async function buildReviewQueue(db, options) {
4692
4815
  WHERE c.user_id = ?
4693
4816
  AND c.blocked = 0
4694
4817
  AND c.state = 'new'
4695
- AND t.deprecated_at IS NULL
4696
- ORDER BY t.bloom_level ASC, t.slug ASC
4697
- LIMIT ?`
4698
- ).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);
4699
4831
  const nowMs = now.getTime();
4700
4832
  const sortedDue = [...dueRows].sort((a, b) => {
4701
4833
  const overdueA = nowMs - new Date(a.due_at).getTime();
@@ -5426,6 +5558,27 @@ function detectSyncProvider(dir) {
5426
5558
  }
5427
5559
  return null;
5428
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
+ }
5429
5582
 
5430
5583
  // src/kernel/system/installer.ts
5431
5584
  import { execFileSync, execSync } from "child_process";
@@ -6292,7 +6445,7 @@ import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync a
6292
6445
  import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
6293
6446
  import { join as join18, resolve as resolve4 } from "path";
6294
6447
  import { Command as Command2 } from "commander";
6295
- import { ulid as ulid8 } from "ulid";
6448
+ import { ulid as ulid9 } from "ulid";
6296
6449
 
6297
6450
  // src/cli/adapters/source-reader.ts
6298
6451
  import dns from "dns";
@@ -6819,20 +6972,28 @@ JSON Array Output:`;
6819
6972
  );
6820
6973
  return readChatContent(res, "LLM curriculum import");
6821
6974
  }
6822
- async function importCurriculumViaLLM(db, text, targetCategory, sourceUrl) {
6975
+ async function importCurriculumViaLLM(db, text, targetCategory, sourceUrl, options) {
6823
6976
  if (text.length > MAX_IMPORT_TEXT_CHARS) {
6824
6977
  throw new Error(
6825
6978
  `Curriculum text exceeds the ${MAX_IMPORT_TEXT_CHARS.toLocaleString()} character limit`
6826
6979
  );
6827
6980
  }
6828
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
+ }
6829
6990
  const endpoint = await resolveUsableTextEndpoint(db);
6830
- const langName = LANGUAGE_NAMES[cfg.locale] || "English";
6991
+ const langName = LANGUAGE_NAMES[locale] || "English";
6831
6992
  let responseText;
6832
6993
  try {
6833
6994
  responseText = await requestCurriculumCards(
6834
6995
  endpoint,
6835
- cfg.locale,
6996
+ locale,
6836
6997
  langName,
6837
6998
  text,
6838
6999
  targetCategory,
@@ -6849,7 +7010,7 @@ async function importCurriculumViaLLM(db, text, targetCategory, sourceUrl) {
6849
7010
  try {
6850
7011
  chunkResponse = await requestCurriculumCards(
6851
7012
  endpoint,
6852
- cfg.locale,
7013
+ locale,
6853
7014
  langName,
6854
7015
  chunk,
6855
7016
  targetCategory,
@@ -7703,6 +7864,14 @@ async function ensureHighQualityQuestion(db, token) {
7703
7864
  }
7704
7865
  async function generateTitleViaLLM(db, input8, opts = {}) {
7705
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
+ }
7706
7875
  const endpoint = await resolveUsableTextEndpoint(db);
7707
7876
  const systemPrompt = `You are an expert at naming knowledge items for a personal knowledge graph.
7708
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.
@@ -7710,7 +7879,7 @@ Your task: given a knowledge token's concept (the full reference answer), questi
7710
7879
  Strict rules from the project ADR:
7711
7880
  - Concise name for the concept (\u2264 80 chars ideal).
7712
7881
  - Thoughtful, memorable name \u2014 NOT a definition or the first sentence of the concept.
7713
- - NEVER echo the domain name (e.g. do not say "Node Drain" if domain is "axon-ivy"; just "Node Drain Protection").
7882
+ - NEVER echo the domain name (e.g. do not say "Workflow Engine Node Drain" if domain is "workflow-engine"; just "Node Drain Protection").
7714
7883
  - Prefer a name over spoiling the full concept.
7715
7884
  - Support Unicode (umlauts, etc.).
7716
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).
@@ -7747,7 +7916,7 @@ Title:`;
7747
7916
  temperature: 0.2,
7748
7917
  max_tokens: 100
7749
7918
  }),
7750
- locale: cfg.locale,
7919
+ locale,
7751
7920
  timeoutMs: opts.timeoutMs,
7752
7921
  hardTimeoutMs: opts.timeoutMs
7753
7922
  });
@@ -7883,7 +8052,7 @@ async function readWebLink(url) {
7883
8052
  redirect: "manual",
7884
8053
  signal: controller.signal,
7885
8054
  headers: {
7886
- "User-Agent": "ZAM-Content-Studio/0.6.3"
8055
+ "User-Agent": "ZAM-Content-Studio/0.8.0"
7887
8056
  }
7888
8057
  });
7889
8058
  if (res.status >= 300 && res.status < 400) {
@@ -10607,6 +10776,25 @@ function getCurriculumProvider(id) {
10607
10776
  return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
10608
10777
  }
10609
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
+
10610
10798
  // src/cli/llm/embedder.ts
10611
10799
  var CANONICAL_EMBEDDING_MODEL_ID = "embeddinggemma-300m";
10612
10800
  var EMBEDDINGGEMMA_ALIASES = /* @__PURE__ */ new Set([
@@ -11921,6 +12109,20 @@ function jsonError(message) {
11921
12109
  console.log(JSON.stringify({ error: msg }, null, 2));
11922
12110
  process.exit(1);
11923
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
+ }
11924
12126
  function parseNonNegativeIntegerOption(name, value) {
11925
12127
  const parsed = Number(value);
11926
12128
  if (!Number.isInteger(parsed) || parsed < 0) {
@@ -11974,16 +12176,23 @@ function parseTokenUpdates(opts) {
11974
12176
  var bridgeCommand = new Command2("bridge").description(
11975
12177
  "Machine-readable JSON protocol for AI integration"
11976
12178
  );
11977
- 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) => {
11978
12180
  await withDb2(async (db) => {
11979
12181
  const userId = await resolveUser(opts, db, { json: true });
11980
- 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
+ );
11981
12189
  const domains = [
11982
12190
  ...new Set(dueCards.map((c) => c.domain).filter(Boolean))
11983
12191
  ].sort();
11984
12192
  jsonOut2({
11985
12193
  userId,
11986
12194
  domain: opts.domain ?? null,
12195
+ knowledgeContext: opts.knowledgeContext ?? null,
11987
12196
  dueCount: dueCards.length,
11988
12197
  domains,
11989
12198
  cards: dueCards.map((c) => ({
@@ -12244,13 +12453,14 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
12244
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(
12245
12454
  "--no-dynamic-question",
12246
12455
  "Use the stored question without generating a fresh LLM question"
12247
- ).action(async (opts) => {
12456
+ ).option("--knowledge-context <context>", "Filter cards by knowledge context").action(async (opts) => {
12248
12457
  await withDb2(async (db) => {
12249
12458
  const userId = await resolveUser(opts, db, { json: true });
12250
12459
  const queue = await buildReviewQueue(db, {
12251
12460
  userId,
12252
12461
  maxReviews: 1,
12253
- maxNew: 1
12462
+ maxNew: 1,
12463
+ knowledgeContext: opts.knowledgeContext || void 0
12254
12464
  });
12255
12465
  if (queue.items.length === 0) {
12256
12466
  jsonOut2({
@@ -12305,7 +12515,10 @@ bridgeCommand.command("get-review").description("Get next review card with promp
12305
12515
  resolvedContext = null;
12306
12516
  }
12307
12517
  }
12308
- const fullQueue = await buildReviewQueue(db, { userId });
12518
+ const fullQueue = await buildReviewQueue(db, {
12519
+ userId,
12520
+ knowledgeContext: opts.knowledgeContext || void 0
12521
+ });
12309
12522
  jsonOut2({
12310
12523
  userId,
12311
12524
  hasReview: true,
@@ -12579,6 +12792,10 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
12579
12792
  domain: data?.domain,
12580
12793
  title: data?.title ?? null
12581
12794
  });
12795
+ const ctxNames = parseKnowledgeContextNames(
12796
+ data?.knowledgeContexts ?? data?.knowledge_contexts
12797
+ );
12798
+ const assignedContexts = await resolveKnowledgeContexts(db, ctxNames);
12582
12799
  const token = await createToken(db, {
12583
12800
  slug: data?.slug,
12584
12801
  title: data?.title,
@@ -12590,6 +12807,9 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
12590
12807
  source_link: data?.source_link ?? null,
12591
12808
  question: data?.question ?? null
12592
12809
  });
12810
+ for (const context of assignedContexts) {
12811
+ await assignTokenToContext(db, token.id, context.id);
12812
+ }
12593
12813
  const card = await ensureCard(db, token.id, userId);
12594
12814
  try {
12595
12815
  await ensureTokenEmbeddings(db, { limit: 8 });
@@ -12597,7 +12817,14 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
12597
12817
  }
12598
12818
  jsonOut2({
12599
12819
  success: true,
12600
- token,
12820
+ token: {
12821
+ ...token,
12822
+ knowledgeContexts: assignedContexts.map((c) => ({
12823
+ name: c.name,
12824
+ label: c.label,
12825
+ language: c.language
12826
+ }))
12827
+ },
12601
12828
  card: {
12602
12829
  id: card.id,
12603
12830
  tokenId: card.token_id,
@@ -12657,6 +12884,22 @@ bridgeCommand.command("relevant-tokens").description("Find tokens relevant to a
12657
12884
  limit
12658
12885
  });
12659
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
+ }
12660
12903
  for (const t2 of results) {
12661
12904
  const card = await getCard(db, t2.id, userId);
12662
12905
  tokens.push({
@@ -12668,6 +12911,7 @@ bridgeCommand.command("relevant-tokens").description("Find tokens relevant to a
12668
12911
  bloom_level: t2.bloom_level,
12669
12912
  score: t2.score,
12670
12913
  similarity: t2.similarity,
12914
+ knowledgeContexts: contextMap.get(t2.id) ?? [],
12671
12915
  card: card ? {
12672
12916
  state: card.state,
12673
12917
  due_at: card.due_at,
@@ -13960,13 +14204,15 @@ bridgeCommand.command("list-tokens").description(
13960
14204
  "User ID (default: whoami) \u2014 when provided, includes personal card info"
13961
14205
  ).option("--domain <domain>", "Filter by exact domain").option(
13962
14206
  "--domain-prefix <prefix>",
13963
- "Filter by domain prefix (e.g. docuware-cops) \u2014 uses / separator for hierarchy"
13964
- ).action(async (opts) => {
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) => {
13965
14209
  await withDb2(async (db) => {
13966
14210
  const userId = opts.user ? await resolveUser(opts, db, { json: true }) : void 0;
13967
14211
  const listOpts = {};
13968
14212
  if (opts.domain) listOpts.domain = opts.domain;
13969
14213
  if (opts.domainPrefix) listOpts.domainPrefix = opts.domainPrefix;
14214
+ if (opts.knowledgeContext)
14215
+ listOpts.knowledgeContext = opts.knowledgeContext;
13970
14216
  const tokens = await listTokens(
13971
14217
  db,
13972
14218
  Object.keys(listOpts).length ? listOpts : void 0
@@ -13981,6 +14227,22 @@ bridgeCommand.command("list-tokens").description(
13981
14227
  ).all(...ids, userId);
13982
14228
  for (const c of cards) cardMap.set(c.token_id, c);
13983
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
+ }
13984
14246
  const out = tokens.map((t2) => {
13985
14247
  const c = cardMap.get(t2.id);
13986
14248
  return {
@@ -13991,6 +14253,7 @@ bridgeCommand.command("list-tokens").description(
13991
14253
  concept: t2.concept,
13992
14254
  domain: t2.domain,
13993
14255
  bloomLevel: t2.bloom_level,
14256
+ knowledgeContexts: contextMap.get(t2.id) ?? [],
13994
14257
  card: c ? {
13995
14258
  state: c.state,
13996
14259
  reps: c.reps,
@@ -14018,6 +14281,23 @@ bridgeCommand.command("get-neighborhood").description(
14018
14281
  jsonError(`Token not found: ${opts.focus}`);
14019
14282
  }
14020
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
+ }
14021
14301
  const mapToken = (nt) => ({
14022
14302
  id: nt.id,
14023
14303
  slug: nt.slug,
@@ -14026,6 +14306,7 @@ bridgeCommand.command("get-neighborhood").description(
14026
14306
  concept: nt.concept,
14027
14307
  domain: nt.domain,
14028
14308
  bloomLevel: nt.bloom_level,
14309
+ knowledgeContexts: contextMap.get(nt.id) ?? [],
14029
14310
  card: nt.card ? {
14030
14311
  state: nt.card.state,
14031
14312
  reps: nt.card.reps,
@@ -14044,23 +14325,62 @@ bridgeCommand.command("get-neighborhood").description(
14044
14325
  });
14045
14326
  });
14046
14327
  });
14047
- 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) => {
14048
14329
  await withDb2(async (db) => {
14049
14330
  const userId = await resolveUser(opts, db, { json: true });
14050
14331
  const cards = await listPersonalCards(db, userId, {
14051
14332
  query: opts.query,
14052
- 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
+ }))
14053
14362
  });
14054
- jsonOut2({ cards });
14055
14363
  });
14056
14364
  });
14057
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(
14058
14366
  "--mode <mode>",
14059
14367
  "Symbiosis mode: shadowing | copilot | autonomy | none",
14060
14368
  "none"
14061
- ).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) => {
14062
14378
  await withDb2(async (db) => {
14063
14379
  const userId = await resolveUser(opts, db, { json: true });
14380
+ const contextNames = parseKnowledgeContextNames(
14381
+ opts.knowledgeContext || []
14382
+ );
14383
+ const contexts = await resolveKnowledgeContexts(db, contextNames);
14064
14384
  const bloom = Number(opts.bloom);
14065
14385
  if (bloom < 1 || bloom > 5) {
14066
14386
  jsonError("bloom must be between 1 and 5");
@@ -14091,6 +14411,9 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
14091
14411
  source_link: opts.sourceLink || null,
14092
14412
  question
14093
14413
  });
14414
+ for (const context of contexts) {
14415
+ await assignTokenToContext(tx, createdToken.id, context.id);
14416
+ }
14094
14417
  const createdCard = await ensureCard(tx, createdToken.id, userId);
14095
14418
  return { token: createdToken, card: createdCard };
14096
14419
  });
@@ -14109,7 +14432,12 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
14109
14432
  sourceLink: token.source_link,
14110
14433
  question: token.question,
14111
14434
  createdAt: token.created_at,
14112
- 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
+ }))
14113
14441
  },
14114
14442
  card: {
14115
14443
  id: card.id,
@@ -14263,14 +14591,28 @@ bridgeCommand.command("personal-card-delete").description("Hard-delete a token a
14263
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(
14264
14592
  "--domain <domain>",
14265
14593
  "Default category/domain for imported cards"
14266
- ).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) => {
14267
14603
  await withDb2(async (db) => {
14268
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;
14269
14610
  const cards = await importCurriculumViaLLM(
14270
14611
  db,
14271
14612
  opts.text,
14272
14613
  opts.domain,
14273
- opts.source || null
14614
+ opts.source || null,
14615
+ { knowledgeContext: firstContext }
14274
14616
  );
14275
14617
  if (opts.preview) {
14276
14618
  jsonOut2({
@@ -14280,6 +14622,24 @@ bridgeCommand.command("personal-card-import-curriculum").description("Parse curr
14280
14622
  return;
14281
14623
  }
14282
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
+ }
14283
14643
  jsonOut2({
14284
14644
  success: true,
14285
14645
  createdCount: result.createdCount,
@@ -14418,7 +14778,7 @@ bridgeCommand.command("personal-source-import").description(
14418
14778
  } else {
14419
14779
  throw new Error(`Invalid source type: ${opts.type}`);
14420
14780
  }
14421
- const sourceId = ulid8();
14781
+ const sourceId = ulid9();
14422
14782
  await db.prepare(
14423
14783
  `INSERT INTO sources (id, type, uri, content)
14424
14784
  VALUES (?, ?, ?, ?)
@@ -14440,16 +14800,46 @@ bridgeCommand.command("personal-source-confirm-import").description(
14440
14800
  ).option("--user <id>", "User ID (default: whoami)").requiredOption("--sourceId <id>", "Source database ID").requiredOption(
14441
14801
  "--proposals <json>",
14442
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
+ []
14443
14811
  ).action(async (opts) => {
14444
14812
  await withDb2(async (db) => {
14445
14813
  const userId = await resolveUser(opts, db, { json: true });
14446
14814
  const proposals = JSON.parse(opts.proposals);
14815
+ const contextNames = parseKnowledgeContextNames(
14816
+ opts.knowledgeContext || []
14817
+ );
14818
+ const contexts = await resolveKnowledgeContexts(db, contextNames);
14447
14819
  const result = await confirmSourceImport(
14448
14820
  db,
14449
14821
  userId,
14450
14822
  opts.sourceId,
14451
14823
  proposals
14452
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
+ }
14453
14843
  jsonOut2({
14454
14844
  success: true,
14455
14845
  createdCount: result.createdCount,
@@ -14607,7 +14997,7 @@ bridgeCommand.command("curriculum-extract-topics").description(
14607
14997
  }
14608
14998
  }
14609
14999
  const pageCleanedText = cleanHtml(rawHtml);
14610
- const sourceId = ulid8();
15000
+ const sourceId = ulid9();
14611
15001
  await db.prepare(
14612
15002
  `INSERT INTO sources (id, type, uri, content)
14613
15003
  VALUES (?, 'web', ?, ?)
@@ -14656,6 +15046,71 @@ bridgeCommand.command("curriculum-set-last-selection").description("Persist the
14656
15046
  jsonOut2({ success: true });
14657
15047
  });
14658
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
+ });
14659
15114
  bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/stdout server").option("--stdin", "Use stdin/stdout for communication").action(async (_opts) => {
14660
15115
  isServeMode = true;
14661
15116
  const {
@@ -14790,10 +15245,16 @@ import { Command as Command3 } from "commander";
14790
15245
  var cardCommand = new Command3("card").description(
14791
15246
  "Manage spaced-repetition cards"
14792
15247
  );
14793
- 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) => {
14794
15249
  await withDb(async (db) => {
14795
15250
  const userId = await resolveUser(opts, db);
14796
- const dueCards = await getDueCards(db, userId, void 0, opts.domain);
15251
+ const dueCards = await getDueCards(
15252
+ db,
15253
+ userId,
15254
+ void 0,
15255
+ opts.domain,
15256
+ opts.knowledgeContext
15257
+ );
14797
15258
  if (opts.json) {
14798
15259
  console.log(JSON.stringify(dueCards, null, 2));
14799
15260
  return;
@@ -15141,6 +15602,48 @@ import { Command as Command5 } from "commander";
15141
15602
  function shouldApplyFixes(opts) {
15142
15603
  return opts.fix === true && opts.dryRun !== true;
15143
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
+ }
15144
15647
  async function confirmDeterministicChanges(opts, message) {
15145
15648
  if (opts.yes) return true;
15146
15649
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -15244,6 +15747,27 @@ function validatedTitleProposal(token, raw) {
15244
15747
  }
15245
15748
  return null;
15246
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
+ }
15247
15771
  var doctorTasks = [
15248
15772
  {
15249
15773
  name: "titles",
@@ -15268,9 +15792,11 @@ var doctorTasks = [
15268
15792
  let rejected = 0;
15269
15793
  for (let i = 0; i < needing.length; i++) {
15270
15794
  const item = needing[i];
15271
- process.stdout.write(
15272
- `\r [${i + 1}/${needing.length}] ${item.token.slug.slice(0, 40)}...`
15273
- );
15795
+ if (!opts.json) {
15796
+ process.stdout.write(
15797
+ `\r [${i + 1}/${needing.length}] ${item.token.slug.slice(0, 40)}...`
15798
+ );
15799
+ }
15274
15800
  let rawProposal = "";
15275
15801
  if (useLLM) {
15276
15802
  try {
@@ -15283,7 +15809,10 @@ var doctorTasks = [
15283
15809
  question: item.token.question,
15284
15810
  context: item.token.context
15285
15811
  },
15286
- { timeoutMs: opts.timeoutMs }
15812
+ {
15813
+ timeoutMs: opts.timeoutMs,
15814
+ knowledgeContext: opts.knowledgeContext
15815
+ }
15287
15816
  );
15288
15817
  rawProposal = gen.text;
15289
15818
  } catch (_e) {
@@ -15304,7 +15833,7 @@ var doctorTasks = [
15304
15833
  reason: item.reason
15305
15834
  });
15306
15835
  }
15307
- process.stdout.write("\n");
15836
+ if (!opts.json) process.stdout.write("\n");
15308
15837
  if (rejected > 0) {
15309
15838
  console.warn(
15310
15839
  `Skipped ${rejected} token(s) because no proposal passed title quality validation.`
@@ -15647,6 +16176,194 @@ Duplicates scan complete. Deprecated ${deprecatedCount} token(s).`
15647
16176
  `Domains rename complete. Renamed ${changes.length} domain(s).`
15648
16177
  );
15649
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
+ }
15650
16367
  }
15651
16368
  ];
15652
16369
  function parseDoctorTimeout(value) {
@@ -15665,30 +16382,128 @@ var doctorCommand = new Command5("doctor").description(
15665
16382
  "--timeout <ms>",
15666
16383
  "LLM timeout in ms per call (default: 20000)",
15667
16384
  "20000"
15668
- ).argument("[task]", "Specific task: titles, texts, duplicates, domains").action(async (taskName, opts) => {
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) => {
15669
16392
  await withDb(async (db) => {
15670
16393
  const fix = !!opts.fix;
15671
16394
  const dryRun = !fix || !!opts.dryRun;
15672
16395
  const yes = !!opts.yes;
15673
16396
  const noLlm = opts.llm === false;
15674
16397
  const timeoutMs = parseDoctorTimeout(opts.timeout);
16398
+ const knowledgeContext = opts.knowledgeContext;
16399
+ const json = !!opts.json;
15675
16400
  if (!taskName) {
15676
- console.log("Available doctor tasks:");
15677
- for (const t2 of doctorTasks) {
15678
- console.log(` ${t2.name} \u2014 ${t2.description}`);
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
+ }
15679
16436
  }
15680
- console.log("\nRun with a task name, e.g. `zam doctor titles --fix`");
15681
16437
  return;
15682
16438
  }
15683
16439
  const task = doctorTasks.find((t2) => t2.name === taskName);
15684
16440
  if (!task) {
15685
- console.error(`Unknown task: ${taskName}`);
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
+ }
15686
16451
  process.exit(1);
15687
16452
  }
15688
- console.log(
15689
- `Running doctor task: ${task.name} (fix=${fix}, dryRun=${dryRun}${noLlm ? ", noLlm=true" : ""})`
15690
- );
15691
- await task.run(db, { fix, dryRun, yes, noLlm, timeoutMs });
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);
15692
16507
  });
15693
16508
  });
15694
16509
 
@@ -16216,9 +17031,202 @@ var initCommand = new Command8("init").description("Launch the guided interactiv
16216
17031
  printLine();
16217
17032
  });
16218
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
+
16219
17227
  // src/cli/commands/learn.ts
16220
17228
  import { input as input5 } from "@inquirer/prompts";
16221
- import { Command as Command9 } from "commander";
17229
+ import { Command as Command10 } from "commander";
16222
17230
 
16223
17231
  // src/cli/learn-format.ts
16224
17232
  var BLOOM_VERBS3 = {
@@ -16562,7 +17570,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
16562
17570
  function isExitPrompt(err) {
16563
17571
  return err instanceof Error && err.name === "ExitPromptError";
16564
17572
  }
16565
- var learnCommand = new Command9("learn").description(
17573
+ var learnCommand = new Command10("learn").description(
16566
17574
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
16567
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) => {
16568
17576
  let db;
@@ -16808,8 +17816,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
16808
17816
  });
16809
17817
 
16810
17818
  // src/cli/commands/monitor.ts
16811
- import { Command as Command10 } from "commander";
16812
- var monitorCommand = new Command10("monitor").description(
17819
+ import { Command as Command11 } from "commander";
17820
+ var monitorCommand = new Command11("monitor").description(
16813
17821
  "Shell observation for real-time task monitoring"
16814
17822
  );
16815
17823
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -16991,8 +17999,8 @@ monitorCommand.command("open").description("Open a new monitored terminal window
16991
17999
  });
16992
18000
 
16993
18001
  // src/cli/commands/observer.ts
16994
- import { Command as Command11 } from "commander";
16995
- var observerCommand = new Command11("observer").description(
18002
+ import { Command as Command12 } from "commander";
18003
+ var observerCommand = new Command12("observer").description(
16996
18004
  "Configure what the UI observer may capture (Layer 2 policy)"
16997
18005
  );
16998
18006
  function applyObserverListChange(current, entry, op) {
@@ -17055,7 +18063,7 @@ observerCommand.command("revoke <process>").description("Remove a process from o
17055
18063
 
17056
18064
  // src/cli/commands/profile.ts
17057
18065
  import { dirname as dirname6, resolve as resolve7 } from "path";
17058
- import { Command as Command12 } from "commander";
18066
+ import { Command as Command13 } from "commander";
17059
18067
  var C2 = {
17060
18068
  reset: "\x1B[0m",
17061
18069
  bold: "\x1B[1m",
@@ -17072,7 +18080,7 @@ function render(profile) {
17072
18080
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
17073
18081
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
17074
18082
  }
17075
- var profileCommand = new Command12("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
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) => {
17076
18084
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
17077
18085
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
17078
18086
  process.exit(1);
@@ -17112,8 +18120,8 @@ var profileCommand = new Command12("profile").description("Show or change this m
17112
18120
 
17113
18121
  // src/cli/commands/provider.ts
17114
18122
  import { password as password2 } from "@inquirer/prompts";
17115
- import { Command as Command13 } from "commander";
17116
- var providerCommand = new Command13("provider").description(
18123
+ import { Command as Command14 } from "commander";
18124
+ var providerCommand = new Command14("provider").description(
17117
18125
  "Configure role-based AI providers (url/model/flavor/key, per role)"
17118
18126
  );
17119
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) => {
@@ -17348,16 +18356,33 @@ providerCommand.command("clear-key <ref>").description("Remove a stored API key"
17348
18356
  });
17349
18357
 
17350
18358
  // src/cli/commands/review.ts
17351
- import { Command as Command14 } from "commander";
17352
- var reviewCommand = new Command14("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
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) => {
17353
18364
  let db;
17354
18365
  try {
17355
18366
  db = await openDatabase();
17356
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
+ }
17357
18381
  const queue = await buildReviewQueue(db, {
17358
18382
  userId,
17359
18383
  maxNew: Number(opts.maxNew),
17360
- maxReviews: Number(opts.maxReviews)
18384
+ maxReviews: Number(opts.maxReviews),
18385
+ knowledgeContext: resolvedContext
17361
18386
  });
17362
18387
  if (queue.items.length === 0) {
17363
18388
  console.log("No cards due for review. You're all caught up!");
@@ -17366,6 +18391,9 @@ var reviewCommand = new Command14("review").description("Start an interactive re
17366
18391
  }
17367
18392
  console.log(`
17368
18393
  Review session: ${queue.items.length} card(s)`);
18394
+ if (resolvedContext) {
18395
+ console.log(` Context: ${resolvedContext}`);
18396
+ }
17369
18397
  console.log(
17370
18398
  ` New: ${queue.newCount} Review: ${queue.reviewCount} Relearn: ${queue.relearnCount}`
17371
18399
  );
@@ -17463,8 +18491,8 @@ ${"\u2550".repeat(50)}`);
17463
18491
  // src/cli/commands/session.ts
17464
18492
  import { readFileSync as readFileSync13 } from "fs";
17465
18493
  import { input as input6, select as select2 } from "@inquirer/prompts";
17466
- import { Command as Command15 } from "commander";
17467
- var sessionCommand = new Command15("session").description(
18494
+ import { Command as Command16 } from "commander";
18495
+ var sessionCommand = new Command16("session").description(
17468
18496
  "Manage learning sessions"
17469
18497
  );
17470
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(
@@ -17843,8 +18871,8 @@ sessionCommand.command("end").description("End a session and show summary").requ
17843
18871
 
17844
18872
  // src/cli/commands/settings.ts
17845
18873
  import { existsSync as existsSync21 } from "fs";
17846
- import { Command as Command16 } from "commander";
17847
- var settingsCommand = new Command16("settings").description(
18874
+ import { Command as Command17 } from "commander";
18875
+ var settingsCommand = new Command17("settings").description(
17848
18876
  "Manage user settings"
17849
18877
  );
17850
18878
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -18020,7 +19048,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
18020
19048
 
18021
19049
  // src/cli/commands/setup.ts
18022
19050
  import { resolve as resolve8 } from "path";
18023
- import { Command as Command17 } from "commander";
19051
+ import { Command as Command18 } from "commander";
18024
19052
  function formatDatabaseInitTarget(target) {
18025
19053
  switch (target.kind) {
18026
19054
  case "local":
@@ -18061,7 +19089,7 @@ async function activateMachineProviderConfig(db) {
18061
19089
  ` activate ${providerCount} machine-local provider(s) from ~/.zam/config.json`
18062
19090
  );
18063
19091
  }
18064
- var setupCommand = new Command17("setup").description(
19092
+ var setupCommand = new Command18("setup").description(
18065
19093
  "Link ZAM skill directories into this workspace and initialize the database"
18066
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(
18067
19095
  "--agents <list>",
@@ -18115,8 +19143,8 @@ var setupCommand = new Command17("setup").description(
18115
19143
  );
18116
19144
 
18117
19145
  // src/cli/commands/skill.ts
18118
- import { Command as Command18 } from "commander";
18119
- var skillCommand = new Command18("skill").description(
19146
+ import { Command as Command19 } from "commander";
19147
+ var skillCommand = new Command19("skill").description(
18120
19148
  "Manage agent skill entries (task recipes)"
18121
19149
  );
18122
19150
  skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
@@ -18203,7 +19231,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
18203
19231
  // src/cli/commands/snapshot.ts
18204
19232
  import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
18205
19233
  import { dirname as dirname7, join as join21 } from "path";
18206
- import { Command as Command19 } from "commander";
19234
+ import { Command as Command20 } from "commander";
18207
19235
  function defaultOutName() {
18208
19236
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
18209
19237
  return `zam-snapshot-${stamp}.sql`;
@@ -18217,7 +19245,7 @@ function summarize(tables) {
18217
19245
  }
18218
19246
  return { total, nonEmpty };
18219
19247
  }
18220
- var exportCmd = new Command19("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
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) => {
18221
19249
  let db;
18222
19250
  try {
18223
19251
  db = await openDatabaseWithSync({ initialize: true });
@@ -18247,7 +19275,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
18247
19275
  process.exit(1);
18248
19276
  }
18249
19277
  });
18250
- var importCmd = new Command19("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
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) => {
18251
19279
  let db;
18252
19280
  try {
18253
19281
  if (!existsSync22(file)) {
@@ -18270,7 +19298,7 @@ var importCmd = new Command19("import").description("Restore a snapshot into the
18270
19298
  process.exit(1);
18271
19299
  }
18272
19300
  });
18273
- var verifyCmd = new Command19("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) => {
18274
19302
  try {
18275
19303
  if (!existsSync22(file)) {
18276
19304
  console.error(`Error: Snapshot file not found: ${file}`);
@@ -18288,11 +19316,11 @@ var verifyCmd = new Command19("verify").description("Check a snapshot's manifest
18288
19316
  process.exit(1);
18289
19317
  }
18290
19318
  });
18291
- var snapshotCommand = new Command19("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);
18292
19320
 
18293
19321
  // src/cli/commands/stats.ts
18294
- import { Command as Command20 } from "commander";
18295
- var statsCommand = new Command20("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
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) => {
18296
19324
  await withDb(async (db) => {
18297
19325
  const userId = await resolveUser(opts, db);
18298
19326
  const stats = await getUserStats(db, userId);
@@ -18328,11 +19356,19 @@ var statsCommand = new Command20("stats").description("Show learning dashboard f
18328
19356
  });
18329
19357
 
18330
19358
  // src/cli/commands/token.ts
18331
- import { Command as Command21 } from "commander";
18332
- var tokenCommand = new Command21("token").description(
19359
+ import { Command as Command22 } from "commander";
19360
+ var tokenCommand = new Command22("token").description(
18333
19361
  "Manage knowledge tokens"
18334
19362
  );
18335
- tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--user <id>", "Owner of the personal card (default: whoami)").option("--no-card", "Register the token only; do not create a personal card").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
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) => {
18336
19372
  await withDb(async (db) => {
18337
19373
  let question = opts.question || null;
18338
19374
  if (!question) {
@@ -18342,6 +19378,10 @@ tokenCommand.command("register").description("Register a new knowledge token").r
18342
19378
  opts.domain
18343
19379
  );
18344
19380
  }
19381
+ const assignedContexts = await resolveOperationKnowledgeContexts(
19382
+ db,
19383
+ opts.knowledgeContext || []
19384
+ );
18345
19385
  const possibleDuplicates = await findPossibleDuplicates(db, {
18346
19386
  concept: opts.concept,
18347
19387
  question,
@@ -18355,6 +19395,9 @@ tokenCommand.command("register").description("Register a new knowledge token").r
18355
19395
  source_link: opts.sourceLink || null,
18356
19396
  question
18357
19397
  });
19398
+ for (const context of assignedContexts) {
19399
+ await assignTokenToContext(db, token.id, context.id);
19400
+ }
18358
19401
  let cardUserId = null;
18359
19402
  if (opts.card !== false) {
18360
19403
  cardUserId = opts.user ?? await getSetting(db, "user.id");
@@ -18373,7 +19416,12 @@ tokenCommand.command("register").description("Register a new knowledge token").r
18373
19416
  {
18374
19417
  ...token,
18375
19418
  card: cardUserId ? { userId: cardUserId } : null,
18376
- 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
+ }))
18377
19425
  },
18378
19426
  null,
18379
19427
  2
@@ -18389,6 +19437,11 @@ tokenCommand.command("register").description("Register a new knowledge token").r
18389
19437
  console.log(` Domain: ${token.domain || "(none)"}`);
18390
19438
  console.log(` Bloom: ${token.bloom_level}`);
18391
19439
  console.log(` Question: ${token.question}`);
19440
+ if (assignedContexts.length > 0) {
19441
+ console.log(
19442
+ ` Contexts: ${assignedContexts.map((c) => c.name).join(", ")}`
19443
+ );
19444
+ }
18392
19445
  if (token.source_link) {
18393
19446
  console.log(` Source: ${token.source_link}`);
18394
19447
  }
@@ -18495,11 +19548,15 @@ tokenCommand.command("find").description("Fuzzy search for tokens").requiredOpti
18495
19548
  console.log("\n(Use slug for CLI commands -- slugs are technical IDs)");
18496
19549
  });
18497
19550
  });
18498
- tokenCommand.command("list").description("List all tokens").option("--domain <domain>", "Filter by domain").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
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) => {
18499
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;
18500
19557
  const tokens = await listTokens(
18501
19558
  db,
18502
- opts.domain ? { domain: opts.domain } : void 0
19559
+ Object.keys(filterOpts).length ? filterOpts : void 0
18503
19560
  );
18504
19561
  if (opts.quiet) return;
18505
19562
  if (opts.json) {
@@ -18797,7 +19854,7 @@ import { existsSync as existsSync23 } from "fs";
18797
19854
  import { homedir as homedir12 } from "os";
18798
19855
  import { dirname as dirname8, join as join22 } from "path";
18799
19856
  import { fileURLToPath as fileURLToPath3 } from "url";
18800
- import { Command as Command22 } from "commander";
19857
+ import { Command as Command23 } from "commander";
18801
19858
  var C3 = {
18802
19859
  reset: "\x1B[0m",
18803
19860
  red: "\x1B[31m",
@@ -18977,7 +20034,7 @@ function createShortcuts(appPath, repoRoot) {
18977
20034
  console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
18978
20035
  }
18979
20036
  }
18980
- var uiCommand = new Command22("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
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(
18981
20038
  "--build",
18982
20039
  "Build the native installer for your OS (needs Rust, one-time)"
18983
20040
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
@@ -19085,7 +20142,7 @@ import { existsSync as existsSync24, readFileSync as readFileSync15, realpathSyn
19085
20142
  import { dirname as dirname9, join as join23 } from "path";
19086
20143
  import { fileURLToPath as fileURLToPath4 } from "url";
19087
20144
  import { confirm as confirm3 } from "@inquirer/prompts";
19088
- import { Command as Command23 } from "commander";
20145
+ import { Command as Command24 } from "commander";
19089
20146
  var GITHUB_REPO = "zam-os/zam";
19090
20147
  var CHANNELS = [
19091
20148
  "developer",
@@ -19163,7 +20220,7 @@ function render2(decision) {
19163
20220
  );
19164
20221
  }
19165
20222
  }
19166
- var checkCmd = new Command23("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(
19167
20224
  "--latest <version>",
19168
20225
  "Compare against this version instead of fetching"
19169
20226
  ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
@@ -19330,7 +20387,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
19330
20387
  process.exit(1);
19331
20388
  }
19332
20389
  }
19333
- var updateCommand = new Command23("update").description(
20390
+ var updateCommand = new Command24("update").description(
19334
20391
  "Update ZAM to the latest release (use `update check` to only check)"
19335
20392
  ).option("-y, --yes", "Apply without confirmation").option(
19336
20393
  "--force",
@@ -19340,8 +20397,8 @@ var updateCommand = new Command23("update").description(
19340
20397
  }).addCommand(checkCmd);
19341
20398
 
19342
20399
  // src/cli/commands/whoami.ts
19343
- import { Command as Command24 } from "commander";
19344
- var whoamiCommand = new Command24("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
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) => {
19345
20402
  await withDb(async (db) => {
19346
20403
  if (opts.set) {
19347
20404
  await setSetting(db, "user.id", opts.set);
@@ -19382,7 +20439,7 @@ import { existsSync as existsSync25, writeFileSync as writeFileSync11 } from "fs
19382
20439
  import { homedir as homedir13 } from "os";
19383
20440
  import { join as join24, resolve as resolve9 } from "path";
19384
20441
  import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
19385
- import { Command as Command25 } from "commander";
20442
+ import { Command as Command26 } from "commander";
19386
20443
  function runGit2(cwd, args) {
19387
20444
  try {
19388
20445
  return execFileSync4("git", args, {
@@ -19400,7 +20457,7 @@ function ghRepoCreateArgs(repoName, repoVisibility) {
19400
20457
  function gitRemoteArgs(githubUrl, hasOrigin) {
19401
20458
  return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
19402
20459
  }
19403
- var workspaceCommand = new Command25("workspace").description(
20460
+ var workspaceCommand = new Command26("workspace").description(
19404
20461
  "Manage your ZAM learning workspace"
19405
20462
  );
19406
20463
  var WORKSPACE_KINDS = [
@@ -19755,13 +20812,14 @@ var __dirname = dirname10(fileURLToPath5(import.meta.url));
19755
20812
  var pkg = JSON.parse(
19756
20813
  readFileSync16(join25(__dirname, "..", "..", "package.json"), "utf-8")
19757
20814
  );
19758
- var program = new Command26();
20815
+ var program = new Command27();
19759
20816
  program.name("zam").description(
19760
20817
  "The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
19761
20818
  ).version(pkg.version);
19762
20819
  program.addCommand(initCommand);
19763
20820
  program.addCommand(setupCommand);
19764
20821
  program.addCommand(tokenCommand);
20822
+ program.addCommand(knowledgeContextCommand);
19765
20823
  program.addCommand(doctorCommand);
19766
20824
  program.addCommand(cardCommand);
19767
20825
  program.addCommand(sessionCommand);