zam-core 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2046 -189
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +107 -6
- package/dist/index.js +331 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -530,6 +530,7 @@ var SCHEMA = `
|
|
|
530
530
|
CREATE TABLE IF NOT EXISTS tokens (
|
|
531
531
|
id TEXT PRIMARY KEY,
|
|
532
532
|
slug TEXT UNIQUE NOT NULL,
|
|
533
|
+
title TEXT NOT NULL DEFAULT '',
|
|
533
534
|
concept TEXT NOT NULL,
|
|
534
535
|
domain TEXT NOT NULL DEFAULT '',
|
|
535
536
|
bloom_level INTEGER NOT NULL DEFAULT 1 CHECK (bloom_level BETWEEN 1 AND 5),
|
|
@@ -668,6 +669,22 @@ CREATE TABLE IF NOT EXISTS agent_skills (
|
|
|
668
669
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
669
670
|
);
|
|
670
671
|
|
|
672
|
+
-- Knowledge contexts: work, school, private
|
|
673
|
+
CREATE TABLE IF NOT EXISTS contexts (
|
|
674
|
+
id TEXT PRIMARY KEY,
|
|
675
|
+
name TEXT NOT NULL UNIQUE,
|
|
676
|
+
label TEXT,
|
|
677
|
+
language TEXT,
|
|
678
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
-- Join table mapping tokens to their knowledge contexts
|
|
682
|
+
CREATE TABLE IF NOT EXISTS token_contexts (
|
|
683
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
684
|
+
context_id TEXT NOT NULL REFERENCES contexts(id) ON DELETE CASCADE,
|
|
685
|
+
PRIMARY KEY (token_id, context_id)
|
|
686
|
+
);
|
|
687
|
+
|
|
671
688
|
-- Performance indexes
|
|
672
689
|
CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain);
|
|
673
690
|
CREATE INDEX IF NOT EXISTS idx_tokens_slug ON tokens(slug);
|
|
@@ -678,6 +695,8 @@ CREATE INDEX IF NOT EXISTS idx_cards_token_user ON cards(token_id, user_id);
|
|
|
678
695
|
CREATE INDEX IF NOT EXISTS idx_review_logs_card ON review_logs(card_id);
|
|
679
696
|
CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed_at);
|
|
680
697
|
CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
|
|
698
|
+
CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title);
|
|
699
|
+
CREATE INDEX IF NOT EXISTS idx_token_contexts_context ON token_contexts(context_id);
|
|
681
700
|
`;
|
|
682
701
|
|
|
683
702
|
// src/kernel/db/sync-adapter.ts
|
|
@@ -1011,6 +1030,34 @@ async function runMigrations(db) {
|
|
|
1011
1030
|
embedded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1012
1031
|
)
|
|
1013
1032
|
`);
|
|
1033
|
+
if (!tokenCols.some((c) => c.name === "title")) {
|
|
1034
|
+
await db.exec(
|
|
1035
|
+
`ALTER TABLE tokens ADD COLUMN title TEXT NOT NULL DEFAULT ''`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
await db.exec(`CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title)`);
|
|
1039
|
+
await db.exec(
|
|
1040
|
+
`CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain)`
|
|
1041
|
+
);
|
|
1042
|
+
await db.exec(`
|
|
1043
|
+
CREATE TABLE IF NOT EXISTS contexts (
|
|
1044
|
+
id TEXT PRIMARY KEY,
|
|
1045
|
+
name TEXT NOT NULL UNIQUE,
|
|
1046
|
+
label TEXT,
|
|
1047
|
+
language TEXT,
|
|
1048
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1049
|
+
)
|
|
1050
|
+
`);
|
|
1051
|
+
await db.exec(`
|
|
1052
|
+
CREATE TABLE IF NOT EXISTS token_contexts (
|
|
1053
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
1054
|
+
context_id TEXT NOT NULL REFERENCES contexts(id) ON DELETE CASCADE,
|
|
1055
|
+
PRIMARY KEY (token_id, context_id)
|
|
1056
|
+
)
|
|
1057
|
+
`);
|
|
1058
|
+
await db.exec(`
|
|
1059
|
+
CREATE INDEX IF NOT EXISTS idx_token_contexts_context ON token_contexts(context_id)
|
|
1060
|
+
`);
|
|
1014
1061
|
}
|
|
1015
1062
|
|
|
1016
1063
|
// src/kernel/db/snapshot.ts
|
|
@@ -1474,24 +1521,27 @@ async function deleteCardForUser(db, tokenId, userId) {
|
|
|
1474
1521
|
await db.prepare("DELETE FROM cards WHERE id = ?").run(card.id);
|
|
1475
1522
|
return { card, impact };
|
|
1476
1523
|
}
|
|
1477
|
-
async function getDueCards(db, userId, now, domain) {
|
|
1524
|
+
async function getDueCards(db, userId, now, domain, knowledgeContext) {
|
|
1478
1525
|
const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1526
|
+
let sql = `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
1527
|
+
FROM cards c
|
|
1528
|
+
JOIN tokens t ON t.id = c.token_id
|
|
1529
|
+
WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ?`;
|
|
1530
|
+
const params = [userId, cutoff];
|
|
1479
1531
|
if (domain) {
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
FROM cards c
|
|
1483
|
-
JOIN tokens t ON t.id = c.token_id
|
|
1484
|
-
WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ? AND t.domain = ?
|
|
1485
|
-
ORDER BY t.bloom_level ASC, c.due_at ASC`
|
|
1486
|
-
).all(userId, cutoff, domain);
|
|
1532
|
+
sql += " AND t.domain = ?";
|
|
1533
|
+
params.push(domain);
|
|
1487
1534
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1535
|
+
if (knowledgeContext) {
|
|
1536
|
+
sql += ` AND EXISTS (
|
|
1537
|
+
SELECT 1 FROM token_contexts tc
|
|
1538
|
+
INNER JOIN contexts context_filter ON context_filter.id = tc.context_id
|
|
1539
|
+
WHERE tc.token_id = t.id AND context_filter.name = ?
|
|
1540
|
+
)`;
|
|
1541
|
+
params.push(knowledgeContext);
|
|
1542
|
+
}
|
|
1543
|
+
sql += " ORDER BY t.bloom_level ASC, c.due_at ASC";
|
|
1544
|
+
return await db.prepare(sql).all(...params);
|
|
1495
1545
|
}
|
|
1496
1546
|
async function getBlockedCards(db, userId) {
|
|
1497
1547
|
return await db.prepare(
|
|
@@ -1503,21 +1553,128 @@ async function getBlockedCards(db, userId) {
|
|
|
1503
1553
|
).all(userId);
|
|
1504
1554
|
}
|
|
1505
1555
|
|
|
1506
|
-
// src/kernel/models/
|
|
1556
|
+
// src/kernel/models/knowledge-context.ts
|
|
1507
1557
|
import { ulid as ulid3 } from "ulid";
|
|
1508
|
-
|
|
1558
|
+
function normalizeContextName(name) {
|
|
1559
|
+
const normalized = name.trim();
|
|
1560
|
+
if (!normalized) {
|
|
1561
|
+
throw new Error("Context name cannot be empty");
|
|
1562
|
+
}
|
|
1563
|
+
return normalized;
|
|
1564
|
+
}
|
|
1565
|
+
function normalizeOptionalText(value) {
|
|
1566
|
+
if (value == null) return null;
|
|
1567
|
+
const normalized = value.trim();
|
|
1568
|
+
return normalized || null;
|
|
1569
|
+
}
|
|
1570
|
+
async function createKnowledgeContext(db, input) {
|
|
1509
1571
|
const id = ulid3();
|
|
1510
1572
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1573
|
+
const name = normalizeContextName(input.name);
|
|
1574
|
+
const existing = await getKnowledgeContextByName(db, name);
|
|
1575
|
+
if (existing) {
|
|
1576
|
+
throw new Error(`Knowledge context with name "${name}" already exists`);
|
|
1577
|
+
}
|
|
1578
|
+
await db.prepare(
|
|
1579
|
+
`INSERT INTO contexts (id, name, label, language, created_at)
|
|
1580
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
1581
|
+
).run(
|
|
1582
|
+
id,
|
|
1583
|
+
name,
|
|
1584
|
+
normalizeOptionalText(input.label),
|
|
1585
|
+
normalizeOptionalText(input.language),
|
|
1586
|
+
now
|
|
1587
|
+
);
|
|
1588
|
+
const context = await getKnowledgeContextById(db, id);
|
|
1589
|
+
if (!context) {
|
|
1590
|
+
throw new Error(
|
|
1591
|
+
`Failed to retrieve newly created knowledge context: ${id}`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
return context;
|
|
1595
|
+
}
|
|
1596
|
+
async function getKnowledgeContextByName(db, name) {
|
|
1597
|
+
const normalized = name.trim();
|
|
1598
|
+
if (!normalized) return void 0;
|
|
1599
|
+
return await db.prepare("SELECT * FROM contexts WHERE name = ?").get(normalized);
|
|
1600
|
+
}
|
|
1601
|
+
async function getKnowledgeContextById(db, id) {
|
|
1602
|
+
return await db.prepare("SELECT * FROM contexts WHERE id = ?").get(id);
|
|
1603
|
+
}
|
|
1604
|
+
async function listKnowledgeContexts(db) {
|
|
1605
|
+
return await db.prepare("SELECT * FROM contexts ORDER BY name ASC").all();
|
|
1606
|
+
}
|
|
1607
|
+
async function updateKnowledgeContext(db, id, updates) {
|
|
1608
|
+
const context = await getKnowledgeContextById(db, id);
|
|
1609
|
+
if (!context) {
|
|
1610
|
+
throw new Error(`Knowledge context not found: ${id}`);
|
|
1611
|
+
}
|
|
1612
|
+
const fields = [];
|
|
1613
|
+
const values = [];
|
|
1614
|
+
if (updates.name !== void 0) {
|
|
1615
|
+
const name = normalizeContextName(updates.name);
|
|
1616
|
+
if (name !== context.name) {
|
|
1617
|
+
const existing = await getKnowledgeContextByName(db, name);
|
|
1618
|
+
if (existing) {
|
|
1619
|
+
throw new Error(`Knowledge context with name "${name}" already exists`);
|
|
1620
|
+
}
|
|
1621
|
+
fields.push("name = ?");
|
|
1622
|
+
values.push(name);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (updates.label !== void 0) {
|
|
1626
|
+
fields.push("label = ?");
|
|
1627
|
+
values.push(normalizeOptionalText(updates.label));
|
|
1628
|
+
}
|
|
1629
|
+
if (updates.language !== void 0) {
|
|
1630
|
+
fields.push("language = ?");
|
|
1631
|
+
values.push(normalizeOptionalText(updates.language));
|
|
1632
|
+
}
|
|
1633
|
+
if (fields.length === 0) {
|
|
1634
|
+
return context;
|
|
1635
|
+
}
|
|
1636
|
+
values.push(id);
|
|
1637
|
+
await db.prepare(`UPDATE contexts SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
1638
|
+
return await getKnowledgeContextById(db, id);
|
|
1639
|
+
}
|
|
1640
|
+
async function deleteKnowledgeContext(db, id) {
|
|
1641
|
+
await db.prepare("DELETE FROM contexts WHERE id = ?").run(id);
|
|
1642
|
+
}
|
|
1643
|
+
async function assignTokenToContext(db, tokenId, contextId) {
|
|
1644
|
+
await db.prepare(
|
|
1645
|
+
`INSERT OR IGNORE INTO token_contexts (token_id, context_id)
|
|
1646
|
+
VALUES (?, ?)`
|
|
1647
|
+
).run(tokenId, contextId);
|
|
1648
|
+
}
|
|
1649
|
+
async function unassignTokenFromContext(db, tokenId, contextId) {
|
|
1650
|
+
await db.prepare("DELETE FROM token_contexts WHERE token_id = ? AND context_id = ?").run(tokenId, contextId);
|
|
1651
|
+
}
|
|
1652
|
+
async function listContextsForToken(db, tokenId) {
|
|
1653
|
+
return await db.prepare(
|
|
1654
|
+
`SELECT c.* FROM contexts c
|
|
1655
|
+
INNER JOIN token_contexts tc ON tc.context_id = c.id
|
|
1656
|
+
WHERE tc.token_id = ?
|
|
1657
|
+
ORDER BY c.name ASC`
|
|
1658
|
+
).all(tokenId);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/kernel/models/token.ts
|
|
1662
|
+
import { ulid as ulid4 } from "ulid";
|
|
1663
|
+
async function createToken(db, input) {
|
|
1664
|
+
const id = ulid4();
|
|
1665
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1511
1666
|
const bloom = input.bloom_level ?? 1;
|
|
1512
1667
|
if (bloom < 1 || bloom > 5) {
|
|
1513
1668
|
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1514
1669
|
}
|
|
1670
|
+
const title = input.title ?? "";
|
|
1515
1671
|
await db.prepare(`
|
|
1516
|
-
INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
|
|
1517
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1672
|
+
INSERT INTO tokens (id, slug, title, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
|
|
1673
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1518
1674
|
`).run(
|
|
1519
1675
|
id,
|
|
1520
1676
|
input.slug,
|
|
1677
|
+
title,
|
|
1521
1678
|
input.concept,
|
|
1522
1679
|
input.domain ?? "",
|
|
1523
1680
|
bloom,
|
|
@@ -1560,6 +1717,10 @@ async function updateToken(db, slug, updates) {
|
|
|
1560
1717
|
}
|
|
1561
1718
|
const fields = [];
|
|
1562
1719
|
const values = [];
|
|
1720
|
+
if (updates.title !== void 0) {
|
|
1721
|
+
fields.push("title = ?");
|
|
1722
|
+
values.push(updates.title ?? "");
|
|
1723
|
+
}
|
|
1563
1724
|
if (updates.concept !== void 0) {
|
|
1564
1725
|
fields.push("concept = ?");
|
|
1565
1726
|
values.push(updates.concept);
|
|
@@ -1665,13 +1826,14 @@ async function deleteToken(db, slug) {
|
|
|
1665
1826
|
await db.transaction(async (tx) => {
|
|
1666
1827
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1667
1828
|
const skillRows = await tx.prepare("SELECT id, token_slugs FROM agent_skills").all();
|
|
1829
|
+
const skillUpdateStmt = tx.prepare(
|
|
1830
|
+
"UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
|
|
1831
|
+
);
|
|
1668
1832
|
for (const row of skillRows) {
|
|
1669
1833
|
const tokenSlugs = JSON.parse(row.token_slugs);
|
|
1670
1834
|
const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
|
|
1671
1835
|
if (filtered.length !== tokenSlugs.length) {
|
|
1672
|
-
await
|
|
1673
|
-
"UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
|
|
1674
|
-
).run(JSON.stringify(filtered), now, row.id);
|
|
1836
|
+
await skillUpdateStmt.run(JSON.stringify(filtered), now, row.id);
|
|
1675
1837
|
}
|
|
1676
1838
|
}
|
|
1677
1839
|
await tx.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
|
|
@@ -1685,10 +1847,16 @@ async function findTokens(db, query) {
|
|
|
1685
1847
|
const shortTerms = searchTokens.filter((t2) => t2.length <= 2);
|
|
1686
1848
|
const longTerms = searchTokens.filter((t2) => t2.length > 2);
|
|
1687
1849
|
const scoreMap = /* @__PURE__ */ new Map();
|
|
1688
|
-
const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
|
|
1850
|
+
const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(title) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
|
|
1851
|
+
const likeStmt = db.prepare(likeSQL);
|
|
1689
1852
|
for (const term of longTerms) {
|
|
1690
1853
|
const pattern = `%${term}%`;
|
|
1691
|
-
const rows = await
|
|
1854
|
+
const rows = await likeStmt.all(
|
|
1855
|
+
pattern,
|
|
1856
|
+
pattern,
|
|
1857
|
+
pattern,
|
|
1858
|
+
pattern
|
|
1859
|
+
);
|
|
1692
1860
|
for (const row of rows) {
|
|
1693
1861
|
const entry = scoreMap.get(row.id);
|
|
1694
1862
|
if (entry) {
|
|
@@ -1701,7 +1869,7 @@ async function findTokens(db, query) {
|
|
|
1701
1869
|
if (shortTerms.length > 0 || longTerms.length === 0) {
|
|
1702
1870
|
const allTokens = await db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
|
|
1703
1871
|
for (const token of allTokens) {
|
|
1704
|
-
const words = `${token.slug} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
|
|
1872
|
+
const words = `${token.slug} ${token.title} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
|
|
1705
1873
|
let matchCount = 0;
|
|
1706
1874
|
for (const term of shortTerms.length > 0 ? shortTerms : searchTokens) {
|
|
1707
1875
|
for (const w of words) {
|
|
@@ -1730,23 +1898,47 @@ async function findTokens(db, query) {
|
|
|
1730
1898
|
return scored;
|
|
1731
1899
|
}
|
|
1732
1900
|
async function listTokens(db, options) {
|
|
1733
|
-
|
|
1901
|
+
const whereClauses = ["deprecated_at IS NULL"];
|
|
1902
|
+
const params = [];
|
|
1734
1903
|
if (options?.domain) {
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1904
|
+
whereClauses.push("domain = ?");
|
|
1905
|
+
params.push(options.domain);
|
|
1906
|
+
} else if (options?.domainPrefix) {
|
|
1907
|
+
const prefix = options.domainPrefix;
|
|
1908
|
+
whereClauses.push("(domain = ? OR domain LIKE ?)");
|
|
1909
|
+
params.push(prefix, `${prefix}/%`);
|
|
1910
|
+
}
|
|
1911
|
+
if (options?.knowledgeContext) {
|
|
1912
|
+
whereClauses.push(`EXISTS (
|
|
1913
|
+
SELECT 1 FROM token_contexts tc
|
|
1914
|
+
INNER JOIN contexts c ON c.id = tc.context_id
|
|
1915
|
+
WHERE tc.token_id = tokens.id AND c.name = ?
|
|
1916
|
+
)`);
|
|
1917
|
+
params.push(options.knowledgeContext);
|
|
1918
|
+
}
|
|
1919
|
+
const orderBy = options?.domain || options?.domainPrefix ? "ORDER BY bloom_level, slug" : "ORDER BY bloom_level, domain, slug";
|
|
1920
|
+
const sql = `SELECT * FROM tokens WHERE ${whereClauses.join(" AND ")} ${orderBy}`;
|
|
1921
|
+
const tokens = await db.prepare(sql).all(...params);
|
|
1743
1922
|
for (const token of tokens) {
|
|
1744
1923
|
parseTokenFallback(token);
|
|
1745
1924
|
}
|
|
1746
1925
|
return tokens;
|
|
1747
1926
|
}
|
|
1748
1927
|
function slugify(text) {
|
|
1749
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1928
|
+
return text.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1929
|
+
}
|
|
1930
|
+
function getShortSlug(slug, domainPrefix) {
|
|
1931
|
+
if (domainPrefix) {
|
|
1932
|
+
const folded = domainPrefix.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1933
|
+
if (folded && slug.startsWith(`${folded}-`)) {
|
|
1934
|
+
return slug.substring(folded.length + 1);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return slug;
|
|
1938
|
+
}
|
|
1939
|
+
function getDisplayTitle(t2, activeDomainScope) {
|
|
1940
|
+
if (t2.title?.trim()) return t2.title.trim();
|
|
1941
|
+
return getShortSlug(t2.slug, activeDomainScope);
|
|
1750
1942
|
}
|
|
1751
1943
|
async function generateTokenSlug(db, domain, concept, question) {
|
|
1752
1944
|
const baseText = question && question.trim().length > 0 ? question : concept;
|
|
@@ -1776,6 +1968,7 @@ async function listPersonalCards(db, userId, options) {
|
|
|
1776
1968
|
SELECT
|
|
1777
1969
|
t.id AS tokenId,
|
|
1778
1970
|
t.slug,
|
|
1971
|
+
t.title,
|
|
1779
1972
|
t.concept,
|
|
1780
1973
|
t.domain,
|
|
1781
1974
|
t.bloom_level AS bloomLevel,
|
|
@@ -1816,6 +2009,14 @@ async function listPersonalCards(db, userId, options) {
|
|
|
1816
2009
|
sql += " AND t.domain = ?";
|
|
1817
2010
|
values.push(options.domain);
|
|
1818
2011
|
}
|
|
2012
|
+
if (options?.knowledgeContext) {
|
|
2013
|
+
sql += ` AND EXISTS (
|
|
2014
|
+
SELECT 1 FROM token_contexts tc
|
|
2015
|
+
INNER JOIN contexts kc ON kc.id = tc.context_id
|
|
2016
|
+
WHERE tc.token_id = t.id AND kc.name = ?
|
|
2017
|
+
)`;
|
|
2018
|
+
values.push(options.knowledgeContext);
|
|
2019
|
+
}
|
|
1819
2020
|
if (options?.query) {
|
|
1820
2021
|
const terms = options.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1821
2022
|
for (const term of terms) {
|
|
@@ -1877,6 +2078,7 @@ async function importCurriculumCards(db, userId, cards) {
|
|
|
1877
2078
|
);
|
|
1878
2079
|
token = await createToken(tx, {
|
|
1879
2080
|
slug: finalSlug,
|
|
2081
|
+
title: card.title,
|
|
1880
2082
|
concept: card.concept,
|
|
1881
2083
|
domain: card.domain,
|
|
1882
2084
|
bloom_level: bloom,
|
|
@@ -1994,23 +2196,28 @@ async function confirmCardSplit(db, userId, originalSlug, action, originalQuesti
|
|
|
1994
2196
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1995
2197
|
originalToken.id
|
|
1996
2198
|
);
|
|
2199
|
+
const insertPrereqStmt = tx.prepare(
|
|
2200
|
+
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
2201
|
+
);
|
|
1997
2202
|
for (const propToken of proposalTokens) {
|
|
1998
|
-
await
|
|
1999
|
-
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
2000
|
-
).run(originalToken.id, propToken.id);
|
|
2203
|
+
await insertPrereqStmt.run(originalToken.id, propToken.id);
|
|
2001
2204
|
}
|
|
2002
2205
|
await tx.prepare(
|
|
2003
2206
|
"UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
|
|
2004
2207
|
).run(originalToken.id, userId);
|
|
2208
|
+
const checkPrereqsStmt = tx.prepare(
|
|
2209
|
+
"SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
|
|
2210
|
+
);
|
|
2211
|
+
const unblockCardStmt = tx.prepare(
|
|
2212
|
+
"UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?"
|
|
2213
|
+
);
|
|
2005
2214
|
for (const propToken of proposalTokens) {
|
|
2006
2215
|
const card = await ensureCard(tx, propToken.id, userId);
|
|
2007
2216
|
if (card.blocked === 1) {
|
|
2008
|
-
const prereqOfPrereq = await
|
|
2009
|
-
"SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
|
|
2010
|
-
).get(propToken.id);
|
|
2217
|
+
const prereqOfPrereq = await checkPrereqsStmt.get(propToken.id);
|
|
2011
2218
|
if (prereqOfPrereq.n === 0) {
|
|
2012
2219
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2013
|
-
await
|
|
2220
|
+
await unblockCardStmt.run(now, card.id);
|
|
2014
2221
|
}
|
|
2015
2222
|
}
|
|
2016
2223
|
}
|
|
@@ -2081,6 +2288,7 @@ async function confirmFoundations(db, userId, originalSlug, proposals) {
|
|
|
2081
2288
|
);
|
|
2082
2289
|
token = await createToken(tx, {
|
|
2083
2290
|
slug: finalSlug,
|
|
2291
|
+
title: card.title,
|
|
2084
2292
|
concept: card.concept,
|
|
2085
2293
|
domain: card.domain,
|
|
2086
2294
|
bloom_level: bloom,
|
|
@@ -2147,6 +2355,7 @@ async function confirmSourceImport(db, userId, sourceId, proposals) {
|
|
|
2147
2355
|
}
|
|
2148
2356
|
token = await createToken(tx, {
|
|
2149
2357
|
slug: finalSlug,
|
|
2358
|
+
title: card.title,
|
|
2150
2359
|
concept: card.concept,
|
|
2151
2360
|
domain: card.domain,
|
|
2152
2361
|
bloom_level: bloom,
|
|
@@ -2249,7 +2458,7 @@ async function addPrerequisite(db, tokenId, requiresId) {
|
|
|
2249
2458
|
}
|
|
2250
2459
|
async function getPrerequisites(db, tokenId) {
|
|
2251
2460
|
return await db.prepare(
|
|
2252
|
-
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
2461
|
+
`SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
|
|
2253
2462
|
FROM prerequisites p
|
|
2254
2463
|
JOIN tokens t ON t.id = p.requires_id
|
|
2255
2464
|
WHERE p.token_id = ?`
|
|
@@ -2257,7 +2466,7 @@ async function getPrerequisites(db, tokenId) {
|
|
|
2257
2466
|
}
|
|
2258
2467
|
async function getDependents(db, tokenId) {
|
|
2259
2468
|
return await db.prepare(
|
|
2260
|
-
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
2469
|
+
`SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
|
|
2261
2470
|
FROM prerequisites p
|
|
2262
2471
|
JOIN tokens t ON t.id = p.token_id
|
|
2263
2472
|
WHERE p.requires_id = ?`
|
|
@@ -2288,6 +2497,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
|
|
|
2288
2497
|
const toNode = (t2, card) => ({
|
|
2289
2498
|
id: t2.id,
|
|
2290
2499
|
slug: t2.slug,
|
|
2500
|
+
title: t2.title ?? t2.slug,
|
|
2291
2501
|
concept: t2.concept,
|
|
2292
2502
|
domain: t2.domain,
|
|
2293
2503
|
bloom_level: t2.bloom_level,
|
|
@@ -2307,6 +2517,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
|
|
|
2307
2517
|
{
|
|
2308
2518
|
id: p.requires_id,
|
|
2309
2519
|
slug: p.slug,
|
|
2520
|
+
title: p.title,
|
|
2310
2521
|
concept: p.concept,
|
|
2311
2522
|
domain: p.domain,
|
|
2312
2523
|
bloom_level: p.bloom_level
|
|
@@ -2319,6 +2530,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
|
|
|
2319
2530
|
{
|
|
2320
2531
|
id: d.token_id,
|
|
2321
2532
|
slug: d.slug,
|
|
2533
|
+
title: d.title,
|
|
2322
2534
|
concept: d.concept,
|
|
2323
2535
|
domain: d.domain,
|
|
2324
2536
|
bloom_level: d.bloom_level
|
|
@@ -2330,12 +2542,12 @@ async function getTokenNeighborhood(db, tokenId, userId) {
|
|
|
2330
2542
|
}
|
|
2331
2543
|
|
|
2332
2544
|
// src/kernel/models/review.ts
|
|
2333
|
-
import { ulid as
|
|
2545
|
+
import { ulid as ulid5 } from "ulid";
|
|
2334
2546
|
async function logReview(db, input) {
|
|
2335
2547
|
if (input.rating < 1 || input.rating > 4) {
|
|
2336
2548
|
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
2337
2549
|
}
|
|
2338
|
-
const id =
|
|
2550
|
+
const id = ulid5();
|
|
2339
2551
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2340
2552
|
await db.prepare(
|
|
2341
2553
|
`INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
|
|
@@ -2378,9 +2590,9 @@ async function getReviewsForUser(db, userId, options) {
|
|
|
2378
2590
|
}
|
|
2379
2591
|
|
|
2380
2592
|
// src/kernel/models/session.ts
|
|
2381
|
-
import { ulid as
|
|
2593
|
+
import { ulid as ulid6 } from "ulid";
|
|
2382
2594
|
async function startSession(db, input) {
|
|
2383
|
-
const id =
|
|
2595
|
+
const id = ulid6();
|
|
2384
2596
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2385
2597
|
const ctx = input.execution_context ?? "shell";
|
|
2386
2598
|
await db.prepare(
|
|
@@ -2414,7 +2626,7 @@ async function logStep(db, input) {
|
|
|
2414
2626
|
if (!session) {
|
|
2415
2627
|
throw new Error(`Session not found: ${input.session_id}`);
|
|
2416
2628
|
}
|
|
2417
|
-
const id =
|
|
2629
|
+
const id = ulid6();
|
|
2418
2630
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2419
2631
|
await db.prepare(
|
|
2420
2632
|
`INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
|
|
@@ -2478,7 +2690,8 @@ import { createHash as createHash2 } from "crypto";
|
|
|
2478
2690
|
function embeddingContentForToken(t2) {
|
|
2479
2691
|
return `${t2.concept}
|
|
2480
2692
|
${t2.question ?? ""}
|
|
2481
|
-
${t2.domain}
|
|
2693
|
+
${t2.domain}
|
|
2694
|
+
${t2.title ?? ""}`;
|
|
2482
2695
|
}
|
|
2483
2696
|
function computeContentHash(text) {
|
|
2484
2697
|
return createHash2("sha256").update(text, "utf8").digest("hex");
|
|
@@ -3312,10 +3525,10 @@ async function syncObserverSidecarPolicy(db, dir = getUiObserverDir()) {
|
|
|
3312
3525
|
}
|
|
3313
3526
|
|
|
3314
3527
|
// src/kernel/observation/session-synthesis.ts
|
|
3315
|
-
import { ulid as
|
|
3528
|
+
import { ulid as ulid8 } from "ulid";
|
|
3316
3529
|
|
|
3317
3530
|
// src/kernel/recall/evaluator.ts
|
|
3318
|
-
import { ulid as
|
|
3531
|
+
import { ulid as ulid7 } from "ulid";
|
|
3319
3532
|
|
|
3320
3533
|
// src/kernel/scheduler/fsrs.ts
|
|
3321
3534
|
var DEFAULT_W = [
|
|
@@ -3494,7 +3707,7 @@ async function evaluateRatingWithinTransaction(db, input) {
|
|
|
3494
3707
|
due_at: updated.dueAt.toISOString(),
|
|
3495
3708
|
last_review_at: now.toISOString()
|
|
3496
3709
|
});
|
|
3497
|
-
const reviewLogId = input.reviewLogId ??
|
|
3710
|
+
const reviewLogId = input.reviewLogId ?? ulid7();
|
|
3498
3711
|
await db.prepare(
|
|
3499
3712
|
`INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
|
|
3500
3713
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
@@ -3828,7 +4041,7 @@ async function applySessionSynthesis(db, input) {
|
|
|
3828
4041
|
return { applied: false, record: parseSynthesisRow(existing) };
|
|
3829
4042
|
}
|
|
3830
4043
|
const card = await ensureCard(tx, token.id, session.user_id);
|
|
3831
|
-
const reviewLogId =
|
|
4044
|
+
const reviewLogId = ulid8();
|
|
3832
4045
|
await evaluateRatingWithinTransaction(tx, {
|
|
3833
4046
|
cardId: card.id,
|
|
3834
4047
|
tokenId: token.id,
|
|
@@ -4657,11 +4870,11 @@ async function buildReviewQueue(db, options) {
|
|
|
4657
4870
|
const maxReviews = options.maxReviews ?? 50;
|
|
4658
4871
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
4659
4872
|
const nowISO = now.toISOString();
|
|
4660
|
-
|
|
4661
|
-
`SELECT
|
|
4873
|
+
let dueSql = `SELECT
|
|
4662
4874
|
c.id AS card_id,
|
|
4663
4875
|
c.token_id AS token_id,
|
|
4664
4876
|
t.slug AS slug,
|
|
4877
|
+
t.title AS title,
|
|
4665
4878
|
t.concept AS concept,
|
|
4666
4879
|
t.domain AS domain,
|
|
4667
4880
|
t.bloom_level AS bloom_level,
|
|
@@ -4675,14 +4888,23 @@ async function buildReviewQueue(db, options) {
|
|
|
4675
4888
|
AND c.blocked = 0
|
|
4676
4889
|
AND c.due_at <= ?
|
|
4677
4890
|
AND c.state IN ('review', 'relearning', 'learning')
|
|
4678
|
-
AND t.deprecated_at IS NULL
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4891
|
+
AND t.deprecated_at IS NULL`;
|
|
4892
|
+
const dueParams = [options.userId, nowISO];
|
|
4893
|
+
if (options.knowledgeContext) {
|
|
4894
|
+
dueSql += ` AND EXISTS (
|
|
4895
|
+
SELECT 1 FROM token_contexts tc
|
|
4896
|
+
INNER JOIN contexts ctx ON ctx.id = tc.context_id
|
|
4897
|
+
WHERE tc.token_id = t.id AND ctx.name = ?
|
|
4898
|
+
)`;
|
|
4899
|
+
dueParams.push(options.knowledgeContext);
|
|
4900
|
+
}
|
|
4901
|
+
dueSql += ` ORDER BY c.due_at ASC`;
|
|
4902
|
+
const dueRows = await db.prepare(dueSql).all(...dueParams);
|
|
4903
|
+
let newSql = `SELECT
|
|
4683
4904
|
c.id AS card_id,
|
|
4684
4905
|
c.token_id AS token_id,
|
|
4685
4906
|
t.slug AS slug,
|
|
4907
|
+
t.title AS title,
|
|
4686
4908
|
t.concept AS concept,
|
|
4687
4909
|
t.domain AS domain,
|
|
4688
4910
|
t.bloom_level AS bloom_level,
|
|
@@ -4695,10 +4917,19 @@ async function buildReviewQueue(db, options) {
|
|
|
4695
4917
|
WHERE c.user_id = ?
|
|
4696
4918
|
AND c.blocked = 0
|
|
4697
4919
|
AND c.state = 'new'
|
|
4698
|
-
AND t.deprecated_at IS NULL
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4920
|
+
AND t.deprecated_at IS NULL`;
|
|
4921
|
+
const newParams = [options.userId];
|
|
4922
|
+
if (options.knowledgeContext) {
|
|
4923
|
+
newSql += ` AND EXISTS (
|
|
4924
|
+
SELECT 1 FROM token_contexts tc
|
|
4925
|
+
INNER JOIN contexts ctx ON ctx.id = tc.context_id
|
|
4926
|
+
WHERE tc.token_id = t.id AND ctx.name = ?
|
|
4927
|
+
)`;
|
|
4928
|
+
newParams.push(options.knowledgeContext);
|
|
4929
|
+
}
|
|
4930
|
+
newSql += ` ORDER BY t.bloom_level ASC, t.slug ASC LIMIT ?`;
|
|
4931
|
+
newParams.push(maxNew);
|
|
4932
|
+
const newRows = await db.prepare(newSql).all(...newParams);
|
|
4702
4933
|
const nowMs = now.getTime();
|
|
4703
4934
|
const sortedDue = [...dueRows].sort((a, b) => {
|
|
4704
4935
|
const overdueA = nowMs - new Date(a.due_at).getTime();
|
|
@@ -4742,6 +4973,7 @@ function rowToItem(row) {
|
|
|
4742
4973
|
cardId: row.card_id,
|
|
4743
4974
|
tokenId: row.token_id,
|
|
4744
4975
|
slug: row.slug,
|
|
4976
|
+
title: getDisplayTitle(row),
|
|
4745
4977
|
concept: row.concept,
|
|
4746
4978
|
domain: row.domain,
|
|
4747
4979
|
bloomLevel: row.bloom_level,
|
|
@@ -5444,6 +5676,27 @@ function detectSyncProvider(dir) {
|
|
|
5444
5676
|
}
|
|
5445
5677
|
return null;
|
|
5446
5678
|
}
|
|
5679
|
+
function getActiveWorkspaceContext(path = defaultConfigPath()) {
|
|
5680
|
+
const activeWorkspace = getActiveWorkspace(path);
|
|
5681
|
+
return activeWorkspace?.activeKnowledgeContext;
|
|
5682
|
+
}
|
|
5683
|
+
function setActiveWorkspaceContext(contextName, path = defaultConfigPath()) {
|
|
5684
|
+
const config = loadInstallConfig(path);
|
|
5685
|
+
const activeId = config.activeWorkspaceId;
|
|
5686
|
+
if (activeId && config.workspaces) {
|
|
5687
|
+
const workspace = config.workspaces.find((w) => w.id === activeId);
|
|
5688
|
+
if (workspace) {
|
|
5689
|
+
if (contextName) {
|
|
5690
|
+
workspace.activeKnowledgeContext = contextName;
|
|
5691
|
+
} else {
|
|
5692
|
+
delete workspace.activeKnowledgeContext;
|
|
5693
|
+
}
|
|
5694
|
+
saveInstallConfig(config, path);
|
|
5695
|
+
return true;
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
return false;
|
|
5699
|
+
}
|
|
5447
5700
|
|
|
5448
5701
|
// src/kernel/system/installer.ts
|
|
5449
5702
|
import { execFileSync, execSync } from "child_process";
|
|
@@ -5939,6 +6192,7 @@ export {
|
|
|
5939
6192
|
analyzeObservation,
|
|
5940
6193
|
appendUiObservationReport,
|
|
5941
6194
|
applySessionSynthesis,
|
|
6195
|
+
assignTokenToContext,
|
|
5942
6196
|
buildAncestorMap,
|
|
5943
6197
|
buildReviewQueue,
|
|
5944
6198
|
buildUiSynthesisCandidates,
|
|
@@ -5956,12 +6210,14 @@ export {
|
|
|
5956
6210
|
createAgentSkill,
|
|
5957
6211
|
createFSRS,
|
|
5958
6212
|
createGoal,
|
|
6213
|
+
createKnowledgeContext,
|
|
5959
6214
|
createToken,
|
|
5960
6215
|
decidePostCapture,
|
|
5961
6216
|
decidePreCapture,
|
|
5962
6217
|
decideUpdate,
|
|
5963
6218
|
decodeEmbedding,
|
|
5964
6219
|
deleteCardForUser,
|
|
6220
|
+
deleteKnowledgeContext,
|
|
5965
6221
|
deleteSetting,
|
|
5966
6222
|
deleteToken,
|
|
5967
6223
|
deprecateToken,
|
|
@@ -5993,6 +6249,7 @@ export {
|
|
|
5993
6249
|
generateZshUnhooks,
|
|
5994
6250
|
getADOCredentials,
|
|
5995
6251
|
getActiveWorkspace,
|
|
6252
|
+
getActiveWorkspaceContext,
|
|
5996
6253
|
getActiveWorkspaceId,
|
|
5997
6254
|
getAgentSkill,
|
|
5998
6255
|
getAllSettings,
|
|
@@ -6005,6 +6262,7 @@ export {
|
|
|
6005
6262
|
getDatabaseTargetInfo,
|
|
6006
6263
|
getDefaultDbPath,
|
|
6007
6264
|
getDependents,
|
|
6265
|
+
getDisplayTitle,
|
|
6008
6266
|
getDomainCompetence,
|
|
6009
6267
|
getDueCards,
|
|
6010
6268
|
getEmbeddingCoverage,
|
|
@@ -6012,6 +6270,8 @@ export {
|
|
|
6012
6270
|
getGoalTree,
|
|
6013
6271
|
getInstallChannel,
|
|
6014
6272
|
getInstallMode,
|
|
6273
|
+
getKnowledgeContextById,
|
|
6274
|
+
getKnowledgeContextByName,
|
|
6015
6275
|
getMachineAiConfig,
|
|
6016
6276
|
getMonitorDir,
|
|
6017
6277
|
getMonitorLogStats,
|
|
@@ -6025,6 +6285,7 @@ export {
|
|
|
6025
6285
|
getSessionSummary,
|
|
6026
6286
|
getSessionSynthesisRecords,
|
|
6027
6287
|
getSetting,
|
|
6288
|
+
getShortSlug,
|
|
6028
6289
|
getSystemProfile,
|
|
6029
6290
|
getTokenById,
|
|
6030
6291
|
getTokenBySlug,
|
|
@@ -6047,8 +6308,10 @@ export {
|
|
|
6047
6308
|
isOllamaInstalled,
|
|
6048
6309
|
isUiObservationReport,
|
|
6049
6310
|
listAgentSkills,
|
|
6311
|
+
listContextsForToken,
|
|
6050
6312
|
listEmbeddedTokens,
|
|
6051
6313
|
listGoals,
|
|
6314
|
+
listKnowledgeContexts,
|
|
6052
6315
|
listPersonalCards,
|
|
6053
6316
|
listProviderApiKeyRefs,
|
|
6054
6317
|
listTokens,
|
|
@@ -6095,6 +6358,7 @@ export {
|
|
|
6095
6358
|
searchTokensHybrid,
|
|
6096
6359
|
serializeGoal,
|
|
6097
6360
|
setADOCredentials,
|
|
6361
|
+
setActiveWorkspaceContext,
|
|
6098
6362
|
setActiveWorkspaceId,
|
|
6099
6363
|
setInstallChannel,
|
|
6100
6364
|
setInstallMode,
|
|
@@ -6109,9 +6373,11 @@ export {
|
|
|
6109
6373
|
toSidecarPrivacyPolicy,
|
|
6110
6374
|
uiObservationLogExists,
|
|
6111
6375
|
uiObservationTimeSpan,
|
|
6376
|
+
unassignTokenFromContext,
|
|
6112
6377
|
unblockReady,
|
|
6113
6378
|
updateCard,
|
|
6114
6379
|
updateGoalStatus,
|
|
6380
|
+
updateKnowledgeContext,
|
|
6115
6381
|
updateToken,
|
|
6116
6382
|
upsertConfiguredWorkspace,
|
|
6117
6383
|
upsertTokenEmbedding,
|