zam-core 0.5.3 → 0.6.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
@@ -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,415 @@ 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
+ t.source_link AS sourceLink,
1726
+ t.question,
1727
+ t.created_at AS createdAt,
1728
+ t.updated_at AS updatedAt,
1729
+ c.id AS cardId,
1730
+ c.state,
1731
+ c.due_at AS dueAt,
1732
+ c.stability,
1733
+ c.difficulty,
1734
+ c.reps,
1735
+ c.lapses,
1736
+ c.elapsed_days AS elapsedDays,
1737
+ c.scheduled_days AS scheduledDays,
1738
+ c.blocked
1739
+ FROM tokens t
1740
+ INNER JOIN cards c ON c.token_id = t.id AND c.user_id = ?
1741
+ WHERE t.deprecated_at IS NULL
1742
+ `;
1743
+ const values = [userId];
1744
+ if (options?.domain) {
1745
+ sql += " AND t.domain = ?";
1746
+ values.push(options.domain);
1747
+ }
1748
+ if (options?.query) {
1749
+ const terms = options.query.toLowerCase().split(/\s+/).filter(Boolean);
1750
+ for (const term of terms) {
1751
+ sql += ` AND (lower(t.slug) LIKE ? OR lower(t.concept) LIKE ? OR lower(t.domain) LIKE ? OR lower(t.question) LIKE ?)`;
1752
+ const pattern = `%${term}%`;
1753
+ values.push(pattern, pattern, pattern, pattern);
1754
+ }
1755
+ }
1756
+ sql += " ORDER BY t.created_at DESC";
1757
+ const rows = await db.prepare(sql).all(...values);
1758
+ return rows;
1759
+ }
1760
+ async function importCurriculumCards(db, userId, cards) {
1761
+ let createdCount = 0;
1762
+ let ensuredCount = 0;
1763
+ await db.transaction(async (tx) => {
1764
+ for (const card of cards) {
1765
+ const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
1766
+ if (bloom < 1 || bloom > 5) {
1767
+ throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1768
+ }
1769
+ let symbiosisMode = null;
1770
+ if (card.symbiosis_mode) {
1771
+ if (!["shadowing", "copilot", "autonomy", "none"].includes(
1772
+ card.symbiosis_mode
1773
+ )) {
1774
+ throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
1775
+ }
1776
+ symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
1777
+ }
1778
+ const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
1779
+ const cleanDomain = slugify(card.domain || "");
1780
+ const cleanBase = slugify(baseText);
1781
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
1782
+ if (baseSlug.length > 60) {
1783
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
1784
+ }
1785
+ if (!baseSlug) {
1786
+ baseSlug = "token";
1787
+ }
1788
+ let token = await getTokenBySlug(tx, baseSlug);
1789
+ if (!token) {
1790
+ const finalSlug = await generateTokenSlug(
1791
+ tx,
1792
+ card.domain,
1793
+ card.concept,
1794
+ card.question
1795
+ );
1796
+ token = await createToken(tx, {
1797
+ slug: finalSlug,
1798
+ concept: card.concept,
1799
+ domain: card.domain,
1800
+ bloom_level: bloom,
1801
+ context: card.context || "",
1802
+ symbiosis_mode: symbiosisMode,
1803
+ source_link: card.source_link || null,
1804
+ question: card.question || null
1805
+ });
1806
+ createdCount++;
1807
+ }
1808
+ const existingCard = await getCard(tx, token.id, userId);
1809
+ if (!existingCard) {
1810
+ await ensureCard(tx, token.id, userId);
1811
+ ensuredCount++;
1812
+ }
1813
+ }
1814
+ });
1815
+ return { createdCount, ensuredCount };
1816
+ }
1817
+ async function confirmCardSplit(db, userId, originalSlug, action, originalQuestion, originalConcept, proposals) {
1818
+ if (action !== "block" && action !== "remove") {
1819
+ throw new Error(`Invalid split action: ${action}`);
1820
+ }
1821
+ if (proposals.length < 2 || proposals.length > 4) {
1822
+ throw new Error("A card split requires between 2 and 4 proposals");
1823
+ }
1824
+ const originalToken = await getTokenBySlug(db, originalSlug);
1825
+ if (!originalToken) {
1826
+ throw new Error(`Original token not found: ${originalSlug}`);
1827
+ }
1828
+ let createdCount = 0;
1829
+ let ensuredCount = 0;
1830
+ await db.transaction(async (tx) => {
1831
+ const originalCard = await getCard(tx, originalToken.id, userId);
1832
+ if (!originalCard) {
1833
+ throw new Error(
1834
+ `Card not found for token ${originalSlug} and user ${userId}`
1835
+ );
1836
+ }
1837
+ const proposalTokens = [];
1838
+ for (const card of proposals) {
1839
+ const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
1840
+ if (bloom < 1 || bloom > 5) {
1841
+ throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1842
+ }
1843
+ let symbiosisMode = null;
1844
+ if (card.symbiosis_mode) {
1845
+ if (!["shadowing", "copilot", "autonomy", "none"].includes(
1846
+ card.symbiosis_mode
1847
+ )) {
1848
+ throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
1849
+ }
1850
+ symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
1851
+ }
1852
+ const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
1853
+ const cleanDomain = slugify(card.domain || "");
1854
+ const cleanBase = slugify(baseText);
1855
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
1856
+ if (baseSlug.length > 60) {
1857
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
1858
+ }
1859
+ if (!baseSlug) {
1860
+ baseSlug = "token";
1861
+ }
1862
+ let token = await getTokenBySlug(tx, baseSlug);
1863
+ if (!token) {
1864
+ const finalSlug = await generateTokenSlug(
1865
+ tx,
1866
+ card.domain,
1867
+ card.concept,
1868
+ card.question
1869
+ );
1870
+ token = await createToken(tx, {
1871
+ slug: finalSlug,
1872
+ concept: card.concept,
1873
+ domain: card.domain,
1874
+ bloom_level: bloom,
1875
+ context: card.context || "",
1876
+ symbiosis_mode: symbiosisMode,
1877
+ source_link: card.source_link || originalToken.source_link || null,
1878
+ question: card.question || null
1879
+ });
1880
+ createdCount++;
1881
+ }
1882
+ if (token.id === originalToken.id) {
1883
+ throw new Error("A token cannot be a prerequisite of itself");
1884
+ }
1885
+ await assertPrerequisiteDoesNotCreateCycle(
1886
+ tx,
1887
+ originalToken.id,
1888
+ token.id
1889
+ );
1890
+ proposalTokens.push(token);
1891
+ const existingCard = await getCard(tx, token.id, userId);
1892
+ if (!existingCard) {
1893
+ await ensureCard(tx, token.id, userId);
1894
+ ensuredCount++;
1895
+ }
1896
+ }
1897
+ if (action === "block") {
1898
+ await tx.prepare(
1899
+ "UPDATE tokens SET question = ?, concept = ?, updated_at = ? WHERE id = ?"
1900
+ ).run(
1901
+ originalQuestion || null,
1902
+ originalConcept,
1903
+ (/* @__PURE__ */ new Date()).toISOString(),
1904
+ originalToken.id
1905
+ );
1906
+ for (const propToken of proposalTokens) {
1907
+ await tx.prepare(
1908
+ "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
1909
+ ).run(originalToken.id, propToken.id);
1910
+ }
1911
+ await tx.prepare(
1912
+ "UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
1913
+ ).run(originalToken.id, userId);
1914
+ for (const propToken of proposalTokens) {
1915
+ const card = await ensureCard(tx, propToken.id, userId);
1916
+ if (card.blocked === 1) {
1917
+ const prereqOfPrereq = await tx.prepare(
1918
+ "SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
1919
+ ).get(propToken.id);
1920
+ if (prereqOfPrereq.n === 0) {
1921
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1922
+ await tx.prepare("UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?").run(now, card.id);
1923
+ }
1924
+ }
1925
+ }
1926
+ } else if (action === "remove") {
1927
+ await deleteCardForUser(tx, originalToken.id, userId);
1928
+ }
1929
+ });
1930
+ return { createdCount, ensuredCount };
1931
+ }
1932
+ async function confirmFoundations(db, userId, originalSlug, proposals) {
1933
+ const originalToken = await getTokenBySlug(db, originalSlug);
1934
+ if (!originalToken) {
1935
+ throw new Error(`Original token not found: ${originalSlug}`);
1936
+ }
1937
+ let createdCount = 0;
1938
+ let linkedCount = 0;
1939
+ await db.transaction(async (tx) => {
1940
+ const originalCard = await getCard(tx, originalToken.id, userId);
1941
+ if (!originalCard) {
1942
+ throw new Error(
1943
+ `Card not found for token ${originalSlug} and user ${userId}`
1944
+ );
1945
+ }
1946
+ for (const card of proposals) {
1947
+ let targetTokenId;
1948
+ if (card.exists && card.slug) {
1949
+ const existingToken = await getTokenBySlug(tx, card.slug);
1950
+ if (!existingToken) {
1951
+ throw new Error(`Prerequisite token not found by slug: ${card.slug}`);
1952
+ }
1953
+ targetTokenId = existingToken.id;
1954
+ const existingCard = await getCard(tx, existingToken.id, userId);
1955
+ if (!existingCard) {
1956
+ await ensureCard(tx, existingToken.id, userId);
1957
+ }
1958
+ linkedCount++;
1959
+ } else {
1960
+ const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
1961
+ if (bloom < 1 || bloom > 5) {
1962
+ throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1963
+ }
1964
+ let symbiosisMode = null;
1965
+ if (card.symbiosis_mode) {
1966
+ if (!["shadowing", "copilot", "autonomy", "none"].includes(
1967
+ card.symbiosis_mode
1968
+ )) {
1969
+ throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
1970
+ }
1971
+ symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
1972
+ }
1973
+ const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
1974
+ const cleanDomain = slugify(card.domain || "");
1975
+ const cleanBase = slugify(baseText);
1976
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
1977
+ if (baseSlug.length > 60) {
1978
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
1979
+ }
1980
+ if (!baseSlug) {
1981
+ baseSlug = "token";
1982
+ }
1983
+ let token = await getTokenBySlug(tx, baseSlug);
1984
+ if (!token) {
1985
+ const finalSlug = await generateTokenSlug(
1986
+ tx,
1987
+ card.domain,
1988
+ card.concept,
1989
+ card.question
1990
+ );
1991
+ token = await createToken(tx, {
1992
+ slug: finalSlug,
1993
+ concept: card.concept,
1994
+ domain: card.domain,
1995
+ bloom_level: bloom,
1996
+ context: card.context || "",
1997
+ symbiosis_mode: symbiosisMode,
1998
+ source_link: card.source_link || originalToken.source_link || null,
1999
+ question: card.question || null
2000
+ });
2001
+ createdCount++;
2002
+ }
2003
+ targetTokenId = token.id;
2004
+ const existingCard = await getCard(tx, token.id, userId);
2005
+ if (!existingCard) {
2006
+ await ensureCard(tx, token.id, userId);
2007
+ }
2008
+ }
2009
+ if (targetTokenId === originalToken.id) {
2010
+ throw new Error("A token cannot be a prerequisite of itself");
2011
+ }
2012
+ await assertPrerequisiteDoesNotCreateCycle(
2013
+ tx,
2014
+ originalToken.id,
2015
+ targetTokenId
2016
+ );
2017
+ await tx.prepare(
2018
+ "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
2019
+ ).run(originalToken.id, targetTokenId);
2020
+ }
2021
+ });
2022
+ return { createdCount, linkedCount };
2023
+ }
2024
+ async function confirmSourceImport(db, userId, sourceId, proposals) {
2025
+ let createdCount = 0;
2026
+ let linkedCount = 0;
2027
+ await db.transaction(async (tx) => {
2028
+ const source = await tx.prepare("SELECT id FROM sources WHERE id = ?").get(sourceId);
2029
+ if (!source) {
2030
+ throw new Error(`Source not found: ${sourceId}`);
2031
+ }
2032
+ for (const card of proposals) {
2033
+ const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
2034
+ const cleanDomain = slugify(card.domain || "");
2035
+ const cleanBase = slugify(baseText);
2036
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
2037
+ if (baseSlug.length > 60) {
2038
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
2039
+ }
2040
+ if (!baseSlug) {
2041
+ baseSlug = "token";
2042
+ }
2043
+ let token = await getTokenBySlug(tx, baseSlug);
2044
+ if (!token) {
2045
+ const finalSlug = await generateTokenSlug(
2046
+ tx,
2047
+ card.domain,
2048
+ card.concept,
2049
+ card.question
2050
+ );
2051
+ const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
2052
+ let symbiosisMode = null;
2053
+ if (card.symbiosis_mode && card.symbiosis_mode !== "none") {
2054
+ symbiosisMode = card.symbiosis_mode;
2055
+ }
2056
+ token = await createToken(tx, {
2057
+ slug: finalSlug,
2058
+ concept: card.concept,
2059
+ domain: card.domain,
2060
+ bloom_level: bloom,
2061
+ context: card.excerpt || "",
2062
+ symbiosis_mode: symbiosisMode,
2063
+ question: card.question || null
2064
+ });
2065
+ createdCount++;
2066
+ } else {
2067
+ linkedCount++;
2068
+ }
2069
+ const existingCard = await getCard(tx, token.id, userId);
2070
+ if (!existingCard) {
2071
+ await ensureCard(tx, token.id, userId);
2072
+ }
2073
+ await tx.prepare(
2074
+ `INSERT INTO token_sources (token_id, source_id, excerpt, page_number)
2075
+ VALUES (?, ?, ?, ?)
2076
+ ON CONFLICT(token_id, source_id) DO UPDATE SET
2077
+ excerpt = excluded.excerpt,
2078
+ page_number = excluded.page_number`
2079
+ ).run(token.id, sourceId, card.excerpt || "", card.page_number || null);
2080
+ }
2081
+ });
2082
+ return { createdCount, linkedCount };
2083
+ }
2084
+ async function assertPrerequisiteDoesNotCreateCycle(db, tokenId, prerequisiteId) {
2085
+ const cycleCheck = await db.prepare(
2086
+ `WITH RECURSIVE dependents(token_id) AS (
2087
+ SELECT token_id FROM prerequisites WHERE requires_id = ?
2088
+ UNION
2089
+ SELECT p.token_id FROM prerequisites p
2090
+ JOIN dependents d ON p.requires_id = d.token_id
2091
+ )
2092
+ SELECT COUNT(*) as n FROM dependents WHERE token_id = ?`
2093
+ ).get(tokenId, prerequisiteId);
2094
+ if (cycleCheck.n > 0) {
2095
+ throw new Error("Cannot add prerequisite: would create a cycle");
2096
+ }
2097
+ }
1653
2098
 
1654
2099
  // src/kernel/models/prerequisite.ts
1655
2100
  async function buildAncestorMap(db) {
@@ -5390,14 +5835,19 @@ var agentCommand = new Command("agent").description("Provision and inspect the a
5390
5835
  // src/cli/commands/bridge.ts
5391
5836
  import { execFileSync as execFileSync3 } from "child_process";
5392
5837
  import { randomBytes as randomBytes2 } from "crypto";
5393
- import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
5838
+ import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync12, rmSync as rmSync3 } from "fs";
5394
5839
  import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
5395
5840
  import { join as join18, resolve as resolve4 } from "path";
5396
5841
  import { Command as Command2 } from "commander";
5842
+ import { ulid as ulid8 } from "ulid";
5843
+
5844
+ // src/cli/adapters/source-reader.ts
5845
+ import dns from "dns";
5846
+ import fs from "fs";
5397
5847
 
5398
5848
  // src/cli/llm/client.ts
5399
5849
  import { spawn as spawn2 } from "child_process";
5400
- import { existsSync as existsSync14 } from "fs";
5850
+ import { existsSync as existsSync14, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
5401
5851
  var DEFAULT_LLM_URL = "http://localhost:8000/v1";
5402
5852
  var DEFAULT_LLM_MAX_TOKENS = 1e4;
5403
5853
  var RECALL_QUESTION_MAX_OUTPUT_TOKENS = 400;
@@ -5570,10 +6020,21 @@ async function getLegacyRoleConfig(db, role, enabled) {
5570
6020
  function assertChatCompletions(cfg) {
5571
6021
  if (cfg.apiFlavor !== "chat-completions") {
5572
6022
  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.`
6023
+ `This role is configured for a "${cfg.apiFlavor}" provider, which is not supported here yet. Use a chat-completions provider for this role.`
5574
6024
  );
5575
6025
  }
5576
6026
  }
6027
+ async function getVisionConfig(db) {
6028
+ const p = await getProviderForRole(db, "vision");
6029
+ return {
6030
+ enabled: p.enabled,
6031
+ url: p.url,
6032
+ model: p.model,
6033
+ apiKey: p.apiKey,
6034
+ locale: p.locale,
6035
+ maxFrames: p.maxFrames
6036
+ };
6037
+ }
5577
6038
  var LANGUAGE_NAMES = {
5578
6039
  en: "English",
5579
6040
  de: "German",
@@ -5721,6 +6182,306 @@ Evaluation:`;
5721
6182
  providerName: endpoint.providerName
5722
6183
  };
5723
6184
  }
6185
+ var MAX_IMPORT_TEXT_CHARS = 2e5;
6186
+ var VALID_GENERATED_MODES = /* @__PURE__ */ new Set(["shadowing", "copilot", "autonomy"]);
6187
+ function parseGeneratedCardArray(responseText, label, limits) {
6188
+ const startIdx = responseText.indexOf("[");
6189
+ const endIdx = responseText.lastIndexOf("]");
6190
+ if (startIdx === -1 || endIdx === -1 || startIdx > endIdx) {
6191
+ throw new Error(`Invalid ${label} response: JSON array brackets not found`);
6192
+ }
6193
+ let parsed;
6194
+ try {
6195
+ parsed = JSON.parse(responseText.substring(startIdx, endIdx + 1));
6196
+ } catch {
6197
+ throw new Error(`Invalid ${label} response: malformed JSON`);
6198
+ }
6199
+ if (!Array.isArray(parsed)) {
6200
+ throw new Error(`Invalid ${label} response: expected a JSON array`);
6201
+ }
6202
+ if (parsed.length < limits.min || parsed.length > limits.max) {
6203
+ throw new Error(
6204
+ `Invalid ${label} response: expected ${limits.min}-${limits.max} cards, got ${parsed.length}`
6205
+ );
6206
+ }
6207
+ return parsed.map((value, index) => {
6208
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6209
+ throw new Error(
6210
+ `Invalid ${label} card at index ${index}: expected an object`
6211
+ );
6212
+ }
6213
+ const card = value;
6214
+ for (const field of ["question", "concept", "domain", "context"]) {
6215
+ if (typeof card[field] !== "string" || card[field].trim().length === 0) {
6216
+ throw new Error(
6217
+ `Invalid ${label} card at index ${index}: ${field} must be a non-empty string`
6218
+ );
6219
+ }
6220
+ }
6221
+ if (typeof card.bloom_level !== "number" || !Number.isInteger(card.bloom_level) || card.bloom_level < 1 || card.bloom_level > 5) {
6222
+ throw new Error(
6223
+ `Invalid ${label} card at index ${index}: bloom_level must be an integer from 1 to 5`
6224
+ );
6225
+ }
6226
+ if (typeof card.symbiosis_mode !== "string" || !VALID_GENERATED_MODES.has(card.symbiosis_mode)) {
6227
+ throw new Error(
6228
+ `Invalid ${label} card at index ${index}: invalid symbiosis_mode`
6229
+ );
6230
+ }
6231
+ return {
6232
+ question: card.question,
6233
+ concept: card.concept,
6234
+ domain: card.domain,
6235
+ context: card.context,
6236
+ bloom_level: card.bloom_level,
6237
+ symbiosis_mode: card.symbiosis_mode,
6238
+ source_link: null
6239
+ };
6240
+ });
6241
+ }
6242
+ async function importCurriculumViaLLM(db, text, targetCategory, sourceUrl) {
6243
+ if (text.length > MAX_IMPORT_TEXT_CHARS) {
6244
+ throw new Error(
6245
+ `Curriculum text exceeds the ${MAX_IMPORT_TEXT_CHARS.toLocaleString()} character limit`
6246
+ );
6247
+ }
6248
+ const cfg = await getProviderForRole(db, "text");
6249
+ const endpoint = await resolveUsableTextEndpoint(db);
6250
+ const langName = LANGUAGE_NAMES[cfg.locale] || "English";
6251
+ const systemPrompt = `You are ZAM, a highly precise agentic curriculum parser.
6252
+ 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}.
6253
+
6254
+ For each extracted learning card, you MUST generate:
6255
+ 1. "question": A clear, concise active-recall question testing the concept. The question must not reveal the answer itself.
6256
+ 2. "concept": The reference answer, core fact, or target conceptual explanation.
6257
+ 3. "domain": The category of the card. Use "${targetCategory}" as the default, but you may refine it if a specific sub-topic is evident.
6258
+ 4. "context": The exact sentence or short excerpt from the source text that this card is based on.
6259
+ 5. "bloom_level": Initial Bloom cognitive level (1 = Remember, 2 = Understand, 3 = Apply, 4 = Analyze, 5 = Synthesize).
6260
+ 6. "symbiosis_mode": ZAM agent symbiosis level: "shadowing" (reading/monitoring), "copilot" (interactive helper), or "autonomy" (autonomous tasks).
6261
+
6262
+ Guidelines:
6263
+ - Break complex requirements into multiple separate, atomic cards.
6264
+ - Keep questions focused on one fact.
6265
+ - Do not repeat the same concept in multiple cards.
6266
+ - 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.`;
6267
+ const userPrompt = `Curriculum Text to Parse:
6268
+ ${text}
6269
+
6270
+ Target Category: ${targetCategory}
6271
+ ${sourceUrl ? `Source Reference Link: ${sourceUrl}` : ""}
6272
+
6273
+ JSON Array Output:`;
6274
+ const res = await fetchWithInteractiveTimeout(
6275
+ `${endpoint.url}/chat/completions`,
6276
+ {
6277
+ method: "POST",
6278
+ headers: {
6279
+ "Content-Type": "application/json",
6280
+ Authorization: `Bearer ${endpoint.apiKey}`
6281
+ },
6282
+ body: JSON.stringify({
6283
+ model: endpoint.model,
6284
+ messages: [
6285
+ { role: "system", content: systemPrompt },
6286
+ { role: "user", content: userPrompt }
6287
+ ],
6288
+ temperature: 0.1,
6289
+ max_tokens: DEFAULT_LLM_MAX_TOKENS
6290
+ }),
6291
+ locale: cfg.locale
6292
+ }
6293
+ );
6294
+ const responseText = await readChatContent(res, "LLM curriculum import");
6295
+ return parseGeneratedCardArray(responseText, "curriculum import", {
6296
+ min: 0,
6297
+ max: 200
6298
+ }).map((card) => ({ ...card, source_link: sourceUrl || null }));
6299
+ }
6300
+ async function generateSplitProposalsViaLLM(db, token) {
6301
+ const cfg = await getProviderForRole(db, "text");
6302
+ const endpoint = await resolveUsableTextEndpoint(db);
6303
+ const langName = LANGUAGE_NAMES[cfg.locale] || "English";
6304
+ const systemPrompt = `You are ZAM, a highly precise agentic learning assistant.
6305
+ 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}.
6306
+
6307
+ The input card details are:
6308
+ - Question: ${token.question || "N/A"}
6309
+ - Core Concept: ${token.concept}
6310
+ - Domain: ${token.domain}
6311
+ - Excerpt Context: ${token.context || "N/A"}
6312
+
6313
+ For each split proposal card, you MUST generate:
6314
+ 1. "question": A clear, concise active-recall question testing a single focused fact or concept. The question must not reveal the answer itself.
6315
+ 2. "concept": The reference answer or core fact.
6316
+ 3. "domain": The category of the card (default to "${token.domain}").
6317
+ 4. "context": The context excerpt from the original card's context or a derivation of it.
6318
+ 5. "bloom_level": Bloom taxonomy level (1 to 5).
6319
+ 6. "symbiosis_mode": Symbiosis mode ("shadowing", "copilot", or "autonomy").
6320
+
6321
+ Guidelines:
6322
+ - Make sure each card is completely atomic (covers exactly one concept).
6323
+ - Do not repeat the same concept across cards.
6324
+ - Output ONLY a raw valid JSON array of objects. Do NOT wrap the JSON in markdown code blocks, HTML, or include any conversational filler.`;
6325
+ const userPrompt = `Split the broad card details above into 2 to 4 atomic cards.
6326
+
6327
+ JSON Array Output:`;
6328
+ const res = await fetchWithInteractiveTimeout(
6329
+ `${endpoint.url}/chat/completions`,
6330
+ {
6331
+ method: "POST",
6332
+ headers: {
6333
+ "Content-Type": "application/json",
6334
+ Authorization: `Bearer ${endpoint.apiKey}`
6335
+ },
6336
+ body: JSON.stringify({
6337
+ model: endpoint.model,
6338
+ messages: [
6339
+ { role: "system", content: systemPrompt },
6340
+ { role: "user", content: userPrompt }
6341
+ ],
6342
+ temperature: 0.2,
6343
+ max_tokens: DEFAULT_LLM_MAX_TOKENS
6344
+ }),
6345
+ locale: cfg.locale
6346
+ }
6347
+ );
6348
+ const responseText = await readChatContent(res, "LLM card split proposals");
6349
+ return parseGeneratedCardArray(responseText, "card split", {
6350
+ min: 2,
6351
+ max: 4
6352
+ }).map((card) => ({ ...card, source_link: token.source_link || null }));
6353
+ }
6354
+ async function generateFoundationsProposalsViaLLM(db, token) {
6355
+ const cfg = await getProviderForRole(db, "text");
6356
+ const endpoint = await resolveUsableTextEndpoint(db);
6357
+ const langName = LANGUAGE_NAMES[cfg.locale] || "English";
6358
+ const systemPrompt = `You are ZAM, a highly precise agentic learning assistant.
6359
+ 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}.
6360
+
6361
+ The current card details are:
6362
+ - Question: ${token.question || "N/A"}
6363
+ - Core Concept: ${token.concept}
6364
+ - Domain: ${token.domain}
6365
+ - Excerpt Context: ${token.context || "N/A"}
6366
+
6367
+ For each proposed foundational card, you MUST generate:
6368
+ 1. "question": A clear, concise active-recall question testing a single foundational fact or prerequisite concept.
6369
+ 2. "concept": The reference answer or core fact.
6370
+ 3. "domain": The category of the card (default to "${token.domain}").
6371
+ 4. "context": The context excerpt explaining where this prerequisite fits in the learning hierarchy.
6372
+ 5. "bloom_level": Bloom taxonomy level (typically 1 or 2 for foundational facts).
6373
+ 6. "symbiosis_mode": Symbiosis mode ("shadowing", "copilot", or "autonomy").
6374
+
6375
+ Guidelines:
6376
+ - Recommend only highly relevant and necessary prerequisites.
6377
+ - Keep each proposed card atomic and focused on one concept.
6378
+ - Output ONLY a raw valid JSON array of objects. Do NOT wrap the JSON in markdown code blocks, HTML, or include any conversational filler.`;
6379
+ const userPrompt = `Suggest 2 to 4 foundational prerequisite cards for the card above.
6380
+
6381
+ JSON Array Output:`;
6382
+ const res = await fetchWithInteractiveTimeout(
6383
+ `${endpoint.url}/chat/completions`,
6384
+ {
6385
+ method: "POST",
6386
+ headers: {
6387
+ "Content-Type": "application/json",
6388
+ Authorization: `Bearer ${endpoint.apiKey}`
6389
+ },
6390
+ body: JSON.stringify({
6391
+ model: endpoint.model,
6392
+ messages: [
6393
+ { role: "system", content: systemPrompt },
6394
+ { role: "user", content: userPrompt }
6395
+ ],
6396
+ temperature: 0.2,
6397
+ max_tokens: DEFAULT_LLM_MAX_TOKENS
6398
+ }),
6399
+ locale: cfg.locale
6400
+ }
6401
+ );
6402
+ const responseText = await readChatContent(
6403
+ res,
6404
+ "LLM card foundations proposals"
6405
+ );
6406
+ return parseGeneratedCardArray(responseText, "foundation proposal", {
6407
+ min: 2,
6408
+ max: 4
6409
+ }).map((card) => ({ ...card, source_link: token.source_link || null }));
6410
+ }
6411
+ async function extractTextFromScanViaLLM(db, imagePath) {
6412
+ const p = await getProviderForRole(db, "vision");
6413
+ if (!p.enabled) {
6414
+ throw new Error(
6415
+ "Vision role is not enabled in settings (llm.vision.enabled)"
6416
+ );
6417
+ }
6418
+ if (!existsSync14(imagePath)) {
6419
+ throw new Error(`Scan file not found: ${imagePath}`);
6420
+ }
6421
+ const stat = statSync2(imagePath);
6422
+ if (!stat.isFile()) {
6423
+ throw new Error(`Scan path is not a file: ${imagePath}`);
6424
+ }
6425
+ if (stat.size > 10 * 1024 * 1024) {
6426
+ throw new Error("Scan file exceeds 10MB limit");
6427
+ }
6428
+ const ext = imagePath.split(".").pop()?.toLowerCase();
6429
+ const mimeByExtension = {
6430
+ jpeg: "image/jpeg",
6431
+ jpg: "image/jpeg",
6432
+ png: "image/png",
6433
+ webp: "image/webp"
6434
+ };
6435
+ const mime = ext ? mimeByExtension[ext] : void 0;
6436
+ if (!mime) {
6437
+ throw new Error("Unsupported scan format; use PNG, JPEG, or WebP");
6438
+ }
6439
+ const imageBytes = readFileSync9(imagePath);
6440
+ const dataUrl = `data:${mime};base64,${imageBytes.toString("base64")}`;
6441
+ const visionEndpoint = await getVisionConfig(db);
6442
+ const langName = LANGUAGE_NAMES[p.locale] || "English";
6443
+ 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.";
6444
+ const res = await fetchWithInteractiveTimeout(
6445
+ `${visionEndpoint.url}/chat/completions`,
6446
+ {
6447
+ method: "POST",
6448
+ headers: {
6449
+ "Content-Type": "application/json",
6450
+ Authorization: `Bearer ${visionEndpoint.apiKey || DEFAULT_LLM_API_KEY}`
6451
+ },
6452
+ body: JSON.stringify({
6453
+ model: visionEndpoint.model,
6454
+ messages: [
6455
+ { role: "system", content: systemPrompt },
6456
+ {
6457
+ role: "user",
6458
+ content: [
6459
+ {
6460
+ type: "text",
6461
+ text: `Extract all text from this scan in ${langName}:`
6462
+ },
6463
+ {
6464
+ type: "image_url",
6465
+ image_url: { url: dataUrl }
6466
+ }
6467
+ ]
6468
+ }
6469
+ ],
6470
+ temperature: 0,
6471
+ max_tokens: DEFAULT_LLM_MAX_TOKENS
6472
+ }),
6473
+ locale: p.locale
6474
+ }
6475
+ );
6476
+ if (!res.ok) {
6477
+ throw new Error(`LLM vision OCR request failed with status ${res.status}`);
6478
+ }
6479
+ const responseText = await readChatContent(
6480
+ res,
6481
+ "LLM scan OCR text extraction"
6482
+ );
6483
+ return responseText.trim();
6484
+ }
5724
6485
  async function translateQuestionViaLLM(db, question) {
5725
6486
  const cfg = await getProviderForRole(db, "recall");
5726
6487
  const endpoint = await resolveUsableRecallEndpoint(db);
@@ -5841,6 +6602,21 @@ async function resolveUsableRecallEndpoint(db) {
5841
6602
  };
5842
6603
  return selected.endpoint;
5843
6604
  }
6605
+ async function resolveUsableTextEndpoint(db) {
6606
+ const cfg = await getProviderForRole(db, "text");
6607
+ if (!cfg.enabled) {
6608
+ throw new Error(
6609
+ "Text LLM integration is disabled in settings (llm.enabled)"
6610
+ );
6611
+ }
6612
+ assertChatCompletions(cfg);
6613
+ const chain = await checkProviderChain(cfg);
6614
+ const selected = chain.firstUsable;
6615
+ if (!selected || !isEndpointUsable(selected)) {
6616
+ throw new Error("No text LLM endpoint is online");
6617
+ }
6618
+ return selected.endpoint;
6619
+ }
5844
6620
  async function prepareRecallChain(db, opts) {
5845
6621
  const cfg = await getProviderForRole(db, "recall");
5846
6622
  const fail = (reason, partial = {}) => ({
@@ -6337,9 +7113,145 @@ async function ensureHighQualityQuestion(db, token) {
6337
7113
  return null;
6338
7114
  }
6339
7115
 
7116
+ // src/cli/adapters/source-reader.ts
7117
+ var MAX_SOURCE_BYTES = 2 * 1024 * 1024;
7118
+ var MAX_REDIRECTS = 5;
7119
+ function isPrivateOrReservedIp(address) {
7120
+ const ip = address.toLowerCase().split("%", 1)[0];
7121
+ const mappedV4 = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
7122
+ const ipv4 = mappedV4 ?? (ip.includes(".") ? ip : null);
7123
+ if (ipv4) {
7124
+ const parts = ipv4.split(".").map(Number);
7125
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
7126
+ return true;
7127
+ }
7128
+ const [a, b] = parts;
7129
+ 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;
7130
+ }
7131
+ return ip === "::" || ip === "::1" || ip.startsWith("fc") || ip.startsWith("fd") || /^fe[89ab]/.test(ip) || ip.startsWith("ff") || ip.startsWith("2001:db8:");
7132
+ }
7133
+ function cleanHtml(html) {
7134
+ let text = html.replace(
7135
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
7136
+ " "
7137
+ );
7138
+ text = text.replace(/<[^>]+>/g, " ");
7139
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
7140
+ return text.replace(/\s+/g, " ").trim();
7141
+ }
7142
+ async function isSafeUrl(urlString) {
7143
+ try {
7144
+ const url = new URL(urlString);
7145
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
7146
+ return false;
7147
+ }
7148
+ if (url.username || url.password) return false;
7149
+ const hostname = url.hostname.toLowerCase().replace(/\.$/, "");
7150
+ if (hostname === "localhost" || hostname.endsWith(".localhost"))
7151
+ return false;
7152
+ const results = await dns.promises.lookup(hostname, {
7153
+ all: true,
7154
+ verbatim: true
7155
+ });
7156
+ return results.length > 0 && results.every((result) => !isPrivateOrReservedIp(result.address));
7157
+ } catch {
7158
+ return false;
7159
+ }
7160
+ }
7161
+ async function readLocalFile(filepath) {
7162
+ if (!fs.existsSync(filepath)) {
7163
+ throw new Error(`File not found: ${filepath}`);
7164
+ }
7165
+ const stat = fs.statSync(filepath);
7166
+ if (!stat.isFile()) {
7167
+ throw new Error(`Not a file: ${filepath}`);
7168
+ }
7169
+ if (stat.size > MAX_SOURCE_BYTES) {
7170
+ throw new Error("File exceeds 2MB limit");
7171
+ }
7172
+ return fs.readFileSync(filepath, "utf-8");
7173
+ }
7174
+ async function readWebLink(url) {
7175
+ const controller = new AbortController();
7176
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
7177
+ try {
7178
+ let currentUrl = new URL(url);
7179
+ for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
7180
+ if (!await isSafeUrl(currentUrl.href)) {
7181
+ throw new Error(
7182
+ `Access denied to unsafe target URL: ${currentUrl.href}`
7183
+ );
7184
+ }
7185
+ const res = await fetch(currentUrl, {
7186
+ redirect: "manual",
7187
+ signal: controller.signal,
7188
+ headers: {
7189
+ "User-Agent": "ZAM-Content-Studio/0.6.0"
7190
+ }
7191
+ });
7192
+ if (res.status >= 300 && res.status < 400) {
7193
+ const location = res.headers.get("location");
7194
+ if (!location) {
7195
+ throw new Error("Web server returned a redirect without a location");
7196
+ }
7197
+ if (redirects === MAX_REDIRECTS) {
7198
+ throw new Error(`Web request exceeded ${MAX_REDIRECTS} redirects`);
7199
+ }
7200
+ currentUrl = new URL(location, currentUrl);
7201
+ continue;
7202
+ }
7203
+ if (!res.ok) {
7204
+ throw new Error(`Web server responded with status ${res.status}`);
7205
+ }
7206
+ const contentType = res.headers.get("content-type") || "";
7207
+ if (!contentType.includes("text/html") && !contentType.includes("text/plain") && !contentType.includes("application/xhtml+xml") && !contentType.includes("text/xml")) {
7208
+ throw new Error(`Unsupported content type: ${contentType}`);
7209
+ }
7210
+ const declaredLength = Number(res.headers.get("content-length"));
7211
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_SOURCE_BYTES) {
7212
+ throw new Error("Response body exceeds 2MB limit");
7213
+ }
7214
+ const text = await readResponseBodyWithLimit(res, MAX_SOURCE_BYTES);
7215
+ if (contentType.includes("text/html") || contentType.includes("application/xhtml+xml")) {
7216
+ return cleanHtml(text);
7217
+ }
7218
+ return text;
7219
+ }
7220
+ throw new Error("Web request failed");
7221
+ } catch (err) {
7222
+ if (err instanceof Error && err.name === "AbortError") {
7223
+ throw new Error("Connection request timed out after 10 seconds");
7224
+ }
7225
+ throw err;
7226
+ } finally {
7227
+ clearTimeout(timeoutId);
7228
+ }
7229
+ }
7230
+ async function readResponseBodyWithLimit(response, maxBytes) {
7231
+ if (!response.body) return "";
7232
+ const reader = response.body.getReader();
7233
+ const decoder = new TextDecoder();
7234
+ let totalBytes = 0;
7235
+ let text = "";
7236
+ while (true) {
7237
+ const { done, value } = await reader.read();
7238
+ if (done) break;
7239
+ totalBytes += value.byteLength;
7240
+ if (totalBytes > maxBytes) {
7241
+ await reader.cancel();
7242
+ throw new Error("Response body exceeds 2MB limit");
7243
+ }
7244
+ text += decoder.decode(value, { stream: true });
7245
+ }
7246
+ return text + decoder.decode();
7247
+ }
7248
+ async function readImageOCR(db, imagePath) {
7249
+ return extractTextFromScanViaLLM(db, imagePath);
7250
+ }
7251
+
6340
7252
  // src/cli/llm/vision.ts
6341
7253
  import { randomBytes } from "crypto";
6342
- import { readFileSync as readFileSync9 } from "fs";
7254
+ import { readFileSync as readFileSync10 } from "fs";
6343
7255
  import { tmpdir as tmpdir2 } from "os";
6344
7256
  import { basename as basename3, join as join14 } from "path";
6345
7257
  var LANGUAGE_NAMES2 = {
@@ -6412,7 +7324,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
6412
7324
  }
6413
7325
  }
6414
7326
  for (const file of sampledFiles) {
6415
- const bytes = readFileSync9(join14(tempDir, file));
7327
+ const bytes = readFileSync10(join14(tempDir, file));
6416
7328
  imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
6417
7329
  }
6418
7330
  } finally {
@@ -6422,7 +7334,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
6422
7334
  }
6423
7335
  }
6424
7336
  } else {
6425
- const imageBytes = readFileSync9(input8.imagePath);
7337
+ const imageBytes = readFileSync10(input8.imagePath);
6426
7338
  const ext = input8.imagePath.split(".").pop()?.toLowerCase();
6427
7339
  const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
6428
7340
  imageUrls.push(`data:${mime};base64,${imageBytes.toString("base64")}`);
@@ -6890,7 +7802,7 @@ import {
6890
7802
  existsSync as existsSync15,
6891
7803
  lstatSync,
6892
7804
  mkdirSync as mkdirSync8,
6893
- readFileSync as readFileSync10,
7805
+ readFileSync as readFileSync11,
6894
7806
  realpathSync,
6895
7807
  rmSync as rmSync2,
6896
7808
  symlinkSync,
@@ -6978,7 +7890,7 @@ function isZamSkillCopy(destinationDir) {
6978
7890
  if (!lstatSync(destinationDir).isDirectory()) return false;
6979
7891
  const skillFile = join15(destinationDir, "SKILL.md");
6980
7892
  if (!existsSync15(skillFile)) return false;
6981
- return /^name:\s*zam\s*$/m.test(readFileSync10(skillFile, "utf8"));
7893
+ return /^name:\s*zam\s*$/m.test(readFileSync11(skillFile, "utf8"));
6982
7894
  } catch {
6983
7895
  return false;
6984
7896
  }
@@ -7111,7 +8023,7 @@ ${ZAM_BLOCK_END}`;
7111
8023
  }
7112
8024
  return "write";
7113
8025
  }
7114
- const existing = readFileSync10(dest, "utf8");
8026
+ const existing = readFileSync11(dest, "utf8");
7115
8027
  if (existing.includes(block)) return "skip";
7116
8028
  const start = existing.indexOf(ZAM_BLOCK_START);
7117
8029
  const end = existing.indexOf(ZAM_BLOCK_END);
@@ -8646,7 +9558,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
8646
9558
  return;
8647
9559
  }
8648
9560
  }
8649
- const imageBytes = readFileSync11(outputPath);
9561
+ const imageBytes = readFileSync12(outputPath);
8650
9562
  const base64 = imageBytes.toString("base64");
8651
9563
  jsonOut2({
8652
9564
  sessionId: opts.session ?? null,
@@ -8768,7 +9680,7 @@ bridgeCommand.command("stop-recording").description(
8768
9680
  }
8769
9681
  const sessionId = opts.session;
8770
9682
  const statePath = join18(tmpdir3(), `zam-recording-${sessionId}.json`);
8771
- const { existsSync: existsSync26, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
9683
+ const { existsSync: existsSync26, readFileSync: readFileSync17, rmSync: rmSync4 } = await import("fs");
8772
9684
  if (!existsSync26(statePath)) {
8773
9685
  jsonOut2({
8774
9686
  sessionId,
@@ -8777,7 +9689,7 @@ bridgeCommand.command("stop-recording").description(
8777
9689
  });
8778
9690
  return;
8779
9691
  }
8780
- const state = JSON.parse(readFileSync16(statePath, "utf8"));
9692
+ const state = JSON.parse(readFileSync17(statePath, "utf8"));
8781
9693
  const { pid, outputPath } = state;
8782
9694
  try {
8783
9695
  process.kill(pid, "SIGINT");
@@ -9291,6 +10203,388 @@ bridgeCommand.command("get-neighborhood").description(
9291
10203
  });
9292
10204
  });
9293
10205
  });
10206
+ 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) => {
10207
+ await withDb2(async (db) => {
10208
+ const userId = await resolveUser(opts, db, { json: true });
10209
+ const cards = await listPersonalCards(db, userId, {
10210
+ query: opts.query,
10211
+ domain: opts.domain
10212
+ });
10213
+ jsonOut2({ cards });
10214
+ });
10215
+ });
10216
+ 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(
10217
+ "--mode <mode>",
10218
+ "Symbiosis mode: shadowing | copilot | autonomy | none",
10219
+ "none"
10220
+ ).option("--context <context>", "Context description", "").action(async (opts) => {
10221
+ await withDb2(async (db) => {
10222
+ const userId = await resolveUser(opts, db, { json: true });
10223
+ const bloom = Number(opts.bloom);
10224
+ if (bloom < 1 || bloom > 5) {
10225
+ jsonError("bloom must be between 1 and 5");
10226
+ }
10227
+ const mode = opts.mode === "none" ? null : opts.mode;
10228
+ if (mode && !["shadowing", "copilot", "autonomy"].includes(mode)) {
10229
+ jsonError(`Invalid mode: ${opts.mode}`);
10230
+ }
10231
+ const slug = await generateTokenSlug(
10232
+ db,
10233
+ opts.domain,
10234
+ opts.concept,
10235
+ opts.question
10236
+ );
10237
+ let question = opts.question || null;
10238
+ if (!question) {
10239
+ question = generateConceptFreeCue(bloom, slug, opts.domain);
10240
+ }
10241
+ const { token, card } = await db.transaction(async (tx) => {
10242
+ const createdToken = await createToken(tx, {
10243
+ slug,
10244
+ concept: opts.concept,
10245
+ domain: opts.domain,
10246
+ bloom_level: bloom,
10247
+ context: opts.context,
10248
+ symbiosis_mode: mode,
10249
+ source_link: opts.sourceLink || null,
10250
+ question
10251
+ });
10252
+ const createdCard = await ensureCard(tx, createdToken.id, userId);
10253
+ return { token: createdToken, card: createdCard };
10254
+ });
10255
+ jsonOut2({
10256
+ success: true,
10257
+ token: {
10258
+ id: token.id,
10259
+ slug: token.slug,
10260
+ concept: token.concept,
10261
+ domain: token.domain,
10262
+ bloomLevel: token.bloom_level,
10263
+ context: token.context,
10264
+ symbiosisMode: token.symbiosis_mode,
10265
+ sourceLink: token.source_link,
10266
+ question: token.question,
10267
+ createdAt: token.created_at,
10268
+ updatedAt: token.updated_at
10269
+ },
10270
+ card: {
10271
+ id: card.id,
10272
+ tokenId: card.token_id,
10273
+ userId: card.user_id,
10274
+ state: card.state,
10275
+ dueAt: card.due_at,
10276
+ blocked: card.blocked
10277
+ }
10278
+ });
10279
+ });
10280
+ });
10281
+ 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(
10282
+ "--mode <mode>",
10283
+ "Updated symbiosis mode: shadowing | copilot | autonomy | none"
10284
+ ).option("--source-link <link>", "Updated source link").option("--question <question>", "Updated question text").action(async (opts) => {
10285
+ await withDb2(async (db) => {
10286
+ const userId = await resolveUser(opts, db, { json: true });
10287
+ const updates = {};
10288
+ if (opts.concept !== void 0) updates.concept = opts.concept;
10289
+ if (opts.domain !== void 0) updates.domain = opts.domain;
10290
+ if (opts.bloom !== void 0) {
10291
+ const bloom = Number(opts.bloom);
10292
+ if (bloom < 1 || bloom > 5) {
10293
+ jsonError("bloom must be between 1 and 5");
10294
+ }
10295
+ updates.bloom_level = bloom;
10296
+ }
10297
+ if (opts.context !== void 0) updates.context = opts.context;
10298
+ if (opts.sourceLink !== void 0) {
10299
+ updates.source_link = opts.sourceLink === "" ? null : opts.sourceLink;
10300
+ }
10301
+ if (opts.question !== void 0) {
10302
+ updates.question = opts.question === "" ? null : opts.question;
10303
+ }
10304
+ if (opts.mode !== void 0) {
10305
+ const validModes = ["shadowing", "copilot", "autonomy", "none"];
10306
+ if (!validModes.includes(opts.mode)) {
10307
+ jsonError(`Invalid mode: ${opts.mode}`);
10308
+ }
10309
+ updates.symbiosis_mode = opts.mode === "none" ? null : opts.mode;
10310
+ }
10311
+ const token = await db.transaction(async (tx) => {
10312
+ const existingToken = await getTokenBySlug(tx, opts.slug);
10313
+ if (!existingToken) {
10314
+ throw new Error(`Token not found: ${opts.slug}`);
10315
+ }
10316
+ const card = await getCard(tx, existingToken.id, userId);
10317
+ if (!card) {
10318
+ throw new Error(
10319
+ `Card not found for token ${opts.slug} and user ${userId}`
10320
+ );
10321
+ }
10322
+ return updateToken(tx, opts.slug, updates);
10323
+ });
10324
+ jsonOut2({
10325
+ success: true,
10326
+ token: {
10327
+ id: token.id,
10328
+ slug: token.slug,
10329
+ concept: token.concept,
10330
+ domain: token.domain,
10331
+ bloomLevel: token.bloom_level,
10332
+ context: token.context,
10333
+ symbiosisMode: token.symbiosis_mode,
10334
+ sourceLink: token.source_link,
10335
+ question: token.question,
10336
+ createdAt: token.created_at,
10337
+ updatedAt: token.updated_at
10338
+ }
10339
+ });
10340
+ });
10341
+ });
10342
+ bridgeCommand.command("personal-card-remove").description(
10343
+ "Remove a personal card (optionally previewing the effects) (JSON)"
10344
+ ).option("--user <id>", "User ID (default: whoami)").requiredOption("--slug <slug>", "Token slug").option("--confirm", "Perform the deletion instead of a preview").action(async (opts) => {
10345
+ await withDb2(async (db) => {
10346
+ const userId = await resolveUser(opts, db, { json: true });
10347
+ const token = await getTokenBySlug(db, opts.slug);
10348
+ if (!token) {
10349
+ jsonError(`Token not found: ${opts.slug}`);
10350
+ }
10351
+ const card = await getCard(db, token.id, userId);
10352
+ if (!card) {
10353
+ jsonError(`Card not found for token ${opts.slug} and user ${userId}`);
10354
+ }
10355
+ if (!opts.confirm) {
10356
+ const impact = await getCardDeletionImpact(db, token.id, userId);
10357
+ jsonOut2({
10358
+ success: true,
10359
+ preview: true,
10360
+ requiresConfirmation: true,
10361
+ token: { id: token.id, slug: token.slug },
10362
+ impact
10363
+ });
10364
+ return;
10365
+ }
10366
+ const result = await deleteCardForUser(db, token.id, userId);
10367
+ jsonOut2({
10368
+ success: true,
10369
+ deletedCard: {
10370
+ id: result.card.id,
10371
+ tokenId: result.card.token_id,
10372
+ userId: result.card.user_id
10373
+ },
10374
+ impact: result.impact
10375
+ });
10376
+ });
10377
+ });
10378
+ 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) => {
10379
+ await withDb2(async (db) => {
10380
+ const token = await getTokenBySlug(db, opts.slug);
10381
+ if (!token) {
10382
+ jsonError(`Token not found: ${opts.slug}`);
10383
+ }
10384
+ if (!opts.confirm) {
10385
+ const impact = await getTokenDeleteImpact(db, opts.slug);
10386
+ jsonOut2({
10387
+ success: true,
10388
+ preview: true,
10389
+ requiresConfirmation: true,
10390
+ token: { id: token.id, slug: token.slug },
10391
+ impact
10392
+ });
10393
+ return;
10394
+ }
10395
+ const result = await deleteToken(db, opts.slug);
10396
+ jsonOut2({
10397
+ success: true,
10398
+ deletedToken: {
10399
+ id: result.token.id,
10400
+ slug: result.token.slug
10401
+ },
10402
+ impact: result.impact
10403
+ });
10404
+ });
10405
+ });
10406
+ 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(
10407
+ "--domain <domain>",
10408
+ "Default category/domain for imported cards"
10409
+ ).option("--source <url>", "Provenance source link or URL").option("--preview", "Return parsed cards without saving them").action(async (opts) => {
10410
+ await withDb2(async (db) => {
10411
+ const userId = await resolveUser(opts, db, { json: true });
10412
+ const cards = await importCurriculumViaLLM(
10413
+ db,
10414
+ opts.text,
10415
+ opts.domain,
10416
+ opts.source || null
10417
+ );
10418
+ if (opts.preview) {
10419
+ jsonOut2({
10420
+ success: true,
10421
+ proposals: cards
10422
+ });
10423
+ return;
10424
+ }
10425
+ const result = await importCurriculumCards(db, userId, cards);
10426
+ jsonOut2({
10427
+ success: true,
10428
+ createdCount: result.createdCount,
10429
+ ensuredCount: result.ensuredCount
10430
+ });
10431
+ });
10432
+ });
10433
+ bridgeCommand.command("personal-card-split-proposals").description("Generate atomic proposals for splitting a card (JSON)").requiredOption("--slug <slug>", "Original token slug").action(async (opts) => {
10434
+ await withDb2(async (db) => {
10435
+ const token = await getTokenBySlug(db, opts.slug);
10436
+ if (!token) {
10437
+ jsonError(`Token not found: ${opts.slug}`);
10438
+ }
10439
+ const proposals = await generateSplitProposalsViaLLM(db, token);
10440
+ jsonOut2({
10441
+ success: true,
10442
+ proposals
10443
+ });
10444
+ });
10445
+ });
10446
+ 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(
10447
+ "--action <action>",
10448
+ "Original card action: 'block' or 'remove'"
10449
+ ).requiredOption(
10450
+ "--original-question <question>",
10451
+ "Rewritten question of the original card"
10452
+ ).requiredOption(
10453
+ "--original-concept <concept>",
10454
+ "Rewritten concept of the original card"
10455
+ ).requiredOption(
10456
+ "--proposals <json>",
10457
+ "JSON string representing proposals array"
10458
+ ).action(async (opts) => {
10459
+ await withDb2(async (db) => {
10460
+ const userId = await resolveUser(opts, db, { json: true });
10461
+ const proposals = JSON.parse(opts.proposals);
10462
+ const result = await confirmCardSplit(
10463
+ db,
10464
+ userId,
10465
+ opts.slug,
10466
+ opts.action,
10467
+ opts.originalQuestion,
10468
+ opts.originalConcept,
10469
+ proposals
10470
+ );
10471
+ jsonOut2({
10472
+ success: true,
10473
+ createdCount: result.createdCount,
10474
+ ensuredCount: result.ensuredCount
10475
+ });
10476
+ });
10477
+ });
10478
+ bridgeCommand.command("personal-card-foundations-proposals").description("Generate prerequisite suggestions for a card (JSON)").requiredOption("--slug <slug>", "Original token slug").action(async (opts) => {
10479
+ await withDb2(async (db) => {
10480
+ const token = await getTokenBySlug(db, opts.slug);
10481
+ if (!token) {
10482
+ jsonError(`Token not found: ${opts.slug}`);
10483
+ }
10484
+ const proposals = await generateFoundationsProposalsViaLLM(db, token);
10485
+ const resolvedProposals = [];
10486
+ for (const prop of proposals) {
10487
+ const baseText = prop.question && prop.question.trim().length > 0 ? prop.question : prop.concept;
10488
+ const cleanDomain = slugify(prop.domain || "");
10489
+ const cleanBase = slugify(baseText);
10490
+ let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
10491
+ if (baseSlug.length > 60) {
10492
+ baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
10493
+ }
10494
+ if (!baseSlug) {
10495
+ baseSlug = "token";
10496
+ }
10497
+ const existingToken = await getTokenBySlug(db, baseSlug);
10498
+ if (existingToken) {
10499
+ resolvedProposals.push({
10500
+ ...prop,
10501
+ exists: true,
10502
+ slug: existingToken.slug,
10503
+ question: existingToken.question || prop.question,
10504
+ concept: existingToken.concept,
10505
+ domain: existingToken.domain
10506
+ });
10507
+ } else {
10508
+ resolvedProposals.push({
10509
+ ...prop,
10510
+ exists: false,
10511
+ slug: null
10512
+ });
10513
+ }
10514
+ }
10515
+ jsonOut2({
10516
+ success: true,
10517
+ proposals: resolvedProposals
10518
+ });
10519
+ });
10520
+ });
10521
+ 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(
10522
+ "--proposals <json>",
10523
+ "JSON string representing proposals array"
10524
+ ).action(async (opts) => {
10525
+ await withDb2(async (db) => {
10526
+ const userId = await resolveUser(opts, db, { json: true });
10527
+ const proposals = JSON.parse(opts.proposals);
10528
+ const result = await confirmFoundations(db, userId, opts.slug, proposals);
10529
+ jsonOut2({
10530
+ success: true,
10531
+ createdCount: result.createdCount,
10532
+ linkedCount: result.linkedCount
10533
+ });
10534
+ });
10535
+ });
10536
+ bridgeCommand.command("personal-source-import").description(
10537
+ "Fetch and clean plain text from local file, web link, or vision scan (JSON)"
10538
+ ).requiredOption("--type <file|web|scan>", "Source type").requiredOption("--uri <uri>", "Source path or URL").action(async (opts) => {
10539
+ await withDb2(async (db) => {
10540
+ let content = "";
10541
+ if (opts.type === "file") {
10542
+ content = await readLocalFile(opts.uri);
10543
+ } else if (opts.type === "web") {
10544
+ content = await readWebLink(opts.uri);
10545
+ } else if (opts.type === "scan") {
10546
+ content = await readImageOCR(db, opts.uri);
10547
+ } else {
10548
+ throw new Error(`Invalid source type: ${opts.type}`);
10549
+ }
10550
+ const sourceId = ulid8();
10551
+ await db.prepare(
10552
+ `INSERT INTO sources (id, type, uri, content)
10553
+ VALUES (?, ?, ?, ?)
10554
+ ON CONFLICT(uri) DO UPDATE SET
10555
+ type = excluded.type,
10556
+ content = excluded.content`
10557
+ ).run(sourceId, opts.type, opts.uri, content);
10558
+ const record = await db.prepare("SELECT id, content FROM sources WHERE uri = ?").get(opts.uri);
10559
+ jsonOut2({
10560
+ success: true,
10561
+ sourceId: record.id,
10562
+ content: record.content
10563
+ });
10564
+ });
10565
+ });
10566
+ bridgeCommand.command("personal-source-confirm-import").description(
10567
+ "Confirm and save cards generated from a source reference (JSON)"
10568
+ ).option("--user <id>", "User ID (default: whoami)").requiredOption("--sourceId <id>", "Source database ID").requiredOption(
10569
+ "--proposals <json>",
10570
+ "JSON string representing proposals array"
10571
+ ).action(async (opts) => {
10572
+ await withDb2(async (db) => {
10573
+ const userId = await resolveUser(opts, db, { json: true });
10574
+ const proposals = JSON.parse(opts.proposals);
10575
+ const result = await confirmSourceImport(
10576
+ db,
10577
+ userId,
10578
+ opts.sourceId,
10579
+ proposals
10580
+ );
10581
+ jsonOut2({
10582
+ success: true,
10583
+ createdCount: result.createdCount,
10584
+ ensuredCount: result.linkedCount
10585
+ });
10586
+ });
10587
+ });
9294
10588
  bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/stdout server").option("--stdin", "Use stdin/stdout for communication").action(async (_opts) => {
9295
10589
  isServeMode = true;
9296
10590
  const {
@@ -11525,7 +12819,7 @@ ${"\u2550".repeat(50)}`);
11525
12819
  });
11526
12820
 
11527
12821
  // src/cli/commands/session.ts
11528
- import { readFileSync as readFileSync12 } from "fs";
12822
+ import { readFileSync as readFileSync13 } from "fs";
11529
12823
  import { input as input6, select as select2 } from "@inquirer/prompts";
11530
12824
  import { Command as Command14 } from "commander";
11531
12825
  var sessionCommand = new Command14("session").description(
@@ -11717,7 +13011,7 @@ function loadPatternFile(path) {
11717
13011
  if (!path) return [];
11718
13012
  let parsed;
11719
13013
  try {
11720
- parsed = JSON.parse(readFileSync12(path, "utf-8"));
13014
+ parsed = JSON.parse(readFileSync13(path, "utf-8"));
11721
13015
  } catch (err) {
11722
13016
  throw new Error(
11723
13017
  `Cannot read synthesis patterns from ${path}: ${err.message}`
@@ -12266,7 +13560,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
12266
13560
  });
12267
13561
 
12268
13562
  // src/cli/commands/snapshot.ts
12269
- import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
13563
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
12270
13564
  import { dirname as dirname7, join as join21 } from "path";
12271
13565
  import { Command as Command18 } from "commander";
12272
13566
  function defaultOutName() {
@@ -12319,7 +13613,7 @@ var importCmd = new Command18("import").description("Restore a snapshot into the
12319
13613
  console.error(`Error: Snapshot file not found: ${file}`);
12320
13614
  process.exit(1);
12321
13615
  }
12322
- const snapshot = readFileSync13(file, "utf-8");
13616
+ const snapshot = readFileSync14(file, "utf-8");
12323
13617
  db = await openDatabaseWithSync({ initialize: true });
12324
13618
  const result = await importSnapshot(db, snapshot, { force: opts.force });
12325
13619
  await db.close();
@@ -12341,7 +13635,7 @@ var verifyCmd = new Command18("verify").description("Check a snapshot's manifest
12341
13635
  console.error(`Error: Snapshot file not found: ${file}`);
12342
13636
  process.exit(1);
12343
13637
  }
12344
- const manifest = verifySnapshot(readFileSync13(file, "utf-8"));
13638
+ const manifest = verifySnapshot(readFileSync14(file, "utf-8"));
12345
13639
  const { total, nonEmpty } = summarize(manifest.tables);
12346
13640
  console.log(`Valid snapshot (format v${manifest.version})`);
12347
13641
  console.log(` created: ${manifest.createdAt}`);
@@ -12969,7 +14263,7 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
12969
14263
 
12970
14264
  // src/cli/commands/update.ts
12971
14265
  import { spawnSync as spawnSync2 } from "child_process";
12972
- import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "fs";
14266
+ import { existsSync as existsSync24, readFileSync as readFileSync15, realpathSync as realpathSync2 } from "fs";
12973
14267
  import { dirname as dirname9, join as join23 } from "path";
12974
14268
  import { fileURLToPath as fileURLToPath4 } from "url";
12975
14269
  import { confirm as confirm3 } from "@inquirer/prompts";
@@ -12995,7 +14289,7 @@ function currentVersion() {
12995
14289
  for (const up of ["..", "../..", "../../.."]) {
12996
14290
  try {
12997
14291
  const pkg2 = JSON.parse(
12998
- readFileSync14(join23(here, up, "package.json"), "utf-8")
14292
+ readFileSync15(join23(here, up, "package.json"), "utf-8")
12999
14293
  );
13000
14294
  if (pkg2.version) return pkg2.version;
13001
14295
  } catch {
@@ -13006,7 +14300,7 @@ function currentVersion() {
13006
14300
  function versionAt(dir) {
13007
14301
  try {
13008
14302
  const pkg2 = JSON.parse(
13009
- readFileSync14(join23(dir, "package.json"), "utf-8")
14303
+ readFileSync15(join23(dir, "package.json"), "utf-8")
13010
14304
  );
13011
14305
  return pkg2.version ?? "unknown";
13012
14306
  } catch {
@@ -13641,7 +14935,7 @@ workspaceCommand.command("backup").description("Back up the local ZAM database i
13641
14935
  // src/cli/index.ts
13642
14936
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
13643
14937
  var pkg = JSON.parse(
13644
- readFileSync15(join25(__dirname, "..", "..", "package.json"), "utf-8")
14938
+ readFileSync16(join25(__dirname, "..", "..", "package.json"), "utf-8")
13645
14939
  );
13646
14940
  var program = new Command25();
13647
14941
  program.name("zam").description(