zam-core 0.5.3 → 0.6.1

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { readFileSync as readFileSync15 } from "fs";
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
7
  import { Command as Command25 } from "commander";
@@ -629,6 +629,24 @@ CREATE TABLE IF NOT EXISTS session_syntheses (
629
629
  PRIMARY KEY (session_id, token_id)
630
630
  );
631
631
 
632
+ -- Sources: textbook files, web links, or scan paths
633
+ CREATE TABLE IF NOT EXISTS sources (
634
+ id TEXT PRIMARY KEY,
635
+ type TEXT NOT NULL CHECK (type IN ('file', 'web', 'scan')),
636
+ uri TEXT NOT NULL UNIQUE,
637
+ content TEXT,
638
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
639
+ );
640
+
641
+ -- Token sources: mapping between tokens and their sources
642
+ CREATE TABLE IF NOT EXISTS token_sources (
643
+ token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
644
+ source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
645
+ excerpt TEXT NOT NULL DEFAULT '',
646
+ page_number TEXT,
647
+ PRIMARY KEY (token_id, source_id)
648
+ );
649
+
632
650
  -- User configuration
633
651
  CREATE TABLE IF NOT EXISTS user_config (
634
652
  key TEXT PRIMARY KEY,
@@ -956,6 +974,24 @@ async function runMigrations(db) {
956
974
  PRIMARY KEY (session_id, token_id)
957
975
  )
958
976
  `);
977
+ await db.exec(`
978
+ CREATE TABLE IF NOT EXISTS sources (
979
+ id TEXT PRIMARY KEY,
980
+ type TEXT NOT NULL CHECK (type IN ('file', 'web', 'scan')),
981
+ uri TEXT NOT NULL UNIQUE,
982
+ content TEXT,
983
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
984
+ )
985
+ `);
986
+ await db.exec(`
987
+ CREATE TABLE IF NOT EXISTS token_sources (
988
+ token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
989
+ source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
990
+ excerpt TEXT NOT NULL DEFAULT '',
991
+ page_number TEXT,
992
+ PRIMARY KEY (token_id, source_id)
993
+ )
994
+ `);
959
995
  }
960
996
 
961
997
  // src/kernel/db/snapshot.ts
@@ -1650,6 +1686,425 @@ async function listTokens(db, options) {
1650
1686
  "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
1651
1687
  ).all();
1652
1688
  }
1689
+ function slugify(text) {
1690
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1691
+ }
1692
+ async function generateTokenSlug(db, domain, concept, question) {
1693
+ const baseText = question && question.trim().length > 0 ? question : concept;
1694
+ const cleanDomain = slugify(domain || "");
1695
+ const cleanBase = slugify(baseText);
1696
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
1697
+ if (baseSlug.length > 60) {
1698
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
1699
+ }
1700
+ if (!baseSlug) {
1701
+ baseSlug = "token";
1702
+ }
1703
+ let slug = baseSlug;
1704
+ let counter = 1;
1705
+ while (true) {
1706
+ const existing = await db.prepare("SELECT id FROM tokens WHERE slug = ?").get(slug);
1707
+ if (!existing) {
1708
+ return slug;
1709
+ }
1710
+ const suffix = `-${counter}`;
1711
+ slug = baseSlug.slice(0, 60 - suffix.length).replace(/-$/, "") + suffix;
1712
+ counter++;
1713
+ }
1714
+ }
1715
+ async function listPersonalCards(db, userId, options) {
1716
+ let sql = `
1717
+ SELECT
1718
+ t.id AS tokenId,
1719
+ t.slug,
1720
+ t.concept,
1721
+ t.domain,
1722
+ t.bloom_level AS bloomLevel,
1723
+ t.context,
1724
+ t.symbiosis_mode AS symbiosisMode,
1725
+ COALESCE(
1726
+ t.source_link,
1727
+ (
1728
+ SELECT s.uri
1729
+ FROM token_sources ts
1730
+ INNER JOIN sources s ON s.id = ts.source_id
1731
+ WHERE ts.token_id = t.id
1732
+ ORDER BY s.created_at DESC, s.id DESC
1733
+ LIMIT 1
1734
+ )
1735
+ ) AS sourceLink,
1736
+ t.question,
1737
+ t.created_at AS createdAt,
1738
+ t.updated_at AS updatedAt,
1739
+ c.id AS cardId,
1740
+ c.state,
1741
+ c.due_at AS dueAt,
1742
+ c.stability,
1743
+ c.difficulty,
1744
+ c.reps,
1745
+ c.lapses,
1746
+ c.elapsed_days AS elapsedDays,
1747
+ c.scheduled_days AS scheduledDays,
1748
+ c.blocked
1749
+ FROM tokens t
1750
+ INNER JOIN cards c ON c.token_id = t.id AND c.user_id = ?
1751
+ WHERE t.deprecated_at IS NULL
1752
+ `;
1753
+ const values = [userId];
1754
+ if (options?.domain) {
1755
+ sql += " AND t.domain = ?";
1756
+ values.push(options.domain);
1757
+ }
1758
+ if (options?.query) {
1759
+ const terms = options.query.toLowerCase().split(/\s+/).filter(Boolean);
1760
+ for (const term of terms) {
1761
+ sql += ` AND (lower(t.slug) LIKE ? OR lower(t.concept) LIKE ? OR lower(t.domain) LIKE ? OR lower(t.question) LIKE ?)`;
1762
+ const pattern = `%${term}%`;
1763
+ values.push(pattern, pattern, pattern, pattern);
1764
+ }
1765
+ }
1766
+ sql += " ORDER BY t.created_at DESC";
1767
+ const rows = await db.prepare(sql).all(...values);
1768
+ return rows;
1769
+ }
1770
+ async function importCurriculumCards(db, userId, cards) {
1771
+ let createdCount = 0;
1772
+ let ensuredCount = 0;
1773
+ await db.transaction(async (tx) => {
1774
+ for (const card of cards) {
1775
+ const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
1776
+ if (bloom < 1 || bloom > 5) {
1777
+ throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1778
+ }
1779
+ let symbiosisMode = null;
1780
+ if (card.symbiosis_mode) {
1781
+ if (!["shadowing", "copilot", "autonomy", "none"].includes(
1782
+ card.symbiosis_mode
1783
+ )) {
1784
+ throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
1785
+ }
1786
+ symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
1787
+ }
1788
+ const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
1789
+ const cleanDomain = slugify(card.domain || "");
1790
+ const cleanBase = slugify(baseText);
1791
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
1792
+ if (baseSlug.length > 60) {
1793
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
1794
+ }
1795
+ if (!baseSlug) {
1796
+ baseSlug = "token";
1797
+ }
1798
+ let token = await getTokenBySlug(tx, baseSlug);
1799
+ if (!token) {
1800
+ const finalSlug = await generateTokenSlug(
1801
+ tx,
1802
+ card.domain,
1803
+ card.concept,
1804
+ card.question
1805
+ );
1806
+ token = await createToken(tx, {
1807
+ slug: finalSlug,
1808
+ concept: card.concept,
1809
+ domain: card.domain,
1810
+ bloom_level: bloom,
1811
+ context: card.context || "",
1812
+ symbiosis_mode: symbiosisMode,
1813
+ source_link: card.source_link || null,
1814
+ question: card.question || null
1815
+ });
1816
+ createdCount++;
1817
+ }
1818
+ const existingCard = await getCard(tx, token.id, userId);
1819
+ if (!existingCard) {
1820
+ await ensureCard(tx, token.id, userId);
1821
+ ensuredCount++;
1822
+ }
1823
+ }
1824
+ });
1825
+ return { createdCount, ensuredCount };
1826
+ }
1827
+ async function confirmCardSplit(db, userId, originalSlug, action, originalQuestion, originalConcept, proposals) {
1828
+ if (action !== "block" && action !== "remove") {
1829
+ throw new Error(`Invalid split action: ${action}`);
1830
+ }
1831
+ if (proposals.length < 2 || proposals.length > 4) {
1832
+ throw new Error("A card split requires between 2 and 4 proposals");
1833
+ }
1834
+ const originalToken = await getTokenBySlug(db, originalSlug);
1835
+ if (!originalToken) {
1836
+ throw new Error(`Original token not found: ${originalSlug}`);
1837
+ }
1838
+ let createdCount = 0;
1839
+ let ensuredCount = 0;
1840
+ await db.transaction(async (tx) => {
1841
+ const originalCard = await getCard(tx, originalToken.id, userId);
1842
+ if (!originalCard) {
1843
+ throw new Error(
1844
+ `Card not found for token ${originalSlug} and user ${userId}`
1845
+ );
1846
+ }
1847
+ const proposalTokens = [];
1848
+ for (const card of proposals) {
1849
+ const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
1850
+ if (bloom < 1 || bloom > 5) {
1851
+ throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1852
+ }
1853
+ let symbiosisMode = null;
1854
+ if (card.symbiosis_mode) {
1855
+ if (!["shadowing", "copilot", "autonomy", "none"].includes(
1856
+ card.symbiosis_mode
1857
+ )) {
1858
+ throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
1859
+ }
1860
+ symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
1861
+ }
1862
+ const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
1863
+ const cleanDomain = slugify(card.domain || "");
1864
+ const cleanBase = slugify(baseText);
1865
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
1866
+ if (baseSlug.length > 60) {
1867
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
1868
+ }
1869
+ if (!baseSlug) {
1870
+ baseSlug = "token";
1871
+ }
1872
+ let token = await getTokenBySlug(tx, baseSlug);
1873
+ if (!token) {
1874
+ const finalSlug = await generateTokenSlug(
1875
+ tx,
1876
+ card.domain,
1877
+ card.concept,
1878
+ card.question
1879
+ );
1880
+ token = await createToken(tx, {
1881
+ slug: finalSlug,
1882
+ concept: card.concept,
1883
+ domain: card.domain,
1884
+ bloom_level: bloom,
1885
+ context: card.context || "",
1886
+ symbiosis_mode: symbiosisMode,
1887
+ source_link: card.source_link || originalToken.source_link || null,
1888
+ question: card.question || null
1889
+ });
1890
+ createdCount++;
1891
+ }
1892
+ if (token.id === originalToken.id) {
1893
+ throw new Error("A token cannot be a prerequisite of itself");
1894
+ }
1895
+ await assertPrerequisiteDoesNotCreateCycle(
1896
+ tx,
1897
+ originalToken.id,
1898
+ token.id
1899
+ );
1900
+ proposalTokens.push(token);
1901
+ const existingCard = await getCard(tx, token.id, userId);
1902
+ if (!existingCard) {
1903
+ await ensureCard(tx, token.id, userId);
1904
+ ensuredCount++;
1905
+ }
1906
+ }
1907
+ if (action === "block") {
1908
+ await tx.prepare(
1909
+ "UPDATE tokens SET question = ?, concept = ?, updated_at = ? WHERE id = ?"
1910
+ ).run(
1911
+ originalQuestion || null,
1912
+ originalConcept,
1913
+ (/* @__PURE__ */ new Date()).toISOString(),
1914
+ originalToken.id
1915
+ );
1916
+ for (const propToken of proposalTokens) {
1917
+ await tx.prepare(
1918
+ "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
1919
+ ).run(originalToken.id, propToken.id);
1920
+ }
1921
+ await tx.prepare(
1922
+ "UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
1923
+ ).run(originalToken.id, userId);
1924
+ for (const propToken of proposalTokens) {
1925
+ const card = await ensureCard(tx, propToken.id, userId);
1926
+ if (card.blocked === 1) {
1927
+ const prereqOfPrereq = await tx.prepare(
1928
+ "SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
1929
+ ).get(propToken.id);
1930
+ if (prereqOfPrereq.n === 0) {
1931
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1932
+ await tx.prepare("UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?").run(now, card.id);
1933
+ }
1934
+ }
1935
+ }
1936
+ } else if (action === "remove") {
1937
+ await deleteCardForUser(tx, originalToken.id, userId);
1938
+ }
1939
+ });
1940
+ return { createdCount, ensuredCount };
1941
+ }
1942
+ async function confirmFoundations(db, userId, originalSlug, proposals) {
1943
+ const originalToken = await getTokenBySlug(db, originalSlug);
1944
+ if (!originalToken) {
1945
+ throw new Error(`Original token not found: ${originalSlug}`);
1946
+ }
1947
+ let createdCount = 0;
1948
+ let linkedCount = 0;
1949
+ await db.transaction(async (tx) => {
1950
+ const originalCard = await getCard(tx, originalToken.id, userId);
1951
+ if (!originalCard) {
1952
+ throw new Error(
1953
+ `Card not found for token ${originalSlug} and user ${userId}`
1954
+ );
1955
+ }
1956
+ for (const card of proposals) {
1957
+ let targetTokenId;
1958
+ if (card.exists && card.slug) {
1959
+ const existingToken = await getTokenBySlug(tx, card.slug);
1960
+ if (!existingToken) {
1961
+ throw new Error(`Prerequisite token not found by slug: ${card.slug}`);
1962
+ }
1963
+ targetTokenId = existingToken.id;
1964
+ const existingCard = await getCard(tx, existingToken.id, userId);
1965
+ if (!existingCard) {
1966
+ await ensureCard(tx, existingToken.id, userId);
1967
+ }
1968
+ linkedCount++;
1969
+ } else {
1970
+ const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
1971
+ if (bloom < 1 || bloom > 5) {
1972
+ throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1973
+ }
1974
+ let symbiosisMode = null;
1975
+ if (card.symbiosis_mode) {
1976
+ if (!["shadowing", "copilot", "autonomy", "none"].includes(
1977
+ card.symbiosis_mode
1978
+ )) {
1979
+ throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
1980
+ }
1981
+ symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
1982
+ }
1983
+ const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
1984
+ const cleanDomain = slugify(card.domain || "");
1985
+ const cleanBase = slugify(baseText);
1986
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
1987
+ if (baseSlug.length > 60) {
1988
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
1989
+ }
1990
+ if (!baseSlug) {
1991
+ baseSlug = "token";
1992
+ }
1993
+ let token = await getTokenBySlug(tx, baseSlug);
1994
+ if (!token) {
1995
+ const finalSlug = await generateTokenSlug(
1996
+ tx,
1997
+ card.domain,
1998
+ card.concept,
1999
+ card.question
2000
+ );
2001
+ token = await createToken(tx, {
2002
+ slug: finalSlug,
2003
+ concept: card.concept,
2004
+ domain: card.domain,
2005
+ bloom_level: bloom,
2006
+ context: card.context || "",
2007
+ symbiosis_mode: symbiosisMode,
2008
+ source_link: card.source_link || originalToken.source_link || null,
2009
+ question: card.question || null
2010
+ });
2011
+ createdCount++;
2012
+ }
2013
+ targetTokenId = token.id;
2014
+ const existingCard = await getCard(tx, token.id, userId);
2015
+ if (!existingCard) {
2016
+ await ensureCard(tx, token.id, userId);
2017
+ }
2018
+ }
2019
+ if (targetTokenId === originalToken.id) {
2020
+ throw new Error("A token cannot be a prerequisite of itself");
2021
+ }
2022
+ await assertPrerequisiteDoesNotCreateCycle(
2023
+ tx,
2024
+ originalToken.id,
2025
+ targetTokenId
2026
+ );
2027
+ await tx.prepare(
2028
+ "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
2029
+ ).run(originalToken.id, targetTokenId);
2030
+ }
2031
+ });
2032
+ return { createdCount, linkedCount };
2033
+ }
2034
+ async function confirmSourceImport(db, userId, sourceId, proposals) {
2035
+ let createdCount = 0;
2036
+ let linkedCount = 0;
2037
+ await db.transaction(async (tx) => {
2038
+ const source = await tx.prepare("SELECT id FROM sources WHERE id = ?").get(sourceId);
2039
+ if (!source) {
2040
+ throw new Error(`Source not found: ${sourceId}`);
2041
+ }
2042
+ for (const card of proposals) {
2043
+ const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
2044
+ const cleanDomain = slugify(card.domain || "");
2045
+ const cleanBase = slugify(baseText);
2046
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
2047
+ if (baseSlug.length > 60) {
2048
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
2049
+ }
2050
+ if (!baseSlug) {
2051
+ baseSlug = "token";
2052
+ }
2053
+ let token = await getTokenBySlug(tx, baseSlug);
2054
+ if (!token) {
2055
+ const finalSlug = await generateTokenSlug(
2056
+ tx,
2057
+ card.domain,
2058
+ card.concept,
2059
+ card.question
2060
+ );
2061
+ const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
2062
+ let symbiosisMode = null;
2063
+ if (card.symbiosis_mode && card.symbiosis_mode !== "none") {
2064
+ symbiosisMode = card.symbiosis_mode;
2065
+ }
2066
+ token = await createToken(tx, {
2067
+ slug: finalSlug,
2068
+ concept: card.concept,
2069
+ domain: card.domain,
2070
+ bloom_level: bloom,
2071
+ context: card.excerpt || "",
2072
+ symbiosis_mode: symbiosisMode,
2073
+ question: card.question || null
2074
+ });
2075
+ createdCount++;
2076
+ } else {
2077
+ linkedCount++;
2078
+ }
2079
+ const existingCard = await getCard(tx, token.id, userId);
2080
+ if (!existingCard) {
2081
+ await ensureCard(tx, token.id, userId);
2082
+ }
2083
+ await tx.prepare(
2084
+ `INSERT INTO token_sources (token_id, source_id, excerpt, page_number)
2085
+ VALUES (?, ?, ?, ?)
2086
+ ON CONFLICT(token_id, source_id) DO UPDATE SET
2087
+ excerpt = excluded.excerpt,
2088
+ page_number = excluded.page_number`
2089
+ ).run(token.id, sourceId, card.excerpt || "", card.page_number || null);
2090
+ }
2091
+ });
2092
+ return { createdCount, linkedCount };
2093
+ }
2094
+ async function assertPrerequisiteDoesNotCreateCycle(db, tokenId, prerequisiteId) {
2095
+ const cycleCheck = await db.prepare(
2096
+ `WITH RECURSIVE dependents(token_id) AS (
2097
+ SELECT token_id FROM prerequisites WHERE requires_id = ?
2098
+ UNION
2099
+ SELECT p.token_id FROM prerequisites p
2100
+ JOIN dependents d ON p.requires_id = d.token_id
2101
+ )
2102
+ SELECT COUNT(*) as n FROM dependents WHERE token_id = ?`
2103
+ ).get(tokenId, prerequisiteId);
2104
+ if (cycleCheck.n > 0) {
2105
+ throw new Error("Cannot add prerequisite: would create a cycle");
2106
+ }
2107
+ }
1653
2108
 
1654
2109
  // src/kernel/models/prerequisite.ts
1655
2110
  async function buildAncestorMap(db) {
@@ -4574,12 +5029,7 @@ function installFastFlowLM() {
4574
5029
  }
4575
5030
  }
4576
5031
  function installOllama() {
4577
- const isMac = process.platform === "darwin";
4578
- const isWin = process.platform === "win32";
4579
- const hasOllama = hasCommand("ollama") || isMac && existsSync9("/Applications/Ollama.app") || isWin && existsSync9(
4580
- join10(homedir7(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
4581
- );
4582
- if (hasOllama) {
5032
+ if (isOllamaInstalled()) {
4583
5033
  return { success: true, message: "Ollama is already installed." };
4584
5034
  }
4585
5035
  if (process.platform === "darwin") {
@@ -4634,19 +5084,23 @@ function installOllama() {
4634
5084
  }
4635
5085
  }
4636
5086
  }
4637
- function resolveOllamaCommand() {
4638
- if (hasCommand("ollama")) return "ollama";
4639
- const candidates = process.platform === "win32" ? [
4640
- join10(
4641
- homedir7(),
4642
- "AppData",
4643
- "Local",
4644
- "Programs",
4645
- "Ollama",
4646
- "ollama.exe"
4647
- )
4648
- ] : process.platform === "darwin" ? ["/Applications/Ollama.app/Contents/Resources/ollama"] : [];
4649
- return candidates.find((candidate) => existsSync9(candidate));
5087
+ function resolveOllamaCommand(options = {}) {
5088
+ const platform = options.platform ?? process.platform;
5089
+ const homeDir = options.homeDir ?? homedir7();
5090
+ const commandAvailable = options.commandAvailable ?? hasCommand;
5091
+ const pathExists2 = options.pathExists ?? existsSync9;
5092
+ if (commandAvailable("ollama")) return "ollama";
5093
+ const candidates = platform === "win32" ? [join10(homeDir, "AppData", "Local", "Programs", "Ollama", "ollama.exe")] : platform === "darwin" ? [
5094
+ "/opt/homebrew/bin/ollama",
5095
+ "/usr/local/bin/ollama",
5096
+ "/Applications/Ollama.app/Contents/Resources/ollama"
5097
+ ] : ["/usr/local/bin/ollama", "/usr/bin/ollama"];
5098
+ return candidates.find((candidate) => pathExists2(candidate));
5099
+ }
5100
+ function isOllamaInstalled(options = {}) {
5101
+ const platform = options.platform ?? process.platform;
5102
+ const pathExists2 = options.pathExists ?? existsSync9;
5103
+ return resolveOllamaCommand(options) !== void 0 || platform === "darwin" && pathExists2("/Applications/Ollama.app");
4650
5104
  }
4651
5105
  function prepareLocalModel(runner, model) {
4652
5106
  if (runner === "fastflowlm") {
@@ -5390,14 +5844,19 @@ var agentCommand = new Command("agent").description("Provision and inspect the a
5390
5844
  // src/cli/commands/bridge.ts
5391
5845
  import { execFileSync as execFileSync3 } from "child_process";
5392
5846
  import { randomBytes as randomBytes2 } from "crypto";
5393
- import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
5847
+ import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync12, rmSync as rmSync3 } from "fs";
5394
5848
  import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
5395
5849
  import { join as join18, resolve as resolve4 } from "path";
5396
5850
  import { Command as Command2 } from "commander";
5851
+ import { ulid as ulid8 } from "ulid";
5852
+
5853
+ // src/cli/adapters/source-reader.ts
5854
+ import dns from "dns";
5855
+ import fs from "fs";
5397
5856
 
5398
5857
  // src/cli/llm/client.ts
5399
5858
  import { spawn as spawn2 } from "child_process";
5400
- import { existsSync as existsSync14 } from "fs";
5859
+ import { existsSync as existsSync14, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
5401
5860
  var DEFAULT_LLM_URL = "http://localhost:8000/v1";
5402
5861
  var DEFAULT_LLM_MAX_TOKENS = 1e4;
5403
5862
  var RECALL_QUESTION_MAX_OUTPUT_TOKENS = 400;
@@ -5570,10 +6029,21 @@ async function getLegacyRoleConfig(db, role, enabled) {
5570
6029
  function assertChatCompletions(cfg) {
5571
6030
  if (cfg.apiFlavor !== "chat-completions") {
5572
6031
  throw new Error(
5573
- `This role is configured for a "${cfg.apiFlavor}" provider, which is not supported here yet. Use a chat-completions provider for the recall role.`
6032
+ `This role is configured for a "${cfg.apiFlavor}" provider, which is not supported here yet. Use a chat-completions provider for this role.`
5574
6033
  );
5575
6034
  }
5576
6035
  }
6036
+ async function getVisionConfig(db) {
6037
+ const p = await getProviderForRole(db, "vision");
6038
+ return {
6039
+ enabled: p.enabled,
6040
+ url: p.url,
6041
+ model: p.model,
6042
+ apiKey: p.apiKey,
6043
+ locale: p.locale,
6044
+ maxFrames: p.maxFrames
6045
+ };
6046
+ }
5577
6047
  var LANGUAGE_NAMES = {
5578
6048
  en: "English",
5579
6049
  de: "German",
@@ -5721,6 +6191,317 @@ Evaluation:`;
5721
6191
  providerName: endpoint.providerName
5722
6192
  };
5723
6193
  }
6194
+ var MAX_IMPORT_TEXT_CHARS = 2e5;
6195
+ var VALID_GENERATED_MODES = /* @__PURE__ */ new Set(["shadowing", "copilot", "autonomy"]);
6196
+ function parseGeneratedCardArray(responseText, label, limits) {
6197
+ const startIdx = responseText.indexOf("[");
6198
+ const endIdx = responseText.lastIndexOf("]");
6199
+ if (startIdx === -1 || endIdx === -1 || startIdx > endIdx) {
6200
+ throw new Error(`Invalid ${label} response: JSON array brackets not found`);
6201
+ }
6202
+ let parsed;
6203
+ try {
6204
+ parsed = JSON.parse(responseText.substring(startIdx, endIdx + 1));
6205
+ } catch {
6206
+ throw new Error(`Invalid ${label} response: malformed JSON`);
6207
+ }
6208
+ if (!Array.isArray(parsed)) {
6209
+ throw new Error(`Invalid ${label} response: expected a JSON array`);
6210
+ }
6211
+ if (parsed.length < limits.min || parsed.length > limits.max) {
6212
+ throw new Error(
6213
+ `Invalid ${label} response: expected ${limits.min}-${limits.max} cards, got ${parsed.length}`
6214
+ );
6215
+ }
6216
+ return parsed.map((value, index) => {
6217
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6218
+ throw new Error(
6219
+ `Invalid ${label} card at index ${index}: expected an object`
6220
+ );
6221
+ }
6222
+ const card = value;
6223
+ for (const field of ["question", "concept", "domain", "context"]) {
6224
+ if (typeof card[field] !== "string" || card[field].trim().length === 0) {
6225
+ throw new Error(
6226
+ `Invalid ${label} card at index ${index}: ${field} must be a non-empty string`
6227
+ );
6228
+ }
6229
+ }
6230
+ if (typeof card.bloom_level !== "number" || !Number.isInteger(card.bloom_level) || card.bloom_level < 1 || card.bloom_level > 5) {
6231
+ throw new Error(
6232
+ `Invalid ${label} card at index ${index}: bloom_level must be an integer from 1 to 5`
6233
+ );
6234
+ }
6235
+ if (typeof card.symbiosis_mode !== "string" || !VALID_GENERATED_MODES.has(card.symbiosis_mode)) {
6236
+ throw new Error(
6237
+ `Invalid ${label} card at index ${index}: invalid symbiosis_mode`
6238
+ );
6239
+ }
6240
+ return {
6241
+ question: card.question,
6242
+ concept: card.concept,
6243
+ domain: card.domain,
6244
+ context: card.context,
6245
+ bloom_level: card.bloom_level,
6246
+ symbiosis_mode: card.symbiosis_mode,
6247
+ source_link: null
6248
+ };
6249
+ });
6250
+ }
6251
+ async function importCurriculumViaLLM(db, text, targetCategory, sourceUrl) {
6252
+ if (text.length > MAX_IMPORT_TEXT_CHARS) {
6253
+ throw new Error(
6254
+ `Curriculum text exceeds the ${MAX_IMPORT_TEXT_CHARS.toLocaleString()} character limit`
6255
+ );
6256
+ }
6257
+ const cfg = await getProviderForRole(db, "text");
6258
+ const endpoint = await resolveUsableTextEndpoint(db);
6259
+ const langName = LANGUAGE_NAMES[cfg.locale] || "English";
6260
+ const systemPrompt = `You are ZAM, a highly precise agentic curriculum parser.
6261
+ Your task is to analyze curriculum objectives, syllabus requirements, or textbook notes, and extract atomic facts, concepts, or skills as structured learning cards in ${langName}.
6262
+
6263
+ For each extracted learning card, you MUST generate:
6264
+ 1. "question": A clear, concise active-recall question testing the concept. The question must not reveal the answer itself.
6265
+ 2. "concept": The reference answer, core fact, or target conceptual explanation.
6266
+ 3. "domain": The category of the card. Use "${targetCategory}" as the default, but you may refine it if a specific sub-topic is evident.
6267
+ 4. "context": The exact sentence or short excerpt from the source text that this card is based on.
6268
+ 5. "bloom_level": Initial Bloom cognitive level (1 = Remember, 2 = Understand, 3 = Apply, 4 = Analyze, 5 = Synthesize).
6269
+ 6. "symbiosis_mode": ZAM agent symbiosis level: "shadowing" (reading/monitoring), "copilot" (interactive helper), or "autonomy" (autonomous tasks).
6270
+
6271
+ Guidelines:
6272
+ - Break complex requirements into multiple separate, atomic cards.
6273
+ - Keep questions focused on one fact.
6274
+ - Do not repeat the same concept in multiple cards.
6275
+ - Test the subject matter directly. Never ask what learners "must know", what
6276
+ a curriculum "requires", or merely restate a competency as a question.
6277
+ - Stay within the scope of the supplied curriculum. A reference answer may add
6278
+ the definition, formula, or minimal worked example needed to answer a direct
6279
+ subject-matter question, but it must not introduce named methods, terminology,
6280
+ or adjacent topics that the curriculum passage does not mention.
6281
+ - Keep every reference answer concise, mathematically/factually correct, and
6282
+ focused on the competency being tested.
6283
+ - Set "context" to the exact sentence or bullet that supports that specific
6284
+ card. Never reuse an unrelated passage merely because it comes from the same
6285
+ curriculum section.
6286
+ - Output ONLY a raw valid JSON array of objects. Do NOT wrap the JSON in markdown code blocks, HTML, or include any preamble, headers, or conversational filler.`;
6287
+ const userPrompt = `Curriculum Text to Parse:
6288
+ ${text}
6289
+
6290
+ Target Category: ${targetCategory}
6291
+ ${sourceUrl ? `Source Reference Link: ${sourceUrl}` : ""}
6292
+
6293
+ JSON Array Output:`;
6294
+ const res = await fetchWithInteractiveTimeout(
6295
+ `${endpoint.url}/chat/completions`,
6296
+ {
6297
+ method: "POST",
6298
+ headers: {
6299
+ "Content-Type": "application/json",
6300
+ Authorization: `Bearer ${endpoint.apiKey}`
6301
+ },
6302
+ body: JSON.stringify({
6303
+ model: endpoint.model,
6304
+ messages: [
6305
+ { role: "system", content: systemPrompt },
6306
+ { role: "user", content: userPrompt }
6307
+ ],
6308
+ temperature: 0.1,
6309
+ max_tokens: DEFAULT_LLM_MAX_TOKENS
6310
+ }),
6311
+ locale: cfg.locale
6312
+ }
6313
+ );
6314
+ const responseText = await readChatContent(res, "LLM curriculum import");
6315
+ return parseGeneratedCardArray(responseText, "curriculum import", {
6316
+ min: 0,
6317
+ max: 200
6318
+ }).map((card) => ({ ...card, source_link: sourceUrl || null }));
6319
+ }
6320
+ async function generateSplitProposalsViaLLM(db, token) {
6321
+ const cfg = await getProviderForRole(db, "text");
6322
+ const endpoint = await resolveUsableTextEndpoint(db);
6323
+ const langName = LANGUAGE_NAMES[cfg.locale] || "English";
6324
+ const systemPrompt = `You are ZAM, a highly precise agentic learning assistant.
6325
+ Your task is to analyze a learning card that is too broad or covers multiple ideas, and split it into 2 to 4 atomic, focused proposal cards in ${langName}.
6326
+
6327
+ The input card details are:
6328
+ - Question: ${token.question || "N/A"}
6329
+ - Core Concept: ${token.concept}
6330
+ - Domain: ${token.domain}
6331
+ - Excerpt Context: ${token.context || "N/A"}
6332
+
6333
+ For each split proposal card, you MUST generate:
6334
+ 1. "question": A clear, concise active-recall question testing a single focused fact or concept. The question must not reveal the answer itself.
6335
+ 2. "concept": The reference answer or core fact.
6336
+ 3. "domain": The category of the card (default to "${token.domain}").
6337
+ 4. "context": The context excerpt from the original card's context or a derivation of it.
6338
+ 5. "bloom_level": Bloom taxonomy level (1 to 5).
6339
+ 6. "symbiosis_mode": Symbiosis mode ("shadowing", "copilot", or "autonomy").
6340
+
6341
+ Guidelines:
6342
+ - Make sure each card is completely atomic (covers exactly one concept).
6343
+ - Do not repeat the same concept across cards.
6344
+ - Output ONLY a raw valid JSON array of objects. Do NOT wrap the JSON in markdown code blocks, HTML, or include any conversational filler.`;
6345
+ const userPrompt = `Split the broad card details above into 2 to 4 atomic cards.
6346
+
6347
+ JSON Array Output:`;
6348
+ const res = await fetchWithInteractiveTimeout(
6349
+ `${endpoint.url}/chat/completions`,
6350
+ {
6351
+ method: "POST",
6352
+ headers: {
6353
+ "Content-Type": "application/json",
6354
+ Authorization: `Bearer ${endpoint.apiKey}`
6355
+ },
6356
+ body: JSON.stringify({
6357
+ model: endpoint.model,
6358
+ messages: [
6359
+ { role: "system", content: systemPrompt },
6360
+ { role: "user", content: userPrompt }
6361
+ ],
6362
+ temperature: 0.2,
6363
+ max_tokens: DEFAULT_LLM_MAX_TOKENS
6364
+ }),
6365
+ locale: cfg.locale
6366
+ }
6367
+ );
6368
+ const responseText = await readChatContent(res, "LLM card split proposals");
6369
+ return parseGeneratedCardArray(responseText, "card split", {
6370
+ min: 2,
6371
+ max: 4
6372
+ }).map((card) => ({ ...card, source_link: token.source_link || null }));
6373
+ }
6374
+ async function generateFoundationsProposalsViaLLM(db, token) {
6375
+ const cfg = await getProviderForRole(db, "text");
6376
+ const endpoint = await resolveUsableTextEndpoint(db);
6377
+ const langName = LANGUAGE_NAMES[cfg.locale] || "English";
6378
+ const systemPrompt = `You are ZAM, a highly precise agentic learning assistant.
6379
+ Your task is to analyze a learning card and propose 2 to 4 atomic, foundational prerequisite concepts (foundations) that a learner must master *before* studying this card, in ${langName}.
6380
+
6381
+ The current card details are:
6382
+ - Question: ${token.question || "N/A"}
6383
+ - Core Concept: ${token.concept}
6384
+ - Domain: ${token.domain}
6385
+ - Excerpt Context: ${token.context || "N/A"}
6386
+
6387
+ For each proposed foundational card, you MUST generate:
6388
+ 1. "question": A clear, concise active-recall question testing a single foundational fact or prerequisite concept.
6389
+ 2. "concept": The reference answer or core fact.
6390
+ 3. "domain": The category of the card (default to "${token.domain}").
6391
+ 4. "context": The context excerpt explaining where this prerequisite fits in the learning hierarchy.
6392
+ 5. "bloom_level": Bloom taxonomy level (typically 1 or 2 for foundational facts).
6393
+ 6. "symbiosis_mode": Symbiosis mode ("shadowing", "copilot", or "autonomy").
6394
+
6395
+ Guidelines:
6396
+ - Recommend only highly relevant and necessary prerequisites.
6397
+ - Keep each proposed card atomic and focused on one concept.
6398
+ - Output ONLY a raw valid JSON array of objects. Do NOT wrap the JSON in markdown code blocks, HTML, or include any conversational filler.`;
6399
+ const userPrompt = `Suggest 2 to 4 foundational prerequisite cards for the card above.
6400
+
6401
+ JSON Array Output:`;
6402
+ const res = await fetchWithInteractiveTimeout(
6403
+ `${endpoint.url}/chat/completions`,
6404
+ {
6405
+ method: "POST",
6406
+ headers: {
6407
+ "Content-Type": "application/json",
6408
+ Authorization: `Bearer ${endpoint.apiKey}`
6409
+ },
6410
+ body: JSON.stringify({
6411
+ model: endpoint.model,
6412
+ messages: [
6413
+ { role: "system", content: systemPrompt },
6414
+ { role: "user", content: userPrompt }
6415
+ ],
6416
+ temperature: 0.2,
6417
+ max_tokens: DEFAULT_LLM_MAX_TOKENS
6418
+ }),
6419
+ locale: cfg.locale
6420
+ }
6421
+ );
6422
+ const responseText = await readChatContent(
6423
+ res,
6424
+ "LLM card foundations proposals"
6425
+ );
6426
+ return parseGeneratedCardArray(responseText, "foundation proposal", {
6427
+ min: 2,
6428
+ max: 4
6429
+ }).map((card) => ({ ...card, source_link: token.source_link || null }));
6430
+ }
6431
+ async function extractTextFromScanViaLLM(db, imagePath) {
6432
+ const p = await getProviderForRole(db, "vision");
6433
+ if (!p.enabled) {
6434
+ throw new Error(
6435
+ "Vision role is not enabled in settings (llm.vision.enabled)"
6436
+ );
6437
+ }
6438
+ if (!existsSync14(imagePath)) {
6439
+ throw new Error(`Scan file not found: ${imagePath}`);
6440
+ }
6441
+ const stat = statSync2(imagePath);
6442
+ if (!stat.isFile()) {
6443
+ throw new Error(`Scan path is not a file: ${imagePath}`);
6444
+ }
6445
+ if (stat.size > 10 * 1024 * 1024) {
6446
+ throw new Error("Scan file exceeds 10MB limit");
6447
+ }
6448
+ const ext = imagePath.split(".").pop()?.toLowerCase();
6449
+ const mimeByExtension = {
6450
+ jpeg: "image/jpeg",
6451
+ jpg: "image/jpeg",
6452
+ png: "image/png",
6453
+ webp: "image/webp"
6454
+ };
6455
+ const mime = ext ? mimeByExtension[ext] : void 0;
6456
+ if (!mime) {
6457
+ throw new Error("Unsupported scan format; use PNG, JPEG, or WebP");
6458
+ }
6459
+ const imageBytes = readFileSync9(imagePath);
6460
+ const dataUrl = `data:${mime};base64,${imageBytes.toString("base64")}`;
6461
+ const visionEndpoint = await getVisionConfig(db);
6462
+ const langName = LANGUAGE_NAMES[p.locale] || "English";
6463
+ const systemPrompt = "You are ZAM's OCR processor. Extract all visible text from the image exactly as written. Output only the extracted text without commentary, formatting, or prefixes.";
6464
+ const res = await fetchWithInteractiveTimeout(
6465
+ `${visionEndpoint.url}/chat/completions`,
6466
+ {
6467
+ method: "POST",
6468
+ headers: {
6469
+ "Content-Type": "application/json",
6470
+ Authorization: `Bearer ${visionEndpoint.apiKey || DEFAULT_LLM_API_KEY}`
6471
+ },
6472
+ body: JSON.stringify({
6473
+ model: visionEndpoint.model,
6474
+ messages: [
6475
+ { role: "system", content: systemPrompt },
6476
+ {
6477
+ role: "user",
6478
+ content: [
6479
+ {
6480
+ type: "text",
6481
+ text: `Extract all text from this scan in ${langName}:`
6482
+ },
6483
+ {
6484
+ type: "image_url",
6485
+ image_url: { url: dataUrl }
6486
+ }
6487
+ ]
6488
+ }
6489
+ ],
6490
+ temperature: 0,
6491
+ max_tokens: DEFAULT_LLM_MAX_TOKENS
6492
+ }),
6493
+ locale: p.locale
6494
+ }
6495
+ );
6496
+ if (!res.ok) {
6497
+ throw new Error(`LLM vision OCR request failed with status ${res.status}`);
6498
+ }
6499
+ const responseText = await readChatContent(
6500
+ res,
6501
+ "LLM scan OCR text extraction"
6502
+ );
6503
+ return responseText.trim();
6504
+ }
5724
6505
  async function translateQuestionViaLLM(db, question) {
5725
6506
  const cfg = await getProviderForRole(db, "recall");
5726
6507
  const endpoint = await resolveUsableRecallEndpoint(db);
@@ -5841,6 +6622,21 @@ async function resolveUsableRecallEndpoint(db) {
5841
6622
  };
5842
6623
  return selected.endpoint;
5843
6624
  }
6625
+ async function resolveUsableTextEndpoint(db) {
6626
+ const cfg = await getProviderForRole(db, "text");
6627
+ if (!cfg.enabled) {
6628
+ throw new Error(
6629
+ "Text LLM integration is disabled in settings (llm.enabled)"
6630
+ );
6631
+ }
6632
+ assertChatCompletions(cfg);
6633
+ const chain = await checkProviderChain(cfg);
6634
+ const selected = chain.firstUsable;
6635
+ if (!selected || !isEndpointUsable(selected)) {
6636
+ throw new Error("No text LLM endpoint is online");
6637
+ }
6638
+ return selected.endpoint;
6639
+ }
5844
6640
  async function prepareRecallChain(db, opts) {
5845
6641
  const cfg = await getProviderForRole(db, "recall");
5846
6642
  const fail = (reason, partial = {}) => ({
@@ -6337,9 +7133,145 @@ async function ensureHighQualityQuestion(db, token) {
6337
7133
  return null;
6338
7134
  }
6339
7135
 
7136
+ // src/cli/adapters/source-reader.ts
7137
+ var MAX_SOURCE_BYTES = 2 * 1024 * 1024;
7138
+ var MAX_REDIRECTS = 5;
7139
+ function isPrivateOrReservedIp(address) {
7140
+ const ip = address.toLowerCase().split("%", 1)[0];
7141
+ const mappedV4 = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
7142
+ const ipv4 = mappedV4 ?? (ip.includes(".") ? ip : null);
7143
+ if (ipv4) {
7144
+ const parts = ipv4.split(".").map(Number);
7145
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
7146
+ return true;
7147
+ }
7148
+ const [a, b] = parts;
7149
+ return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 || a === 192 && b === 0 && parts[2] === 2 || a === 198 && (b === 18 || b === 19 || b === 51) || a === 203 && b === 0 && parts[2] === 113 || a >= 224;
7150
+ }
7151
+ return ip === "::" || ip === "::1" || ip.startsWith("fc") || ip.startsWith("fd") || /^fe[89ab]/.test(ip) || ip.startsWith("ff") || ip.startsWith("2001:db8:");
7152
+ }
7153
+ function cleanHtml(html) {
7154
+ let text = html.replace(
7155
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
7156
+ " "
7157
+ );
7158
+ text = text.replace(/<[^>]+>/g, " ");
7159
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
7160
+ return text.replace(/\s+/g, " ").trim();
7161
+ }
7162
+ async function isSafeUrl(urlString) {
7163
+ try {
7164
+ const url = new URL(urlString);
7165
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
7166
+ return false;
7167
+ }
7168
+ if (url.username || url.password) return false;
7169
+ const hostname = url.hostname.toLowerCase().replace(/\.$/, "");
7170
+ if (hostname === "localhost" || hostname.endsWith(".localhost"))
7171
+ return false;
7172
+ const results = await dns.promises.lookup(hostname, {
7173
+ all: true,
7174
+ verbatim: true
7175
+ });
7176
+ return results.length > 0 && results.every((result) => !isPrivateOrReservedIp(result.address));
7177
+ } catch {
7178
+ return false;
7179
+ }
7180
+ }
7181
+ async function readLocalFile(filepath) {
7182
+ if (!fs.existsSync(filepath)) {
7183
+ throw new Error(`File not found: ${filepath}`);
7184
+ }
7185
+ const stat = fs.statSync(filepath);
7186
+ if (!stat.isFile()) {
7187
+ throw new Error(`Not a file: ${filepath}`);
7188
+ }
7189
+ if (stat.size > MAX_SOURCE_BYTES) {
7190
+ throw new Error("File exceeds 2MB limit");
7191
+ }
7192
+ return fs.readFileSync(filepath, "utf-8");
7193
+ }
7194
+ async function readWebLink(url) {
7195
+ const controller = new AbortController();
7196
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
7197
+ try {
7198
+ let currentUrl = new URL(url);
7199
+ for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
7200
+ if (!await isSafeUrl(currentUrl.href)) {
7201
+ throw new Error(
7202
+ `Access denied to unsafe target URL: ${currentUrl.href}`
7203
+ );
7204
+ }
7205
+ const res = await fetch(currentUrl, {
7206
+ redirect: "manual",
7207
+ signal: controller.signal,
7208
+ headers: {
7209
+ "User-Agent": "ZAM-Content-Studio/0.6.1"
7210
+ }
7211
+ });
7212
+ if (res.status >= 300 && res.status < 400) {
7213
+ const location = res.headers.get("location");
7214
+ if (!location) {
7215
+ throw new Error("Web server returned a redirect without a location");
7216
+ }
7217
+ if (redirects === MAX_REDIRECTS) {
7218
+ throw new Error(`Web request exceeded ${MAX_REDIRECTS} redirects`);
7219
+ }
7220
+ currentUrl = new URL(location, currentUrl);
7221
+ continue;
7222
+ }
7223
+ if (!res.ok) {
7224
+ throw new Error(`Web server responded with status ${res.status}`);
7225
+ }
7226
+ const contentType = res.headers.get("content-type") || "";
7227
+ if (!contentType.includes("text/html") && !contentType.includes("text/plain") && !contentType.includes("application/xhtml+xml") && !contentType.includes("text/xml")) {
7228
+ throw new Error(`Unsupported content type: ${contentType}`);
7229
+ }
7230
+ const declaredLength = Number(res.headers.get("content-length"));
7231
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_SOURCE_BYTES) {
7232
+ throw new Error("Response body exceeds 2MB limit");
7233
+ }
7234
+ const text = await readResponseBodyWithLimit(res, MAX_SOURCE_BYTES);
7235
+ if (contentType.includes("text/html") || contentType.includes("application/xhtml+xml")) {
7236
+ return cleanHtml(text);
7237
+ }
7238
+ return text;
7239
+ }
7240
+ throw new Error("Web request failed");
7241
+ } catch (err) {
7242
+ if (err instanceof Error && err.name === "AbortError") {
7243
+ throw new Error("Connection request timed out after 10 seconds");
7244
+ }
7245
+ throw err;
7246
+ } finally {
7247
+ clearTimeout(timeoutId);
7248
+ }
7249
+ }
7250
+ async function readResponseBodyWithLimit(response, maxBytes) {
7251
+ if (!response.body) return "";
7252
+ const reader = response.body.getReader();
7253
+ const decoder = new TextDecoder();
7254
+ let totalBytes = 0;
7255
+ let text = "";
7256
+ while (true) {
7257
+ const { done, value } = await reader.read();
7258
+ if (done) break;
7259
+ totalBytes += value.byteLength;
7260
+ if (totalBytes > maxBytes) {
7261
+ await reader.cancel();
7262
+ throw new Error("Response body exceeds 2MB limit");
7263
+ }
7264
+ text += decoder.decode(value, { stream: true });
7265
+ }
7266
+ return text + decoder.decode();
7267
+ }
7268
+ async function readImageOCR(db, imagePath) {
7269
+ return extractTextFromScanViaLLM(db, imagePath);
7270
+ }
7271
+
6340
7272
  // src/cli/llm/vision.ts
6341
7273
  import { randomBytes } from "crypto";
6342
- import { readFileSync as readFileSync9 } from "fs";
7274
+ import { readFileSync as readFileSync10 } from "fs";
6343
7275
  import { tmpdir as tmpdir2 } from "os";
6344
7276
  import { basename as basename3, join as join14 } from "path";
6345
7277
  var LANGUAGE_NAMES2 = {
@@ -6412,7 +7344,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
6412
7344
  }
6413
7345
  }
6414
7346
  for (const file of sampledFiles) {
6415
- const bytes = readFileSync9(join14(tempDir, file));
7347
+ const bytes = readFileSync10(join14(tempDir, file));
6416
7348
  imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
6417
7349
  }
6418
7350
  } finally {
@@ -6422,7 +7354,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
6422
7354
  }
6423
7355
  }
6424
7356
  } else {
6425
- const imageBytes = readFileSync9(input8.imagePath);
7357
+ const imageBytes = readFileSync10(input8.imagePath);
6426
7358
  const ext = input8.imagePath.split(".").pop()?.toLowerCase();
6427
7359
  const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
6428
7360
  imageUrls.push(`data:${mime};base64,${imageBytes.toString("base64")}`);
@@ -6890,7 +7822,7 @@ import {
6890
7822
  existsSync as existsSync15,
6891
7823
  lstatSync,
6892
7824
  mkdirSync as mkdirSync8,
6893
- readFileSync as readFileSync10,
7825
+ readFileSync as readFileSync11,
6894
7826
  realpathSync,
6895
7827
  rmSync as rmSync2,
6896
7828
  symlinkSync,
@@ -6978,7 +7910,7 @@ function isZamSkillCopy(destinationDir) {
6978
7910
  if (!lstatSync(destinationDir).isDirectory()) return false;
6979
7911
  const skillFile = join15(destinationDir, "SKILL.md");
6980
7912
  if (!existsSync15(skillFile)) return false;
6981
- return /^name:\s*zam\s*$/m.test(readFileSync10(skillFile, "utf8"));
7913
+ return /^name:\s*zam\s*$/m.test(readFileSync11(skillFile, "utf8"));
6982
7914
  } catch {
6983
7915
  return false;
6984
7916
  }
@@ -7111,7 +8043,7 @@ ${ZAM_BLOCK_END}`;
7111
8043
  }
7112
8044
  return "write";
7113
8045
  }
7114
- const existing = readFileSync10(dest, "utf8");
8046
+ const existing = readFileSync11(dest, "utf8");
7115
8047
  if (existing.includes(block)) return "skip";
7116
8048
  const start = existing.indexOf(ZAM_BLOCK_START);
7117
8049
  const end = existing.indexOf(ZAM_BLOCK_END);
@@ -8646,7 +9578,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
8646
9578
  return;
8647
9579
  }
8648
9580
  }
8649
- const imageBytes = readFileSync11(outputPath);
9581
+ const imageBytes = readFileSync12(outputPath);
8650
9582
  const base64 = imageBytes.toString("base64");
8651
9583
  jsonOut2({
8652
9584
  sessionId: opts.session ?? null,
@@ -8768,7 +9700,7 @@ bridgeCommand.command("stop-recording").description(
8768
9700
  }
8769
9701
  const sessionId = opts.session;
8770
9702
  const statePath = join18(tmpdir3(), `zam-recording-${sessionId}.json`);
8771
- const { existsSync: existsSync26, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
9703
+ const { existsSync: existsSync26, readFileSync: readFileSync17, rmSync: rmSync4 } = await import("fs");
8772
9704
  if (!existsSync26(statePath)) {
8773
9705
  jsonOut2({
8774
9706
  sessionId,
@@ -8777,7 +9709,7 @@ bridgeCommand.command("stop-recording").description(
8777
9709
  });
8778
9710
  return;
8779
9711
  }
8780
- const state = JSON.parse(readFileSync16(statePath, "utf8"));
9712
+ const state = JSON.parse(readFileSync17(statePath, "utf8"));
8781
9713
  const { pid, outputPath } = state;
8782
9714
  try {
8783
9715
  process.kill(pid, "SIGINT");
@@ -9043,7 +9975,7 @@ bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for
9043
9975
  bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
9044
9976
  const profile = getSystemProfile();
9045
9977
  const flmInstalled = hasCommand("flm") || existsSync17("C:\\Program Files\\flm\\flm.exe");
9046
- const ollamaInstalled = hasCommand("ollama");
9978
+ const ollamaInstalled = isOllamaInstalled();
9047
9979
  const runners = [
9048
9980
  { id: "flm", label: "FastFlowLM", installed: flmInstalled },
9049
9981
  { id: "ollama", label: "Ollama", installed: ollamaInstalled },
@@ -9291,6 +10223,388 @@ bridgeCommand.command("get-neighborhood").description(
9291
10223
  });
9292
10224
  });
9293
10225
  });
10226
+ 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) => {
10227
+ await withDb2(async (db) => {
10228
+ const userId = await resolveUser(opts, db, { json: true });
10229
+ const cards = await listPersonalCards(db, userId, {
10230
+ query: opts.query,
10231
+ domain: opts.domain
10232
+ });
10233
+ jsonOut2({ cards });
10234
+ });
10235
+ });
10236
+ bridgeCommand.command("personal-card-create").description("Atomically create a token and its personal card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--concept <concept>", "Concept description / answer").option("--domain <domain>", "Knowledge category / domain", "").option("--question <question>", "Question prompt for recall").option("--source-link <link>", "Source file path or reference URL").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option(
10237
+ "--mode <mode>",
10238
+ "Symbiosis mode: shadowing | copilot | autonomy | none",
10239
+ "none"
10240
+ ).option("--context <context>", "Context description", "").action(async (opts) => {
10241
+ await withDb2(async (db) => {
10242
+ const userId = await resolveUser(opts, db, { json: true });
10243
+ const bloom = Number(opts.bloom);
10244
+ if (bloom < 1 || bloom > 5) {
10245
+ jsonError("bloom must be between 1 and 5");
10246
+ }
10247
+ const mode = opts.mode === "none" ? null : opts.mode;
10248
+ if (mode && !["shadowing", "copilot", "autonomy"].includes(mode)) {
10249
+ jsonError(`Invalid mode: ${opts.mode}`);
10250
+ }
10251
+ const slug = await generateTokenSlug(
10252
+ db,
10253
+ opts.domain,
10254
+ opts.concept,
10255
+ opts.question
10256
+ );
10257
+ let question = opts.question || null;
10258
+ if (!question) {
10259
+ question = generateConceptFreeCue(bloom, slug, opts.domain);
10260
+ }
10261
+ const { token, card } = await db.transaction(async (tx) => {
10262
+ const createdToken = await createToken(tx, {
10263
+ slug,
10264
+ concept: opts.concept,
10265
+ domain: opts.domain,
10266
+ bloom_level: bloom,
10267
+ context: opts.context,
10268
+ symbiosis_mode: mode,
10269
+ source_link: opts.sourceLink || null,
10270
+ question
10271
+ });
10272
+ const createdCard = await ensureCard(tx, createdToken.id, userId);
10273
+ return { token: createdToken, card: createdCard };
10274
+ });
10275
+ jsonOut2({
10276
+ success: true,
10277
+ token: {
10278
+ id: token.id,
10279
+ slug: token.slug,
10280
+ concept: token.concept,
10281
+ domain: token.domain,
10282
+ bloomLevel: token.bloom_level,
10283
+ context: token.context,
10284
+ symbiosisMode: token.symbiosis_mode,
10285
+ sourceLink: token.source_link,
10286
+ question: token.question,
10287
+ createdAt: token.created_at,
10288
+ updatedAt: token.updated_at
10289
+ },
10290
+ card: {
10291
+ id: card.id,
10292
+ tokenId: card.token_id,
10293
+ userId: card.user_id,
10294
+ state: card.state,
10295
+ dueAt: card.due_at,
10296
+ blocked: card.blocked
10297
+ }
10298
+ });
10299
+ });
10300
+ });
10301
+ bridgeCommand.command("personal-card-update").description("Update the mutable token fields of a personal card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--slug <slug>", "Token slug to update").option("--concept <concept>", "Updated concept text").option("--domain <domain>", "Updated domain / category").option("--bloom <level>", "Updated Bloom taxonomy level (1-5)").option("--context <context>", "Updated context").option(
10302
+ "--mode <mode>",
10303
+ "Updated symbiosis mode: shadowing | copilot | autonomy | none"
10304
+ ).option("--source-link <link>", "Updated source link").option("--question <question>", "Updated question text").action(async (opts) => {
10305
+ await withDb2(async (db) => {
10306
+ const userId = await resolveUser(opts, db, { json: true });
10307
+ const updates = {};
10308
+ if (opts.concept !== void 0) updates.concept = opts.concept;
10309
+ if (opts.domain !== void 0) updates.domain = opts.domain;
10310
+ if (opts.bloom !== void 0) {
10311
+ const bloom = Number(opts.bloom);
10312
+ if (bloom < 1 || bloom > 5) {
10313
+ jsonError("bloom must be between 1 and 5");
10314
+ }
10315
+ updates.bloom_level = bloom;
10316
+ }
10317
+ if (opts.context !== void 0) updates.context = opts.context;
10318
+ if (opts.sourceLink !== void 0) {
10319
+ updates.source_link = opts.sourceLink === "" ? null : opts.sourceLink;
10320
+ }
10321
+ if (opts.question !== void 0) {
10322
+ updates.question = opts.question === "" ? null : opts.question;
10323
+ }
10324
+ if (opts.mode !== void 0) {
10325
+ const validModes = ["shadowing", "copilot", "autonomy", "none"];
10326
+ if (!validModes.includes(opts.mode)) {
10327
+ jsonError(`Invalid mode: ${opts.mode}`);
10328
+ }
10329
+ updates.symbiosis_mode = opts.mode === "none" ? null : opts.mode;
10330
+ }
10331
+ const token = await db.transaction(async (tx) => {
10332
+ const existingToken = await getTokenBySlug(tx, opts.slug);
10333
+ if (!existingToken) {
10334
+ throw new Error(`Token not found: ${opts.slug}`);
10335
+ }
10336
+ const card = await getCard(tx, existingToken.id, userId);
10337
+ if (!card) {
10338
+ throw new Error(
10339
+ `Card not found for token ${opts.slug} and user ${userId}`
10340
+ );
10341
+ }
10342
+ return updateToken(tx, opts.slug, updates);
10343
+ });
10344
+ jsonOut2({
10345
+ success: true,
10346
+ token: {
10347
+ id: token.id,
10348
+ slug: token.slug,
10349
+ concept: token.concept,
10350
+ domain: token.domain,
10351
+ bloomLevel: token.bloom_level,
10352
+ context: token.context,
10353
+ symbiosisMode: token.symbiosis_mode,
10354
+ sourceLink: token.source_link,
10355
+ question: token.question,
10356
+ createdAt: token.created_at,
10357
+ updatedAt: token.updated_at
10358
+ }
10359
+ });
10360
+ });
10361
+ });
10362
+ bridgeCommand.command("personal-card-remove").description(
10363
+ "Remove a personal card (optionally previewing the effects) (JSON)"
10364
+ ).option("--user <id>", "User ID (default: whoami)").requiredOption("--slug <slug>", "Token slug").option("--confirm", "Perform the deletion instead of a preview").action(async (opts) => {
10365
+ await withDb2(async (db) => {
10366
+ const userId = await resolveUser(opts, db, { json: true });
10367
+ const token = await getTokenBySlug(db, opts.slug);
10368
+ if (!token) {
10369
+ jsonError(`Token not found: ${opts.slug}`);
10370
+ }
10371
+ const card = await getCard(db, token.id, userId);
10372
+ if (!card) {
10373
+ jsonError(`Card not found for token ${opts.slug} and user ${userId}`);
10374
+ }
10375
+ if (!opts.confirm) {
10376
+ const impact = await getCardDeletionImpact(db, token.id, userId);
10377
+ jsonOut2({
10378
+ success: true,
10379
+ preview: true,
10380
+ requiresConfirmation: true,
10381
+ token: { id: token.id, slug: token.slug },
10382
+ impact
10383
+ });
10384
+ return;
10385
+ }
10386
+ const result = await deleteCardForUser(db, token.id, userId);
10387
+ jsonOut2({
10388
+ success: true,
10389
+ deletedCard: {
10390
+ id: result.card.id,
10391
+ tokenId: result.card.token_id,
10392
+ userId: result.card.user_id
10393
+ },
10394
+ impact: result.impact
10395
+ });
10396
+ });
10397
+ });
10398
+ bridgeCommand.command("personal-card-delete").description("Hard-delete a token and all its dependencies (JSON)").requiredOption("--slug <slug>", "Token slug").option("--confirm", "Perform the deletion instead of a preview").action(async (opts) => {
10399
+ await withDb2(async (db) => {
10400
+ const token = await getTokenBySlug(db, opts.slug);
10401
+ if (!token) {
10402
+ jsonError(`Token not found: ${opts.slug}`);
10403
+ }
10404
+ if (!opts.confirm) {
10405
+ const impact = await getTokenDeleteImpact(db, opts.slug);
10406
+ jsonOut2({
10407
+ success: true,
10408
+ preview: true,
10409
+ requiresConfirmation: true,
10410
+ token: { id: token.id, slug: token.slug },
10411
+ impact
10412
+ });
10413
+ return;
10414
+ }
10415
+ const result = await deleteToken(db, opts.slug);
10416
+ jsonOut2({
10417
+ success: true,
10418
+ deletedToken: {
10419
+ id: result.token.id,
10420
+ slug: result.token.slug
10421
+ },
10422
+ impact: result.impact
10423
+ });
10424
+ });
10425
+ });
10426
+ 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(
10427
+ "--domain <domain>",
10428
+ "Default category/domain for imported cards"
10429
+ ).option("--source <url>", "Provenance source link or URL").option("--preview", "Return parsed cards without saving them").action(async (opts) => {
10430
+ await withDb2(async (db) => {
10431
+ const userId = await resolveUser(opts, db, { json: true });
10432
+ const cards = await importCurriculumViaLLM(
10433
+ db,
10434
+ opts.text,
10435
+ opts.domain,
10436
+ opts.source || null
10437
+ );
10438
+ if (opts.preview) {
10439
+ jsonOut2({
10440
+ success: true,
10441
+ proposals: cards
10442
+ });
10443
+ return;
10444
+ }
10445
+ const result = await importCurriculumCards(db, userId, cards);
10446
+ jsonOut2({
10447
+ success: true,
10448
+ createdCount: result.createdCount,
10449
+ ensuredCount: result.ensuredCount
10450
+ });
10451
+ });
10452
+ });
10453
+ bridgeCommand.command("personal-card-split-proposals").description("Generate atomic proposals for splitting a card (JSON)").requiredOption("--slug <slug>", "Original token slug").action(async (opts) => {
10454
+ await withDb2(async (db) => {
10455
+ const token = await getTokenBySlug(db, opts.slug);
10456
+ if (!token) {
10457
+ jsonError(`Token not found: ${opts.slug}`);
10458
+ }
10459
+ const proposals = await generateSplitProposalsViaLLM(db, token);
10460
+ jsonOut2({
10461
+ success: true,
10462
+ proposals
10463
+ });
10464
+ });
10465
+ });
10466
+ bridgeCommand.command("personal-card-confirm-split").description("Save confirmed card split and modify original card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--slug <slug>", "Original token slug").requiredOption(
10467
+ "--action <action>",
10468
+ "Original card action: 'block' or 'remove'"
10469
+ ).requiredOption(
10470
+ "--original-question <question>",
10471
+ "Rewritten question of the original card"
10472
+ ).requiredOption(
10473
+ "--original-concept <concept>",
10474
+ "Rewritten concept of the original card"
10475
+ ).requiredOption(
10476
+ "--proposals <json>",
10477
+ "JSON string representing proposals array"
10478
+ ).action(async (opts) => {
10479
+ await withDb2(async (db) => {
10480
+ const userId = await resolveUser(opts, db, { json: true });
10481
+ const proposals = JSON.parse(opts.proposals);
10482
+ const result = await confirmCardSplit(
10483
+ db,
10484
+ userId,
10485
+ opts.slug,
10486
+ opts.action,
10487
+ opts.originalQuestion,
10488
+ opts.originalConcept,
10489
+ proposals
10490
+ );
10491
+ jsonOut2({
10492
+ success: true,
10493
+ createdCount: result.createdCount,
10494
+ ensuredCount: result.ensuredCount
10495
+ });
10496
+ });
10497
+ });
10498
+ bridgeCommand.command("personal-card-foundations-proposals").description("Generate prerequisite suggestions for a card (JSON)").requiredOption("--slug <slug>", "Original token slug").action(async (opts) => {
10499
+ await withDb2(async (db) => {
10500
+ const token = await getTokenBySlug(db, opts.slug);
10501
+ if (!token) {
10502
+ jsonError(`Token not found: ${opts.slug}`);
10503
+ }
10504
+ const proposals = await generateFoundationsProposalsViaLLM(db, token);
10505
+ const resolvedProposals = [];
10506
+ for (const prop of proposals) {
10507
+ const baseText = prop.question && prop.question.trim().length > 0 ? prop.question : prop.concept;
10508
+ const cleanDomain = slugify(prop.domain || "");
10509
+ const cleanBase = slugify(baseText);
10510
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
10511
+ if (baseSlug.length > 60) {
10512
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
10513
+ }
10514
+ if (!baseSlug) {
10515
+ baseSlug = "token";
10516
+ }
10517
+ const existingToken = await getTokenBySlug(db, baseSlug);
10518
+ if (existingToken) {
10519
+ resolvedProposals.push({
10520
+ ...prop,
10521
+ exists: true,
10522
+ slug: existingToken.slug,
10523
+ question: existingToken.question || prop.question,
10524
+ concept: existingToken.concept,
10525
+ domain: existingToken.domain
10526
+ });
10527
+ } else {
10528
+ resolvedProposals.push({
10529
+ ...prop,
10530
+ exists: false,
10531
+ slug: null
10532
+ });
10533
+ }
10534
+ }
10535
+ jsonOut2({
10536
+ success: true,
10537
+ proposals: resolvedProposals
10538
+ });
10539
+ });
10540
+ });
10541
+ bridgeCommand.command("personal-card-confirm-foundations").description("Save confirmed foundational prerequisites (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--slug <slug>", "Original token slug").requiredOption(
10542
+ "--proposals <json>",
10543
+ "JSON string representing proposals array"
10544
+ ).action(async (opts) => {
10545
+ await withDb2(async (db) => {
10546
+ const userId = await resolveUser(opts, db, { json: true });
10547
+ const proposals = JSON.parse(opts.proposals);
10548
+ const result = await confirmFoundations(db, userId, opts.slug, proposals);
10549
+ jsonOut2({
10550
+ success: true,
10551
+ createdCount: result.createdCount,
10552
+ linkedCount: result.linkedCount
10553
+ });
10554
+ });
10555
+ });
10556
+ bridgeCommand.command("personal-source-import").description(
10557
+ "Fetch and clean plain text from local file, web link, or vision scan (JSON)"
10558
+ ).requiredOption("--type <file|web|scan>", "Source type").requiredOption("--uri <uri>", "Source path or URL").action(async (opts) => {
10559
+ await withDb2(async (db) => {
10560
+ let content = "";
10561
+ if (opts.type === "file") {
10562
+ content = await readLocalFile(opts.uri);
10563
+ } else if (opts.type === "web") {
10564
+ content = await readWebLink(opts.uri);
10565
+ } else if (opts.type === "scan") {
10566
+ content = await readImageOCR(db, opts.uri);
10567
+ } else {
10568
+ throw new Error(`Invalid source type: ${opts.type}`);
10569
+ }
10570
+ const sourceId = ulid8();
10571
+ await db.prepare(
10572
+ `INSERT INTO sources (id, type, uri, content)
10573
+ VALUES (?, ?, ?, ?)
10574
+ ON CONFLICT(uri) DO UPDATE SET
10575
+ type = excluded.type,
10576
+ content = excluded.content`
10577
+ ).run(sourceId, opts.type, opts.uri, content);
10578
+ const record = await db.prepare("SELECT id, content FROM sources WHERE uri = ?").get(opts.uri);
10579
+ jsonOut2({
10580
+ success: true,
10581
+ sourceId: record.id,
10582
+ content: record.content
10583
+ });
10584
+ });
10585
+ });
10586
+ bridgeCommand.command("personal-source-confirm-import").description(
10587
+ "Confirm and save cards generated from a source reference (JSON)"
10588
+ ).option("--user <id>", "User ID (default: whoami)").requiredOption("--sourceId <id>", "Source database ID").requiredOption(
10589
+ "--proposals <json>",
10590
+ "JSON string representing proposals array"
10591
+ ).action(async (opts) => {
10592
+ await withDb2(async (db) => {
10593
+ const userId = await resolveUser(opts, db, { json: true });
10594
+ const proposals = JSON.parse(opts.proposals);
10595
+ const result = await confirmSourceImport(
10596
+ db,
10597
+ userId,
10598
+ opts.sourceId,
10599
+ proposals
10600
+ );
10601
+ jsonOut2({
10602
+ success: true,
10603
+ createdCount: result.createdCount,
10604
+ ensuredCount: result.linkedCount
10605
+ });
10606
+ });
10607
+ });
9294
10608
  bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/stdout server").option("--stdin", "Use stdin/stdout for communication").action(async (_opts) => {
9295
10609
  isServeMode = true;
9296
10610
  const {
@@ -11525,7 +12839,7 @@ ${"\u2550".repeat(50)}`);
11525
12839
  });
11526
12840
 
11527
12841
  // src/cli/commands/session.ts
11528
- import { readFileSync as readFileSync12 } from "fs";
12842
+ import { readFileSync as readFileSync13 } from "fs";
11529
12843
  import { input as input6, select as select2 } from "@inquirer/prompts";
11530
12844
  import { Command as Command14 } from "commander";
11531
12845
  var sessionCommand = new Command14("session").description(
@@ -11717,7 +13031,7 @@ function loadPatternFile(path) {
11717
13031
  if (!path) return [];
11718
13032
  let parsed;
11719
13033
  try {
11720
- parsed = JSON.parse(readFileSync12(path, "utf-8"));
13034
+ parsed = JSON.parse(readFileSync13(path, "utf-8"));
11721
13035
  } catch (err) {
11722
13036
  throw new Error(
11723
13037
  `Cannot read synthesis patterns from ${path}: ${err.message}`
@@ -12266,7 +13580,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
12266
13580
  });
12267
13581
 
12268
13582
  // src/cli/commands/snapshot.ts
12269
- import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
13583
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
12270
13584
  import { dirname as dirname7, join as join21 } from "path";
12271
13585
  import { Command as Command18 } from "commander";
12272
13586
  function defaultOutName() {
@@ -12319,7 +13633,7 @@ var importCmd = new Command18("import").description("Restore a snapshot into the
12319
13633
  console.error(`Error: Snapshot file not found: ${file}`);
12320
13634
  process.exit(1);
12321
13635
  }
12322
- const snapshot = readFileSync13(file, "utf-8");
13636
+ const snapshot = readFileSync14(file, "utf-8");
12323
13637
  db = await openDatabaseWithSync({ initialize: true });
12324
13638
  const result = await importSnapshot(db, snapshot, { force: opts.force });
12325
13639
  await db.close();
@@ -12341,7 +13655,7 @@ var verifyCmd = new Command18("verify").description("Check a snapshot's manifest
12341
13655
  console.error(`Error: Snapshot file not found: ${file}`);
12342
13656
  process.exit(1);
12343
13657
  }
12344
- const manifest = verifySnapshot(readFileSync13(file, "utf-8"));
13658
+ const manifest = verifySnapshot(readFileSync14(file, "utf-8"));
12345
13659
  const { total, nonEmpty } = summarize(manifest.tables);
12346
13660
  console.log(`Valid snapshot (format v${manifest.version})`);
12347
13661
  console.log(` created: ${manifest.createdAt}`);
@@ -12969,7 +14283,7 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
12969
14283
 
12970
14284
  // src/cli/commands/update.ts
12971
14285
  import { spawnSync as spawnSync2 } from "child_process";
12972
- import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "fs";
14286
+ import { existsSync as existsSync24, readFileSync as readFileSync15, realpathSync as realpathSync2 } from "fs";
12973
14287
  import { dirname as dirname9, join as join23 } from "path";
12974
14288
  import { fileURLToPath as fileURLToPath4 } from "url";
12975
14289
  import { confirm as confirm3 } from "@inquirer/prompts";
@@ -12995,7 +14309,7 @@ function currentVersion() {
12995
14309
  for (const up of ["..", "../..", "../../.."]) {
12996
14310
  try {
12997
14311
  const pkg2 = JSON.parse(
12998
- readFileSync14(join23(here, up, "package.json"), "utf-8")
14312
+ readFileSync15(join23(here, up, "package.json"), "utf-8")
12999
14313
  );
13000
14314
  if (pkg2.version) return pkg2.version;
13001
14315
  } catch {
@@ -13006,7 +14320,7 @@ function currentVersion() {
13006
14320
  function versionAt(dir) {
13007
14321
  try {
13008
14322
  const pkg2 = JSON.parse(
13009
- readFileSync14(join23(dir, "package.json"), "utf-8")
14323
+ readFileSync15(join23(dir, "package.json"), "utf-8")
13010
14324
  );
13011
14325
  return pkg2.version ?? "unknown";
13012
14326
  } catch {
@@ -13641,7 +14955,7 @@ workspaceCommand.command("backup").description("Back up the local ZAM database i
13641
14955
  // src/cli/index.ts
13642
14956
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
13643
14957
  var pkg = JSON.parse(
13644
- readFileSync15(join25(__dirname, "..", "..", "package.json"), "utf-8")
14958
+ readFileSync16(join25(__dirname, "..", "..", "package.json"), "utf-8")
13645
14959
  );
13646
14960
  var program = new Command25();
13647
14961
  program.name("zam").description(