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 +1356 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +105 -1
- package/dist/index.js +482 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
4639
|
-
const
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
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
|
|
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
|
|
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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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:
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14958
|
+
readFileSync16(join25(__dirname, "..", "..", "package.json"), "utf-8")
|
|
13645
14959
|
);
|
|
13646
14960
|
var program = new Command25();
|
|
13647
14961
|
program.name("zam").description(
|