zam-core 0.6.3 → 0.7.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/.agent/skills/zam/SKILL.md +19 -3
- package/.agents/skills/zam/SKILL.md +19 -3
- package/.claude/skills/zam/SKILL.md +19 -3
- package/dist/cli/index.js +979 -34
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +162 -3
- package/dist/index.js +338 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -656,6 +656,18 @@ CREATE TABLE IF NOT EXISTS user_config (
|
|
|
656
656
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
657
657
|
);
|
|
658
658
|
|
|
659
|
+
-- Token embeddings: one vector per token for semantic search (ADR 2026-07-03).
|
|
660
|
+
-- Not a column on tokens: hot paths do SELECT * FROM tokens and a ~3KB blob
|
|
661
|
+
-- per row would ride along on all of them.
|
|
662
|
+
CREATE TABLE IF NOT EXISTS token_embeddings (
|
|
663
|
+
token_id TEXT PRIMARY KEY REFERENCES tokens(id) ON DELETE CASCADE,
|
|
664
|
+
embedding BLOB NOT NULL,
|
|
665
|
+
model TEXT NOT NULL,
|
|
666
|
+
dims INTEGER NOT NULL,
|
|
667
|
+
content_hash TEXT NOT NULL,
|
|
668
|
+
embedded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
669
|
+
);
|
|
670
|
+
|
|
659
671
|
-- Agent skills: task recipes the agent learns from user guidance
|
|
660
672
|
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
661
673
|
id TEXT PRIMARY KEY,
|
|
@@ -1002,6 +1014,16 @@ async function runMigrations(db) {
|
|
|
1002
1014
|
await db.exec(`ALTER TABLE tokens ADD COLUMN topic_id TEXT`);
|
|
1003
1015
|
}
|
|
1004
1016
|
}
|
|
1017
|
+
await db.exec(`
|
|
1018
|
+
CREATE TABLE IF NOT EXISTS token_embeddings (
|
|
1019
|
+
token_id TEXT PRIMARY KEY REFERENCES tokens(id) ON DELETE CASCADE,
|
|
1020
|
+
embedding BLOB NOT NULL,
|
|
1021
|
+
model TEXT NOT NULL,
|
|
1022
|
+
dims INTEGER NOT NULL,
|
|
1023
|
+
content_hash TEXT NOT NULL,
|
|
1024
|
+
embedded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1025
|
+
)
|
|
1026
|
+
`);
|
|
1005
1027
|
}
|
|
1006
1028
|
|
|
1007
1029
|
// src/kernel/db/snapshot.ts
|
|
@@ -2197,9 +2219,9 @@ async function buildAncestorMap(db) {
|
|
|
2197
2219
|
}
|
|
2198
2220
|
return map;
|
|
2199
2221
|
}
|
|
2200
|
-
async function wouldCreateCycle(db, tokenId, requiresId) {
|
|
2222
|
+
async function wouldCreateCycle(db, tokenId, requiresId, ancestors) {
|
|
2201
2223
|
if (tokenId === requiresId) return true;
|
|
2202
|
-
const
|
|
2224
|
+
const map = ancestors ?? await buildAncestorMap(db);
|
|
2203
2225
|
const visited = /* @__PURE__ */ new Set();
|
|
2204
2226
|
const queue = [requiresId];
|
|
2205
2227
|
while (queue.length > 0) {
|
|
@@ -2207,7 +2229,7 @@ async function wouldCreateCycle(db, tokenId, requiresId) {
|
|
|
2207
2229
|
if (current === tokenId) return true;
|
|
2208
2230
|
if (visited.has(current)) continue;
|
|
2209
2231
|
visited.add(current);
|
|
2210
|
-
const parents =
|
|
2232
|
+
const parents = map.get(current);
|
|
2211
2233
|
if (parents) {
|
|
2212
2234
|
for (const parent of parents) {
|
|
2213
2235
|
if (!visited.has(parent)) queue.push(parent);
|
|
@@ -2410,6 +2432,151 @@ async function deleteSetting(db, key) {
|
|
|
2410
2432
|
return result.changes > 0;
|
|
2411
2433
|
}
|
|
2412
2434
|
|
|
2435
|
+
// src/kernel/models/token-embedding.ts
|
|
2436
|
+
import { createHash as createHash2 } from "crypto";
|
|
2437
|
+
function embeddingContentForToken(t2) {
|
|
2438
|
+
return `${t2.concept}
|
|
2439
|
+
${t2.question ?? ""}
|
|
2440
|
+
${t2.domain}`;
|
|
2441
|
+
}
|
|
2442
|
+
function computeContentHash(text) {
|
|
2443
|
+
return createHash2("sha256").update(text, "utf8").digest("hex");
|
|
2444
|
+
}
|
|
2445
|
+
function encodeEmbedding(vec) {
|
|
2446
|
+
const f = Float32Array.from(vec);
|
|
2447
|
+
return new Uint8Array(f.buffer);
|
|
2448
|
+
}
|
|
2449
|
+
function decodeEmbedding(blob) {
|
|
2450
|
+
if (blob.byteLength % 4 !== 0) {
|
|
2451
|
+
throw new Error(
|
|
2452
|
+
"Invalid embedding blob size: must be a multiple of 4 bytes"
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
if (blob.byteOffset % 4 === 0) {
|
|
2456
|
+
return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
2457
|
+
}
|
|
2458
|
+
const copy = Uint8Array.prototype.slice.call(blob);
|
|
2459
|
+
return new Float32Array(copy.buffer, 0, copy.byteLength / 4);
|
|
2460
|
+
}
|
|
2461
|
+
async function upsertTokenEmbedding(db, input8) {
|
|
2462
|
+
const encoded = encodeEmbedding(input8.embedding);
|
|
2463
|
+
const dims = input8.embedding.length;
|
|
2464
|
+
const embeddedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2465
|
+
await db.prepare(`
|
|
2466
|
+
INSERT INTO token_embeddings (token_id, embedding, model, dims, content_hash, embedded_at)
|
|
2467
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2468
|
+
ON CONFLICT(token_id) DO UPDATE SET
|
|
2469
|
+
embedding = excluded.embedding,
|
|
2470
|
+
model = excluded.model,
|
|
2471
|
+
dims = excluded.dims,
|
|
2472
|
+
content_hash = excluded.content_hash,
|
|
2473
|
+
embedded_at = excluded.embedded_at
|
|
2474
|
+
`).run(
|
|
2475
|
+
input8.tokenId,
|
|
2476
|
+
encoded,
|
|
2477
|
+
input8.model,
|
|
2478
|
+
dims,
|
|
2479
|
+
input8.contentHash,
|
|
2480
|
+
embeddedAt
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
async function listTokensNeedingEmbedding(db, model, opts) {
|
|
2484
|
+
const rows = await db.prepare(`
|
|
2485
|
+
SELECT t.*, e.model AS emb_model, e.dims AS emb_dims, e.content_hash AS emb_hash
|
|
2486
|
+
FROM tokens t
|
|
2487
|
+
LEFT JOIN token_embeddings e ON e.token_id = t.id
|
|
2488
|
+
WHERE t.deprecated_at IS NULL
|
|
2489
|
+
`).all();
|
|
2490
|
+
const needing = [];
|
|
2491
|
+
for (const row of rows) {
|
|
2492
|
+
let reason = null;
|
|
2493
|
+
let text = "";
|
|
2494
|
+
if (opts?.force) {
|
|
2495
|
+
reason = "content-changed";
|
|
2496
|
+
} else if (row.emb_model === null) {
|
|
2497
|
+
reason = "missing";
|
|
2498
|
+
} else if (row.emb_model !== model) {
|
|
2499
|
+
reason = "model-changed";
|
|
2500
|
+
} else if (opts?.dims !== void 0 && row.emb_dims !== opts.dims) {
|
|
2501
|
+
reason = "dimension-changed";
|
|
2502
|
+
} else {
|
|
2503
|
+
const computedText = embeddingContentForToken(row);
|
|
2504
|
+
const hash = computeContentHash(computedText);
|
|
2505
|
+
if (row.emb_hash !== hash) {
|
|
2506
|
+
reason = "content-changed";
|
|
2507
|
+
text = computedText;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
if (reason) {
|
|
2511
|
+
if (!text) {
|
|
2512
|
+
text = embeddingContentForToken(row);
|
|
2513
|
+
}
|
|
2514
|
+
needing.push({ token: row, text, reason });
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
if (opts?.limit !== void 0) {
|
|
2518
|
+
return needing.slice(0, opts.limit);
|
|
2519
|
+
}
|
|
2520
|
+
return needing;
|
|
2521
|
+
}
|
|
2522
|
+
async function getEmbeddingCoverage(db, model, opts) {
|
|
2523
|
+
const rows = await db.prepare(`
|
|
2524
|
+
SELECT t.*, e.model AS emb_model, e.dims AS emb_dims, e.content_hash AS emb_hash
|
|
2525
|
+
FROM tokens t
|
|
2526
|
+
LEFT JOIN token_embeddings e ON e.token_id = t.id
|
|
2527
|
+
WHERE t.deprecated_at IS NULL
|
|
2528
|
+
`).all();
|
|
2529
|
+
let missing = 0;
|
|
2530
|
+
let stale = 0;
|
|
2531
|
+
for (const row of rows) {
|
|
2532
|
+
if (row.emb_model === null) {
|
|
2533
|
+
missing++;
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
if (row.emb_model !== model) {
|
|
2537
|
+
stale++;
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
if (opts?.dims !== void 0 && row.emb_dims !== opts.dims) {
|
|
2541
|
+
stale++;
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
const hash = computeContentHash(embeddingContentForToken(row));
|
|
2545
|
+
if (row.emb_hash !== hash) {
|
|
2546
|
+
stale++;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
const tokens = rows.length;
|
|
2550
|
+
return {
|
|
2551
|
+
tokens,
|
|
2552
|
+
embedded: tokens - missing - stale,
|
|
2553
|
+
missing,
|
|
2554
|
+
stale
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
async function listEmbeddedTokens(db, model) {
|
|
2558
|
+
const rows = await db.prepare(`
|
|
2559
|
+
SELECT t.*, e.embedding AS embedding, e.content_hash AS emb_hash
|
|
2560
|
+
FROM token_embeddings e
|
|
2561
|
+
JOIN tokens t ON t.id = e.token_id
|
|
2562
|
+
WHERE e.model = ? AND t.deprecated_at IS NULL
|
|
2563
|
+
`).all(model);
|
|
2564
|
+
return rows.flatMap((row) => {
|
|
2565
|
+
const { embedding, emb_hash, ...token } = row;
|
|
2566
|
+
if (emb_hash !== computeContentHash(embeddingContentForToken(token))) {
|
|
2567
|
+
return [];
|
|
2568
|
+
}
|
|
2569
|
+
try {
|
|
2570
|
+
return [{ token, embedding: decodeEmbedding(embedding) }];
|
|
2571
|
+
} catch (err) {
|
|
2572
|
+
console.warn(
|
|
2573
|
+
`Warning: Corrupted embedding for token ${token.slug} (${token.id}) ignored: ${err.message}`
|
|
2574
|
+
);
|
|
2575
|
+
return [];
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2413
2580
|
// src/kernel/observation/analyzer.ts
|
|
2414
2581
|
function parseMonitorLog(jsonl) {
|
|
2415
2582
|
const events = [];
|
|
@@ -4546,6 +4713,147 @@ function intersperseNew(reviews, newCards, interval) {
|
|
|
4546
4713
|
return result;
|
|
4547
4714
|
}
|
|
4548
4715
|
|
|
4716
|
+
// src/kernel/search/hybrid.ts
|
|
4717
|
+
function cosineSimilarity(a, b) {
|
|
4718
|
+
if (a.length !== b.length) return 0;
|
|
4719
|
+
let dot = 0;
|
|
4720
|
+
let normA = 0;
|
|
4721
|
+
let normB = 0;
|
|
4722
|
+
const len = a.length;
|
|
4723
|
+
for (let i = 0; i < len; i++) {
|
|
4724
|
+
dot += a[i] * b[i];
|
|
4725
|
+
normA += a[i] * a[i];
|
|
4726
|
+
normB += b[i] * b[i];
|
|
4727
|
+
}
|
|
4728
|
+
if (normA === 0 || normB === 0) return 0;
|
|
4729
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
4730
|
+
}
|
|
4731
|
+
async function searchTokensHybrid(db, query, opts) {
|
|
4732
|
+
const limit = opts?.limit ?? 20;
|
|
4733
|
+
const rrfK = opts?.rrfK ?? 60;
|
|
4734
|
+
const vectorTopK = opts?.vectorTopK ?? 10;
|
|
4735
|
+
const lexicalHits = await findTokens(db, query);
|
|
4736
|
+
let vectorHits = [];
|
|
4737
|
+
if (opts?.queryEmbedding && opts?.model) {
|
|
4738
|
+
const queryVec = Float32Array.from(opts.queryEmbedding);
|
|
4739
|
+
const embedded = await listEmbeddedTokens(db, opts.model);
|
|
4740
|
+
const candidates = [];
|
|
4741
|
+
for (const row of embedded) {
|
|
4742
|
+
if (row.embedding.length !== queryVec.length) {
|
|
4743
|
+
continue;
|
|
4744
|
+
}
|
|
4745
|
+
const similarity = cosineSimilarity(queryVec, row.embedding);
|
|
4746
|
+
if (similarity <= 0) {
|
|
4747
|
+
continue;
|
|
4748
|
+
}
|
|
4749
|
+
candidates.push({ token: row.token, similarity });
|
|
4750
|
+
}
|
|
4751
|
+
candidates.sort((a, b) => b.similarity - a.similarity);
|
|
4752
|
+
vectorHits = candidates.slice(0, vectorTopK);
|
|
4753
|
+
}
|
|
4754
|
+
const tokenMap = /* @__PURE__ */ new Map();
|
|
4755
|
+
const getOrCreateEntry = (token) => {
|
|
4756
|
+
let entry = tokenMap.get(token.id);
|
|
4757
|
+
if (!entry) {
|
|
4758
|
+
entry = {
|
|
4759
|
+
...token,
|
|
4760
|
+
score: 0,
|
|
4761
|
+
lexicalRank: null,
|
|
4762
|
+
vectorRank: null,
|
|
4763
|
+
similarity: null
|
|
4764
|
+
};
|
|
4765
|
+
tokenMap.set(token.id, entry);
|
|
4766
|
+
}
|
|
4767
|
+
return entry;
|
|
4768
|
+
};
|
|
4769
|
+
for (let i = 0; i < lexicalHits.length; i++) {
|
|
4770
|
+
const hit = lexicalHits[i];
|
|
4771
|
+
const entry = getOrCreateEntry(hit);
|
|
4772
|
+
entry.lexicalRank = i + 1;
|
|
4773
|
+
entry.score += 1 / (rrfK + entry.lexicalRank);
|
|
4774
|
+
}
|
|
4775
|
+
for (let j = 0; j < vectorHits.length; j++) {
|
|
4776
|
+
const hit = vectorHits[j];
|
|
4777
|
+
const entry = getOrCreateEntry(hit.token);
|
|
4778
|
+
entry.vectorRank = j + 1;
|
|
4779
|
+
entry.similarity = hit.similarity;
|
|
4780
|
+
entry.score += 1 / (rrfK + entry.vectorRank);
|
|
4781
|
+
}
|
|
4782
|
+
const results = Array.from(tokenMap.values());
|
|
4783
|
+
results.sort((a, b) => {
|
|
4784
|
+
if (Math.abs(a.score - b.score) > 1e-9) {
|
|
4785
|
+
return b.score - a.score;
|
|
4786
|
+
}
|
|
4787
|
+
return a.slug.localeCompare(b.slug);
|
|
4788
|
+
});
|
|
4789
|
+
return results.slice(0, limit);
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4792
|
+
// src/kernel/search/suggestions.ts
|
|
4793
|
+
async function suggestFoundations(db, opts) {
|
|
4794
|
+
const minSimilarity = opts.minSimilarity ?? 0.45;
|
|
4795
|
+
const maxSimilarity = opts.maxSimilarity ?? 0.85;
|
|
4796
|
+
const limit = opts.limit ?? 5;
|
|
4797
|
+
const targetBloomLevel = opts.targetBloomLevel ?? 5;
|
|
4798
|
+
if (minSimilarity >= maxSimilarity) {
|
|
4799
|
+
return [];
|
|
4800
|
+
}
|
|
4801
|
+
const embedded = await listEmbeddedTokens(db, opts.model);
|
|
4802
|
+
if (embedded.length === 0) {
|
|
4803
|
+
return [];
|
|
4804
|
+
}
|
|
4805
|
+
const queryVec = Float32Array.from(opts.queryEmbedding);
|
|
4806
|
+
const candidates = [];
|
|
4807
|
+
for (const row of embedded) {
|
|
4808
|
+
if (opts.targetTokenId && row.token.id === opts.targetTokenId) {
|
|
4809
|
+
continue;
|
|
4810
|
+
}
|
|
4811
|
+
const similarity = cosineSimilarity(queryVec, row.embedding);
|
|
4812
|
+
if (similarity >= minSimilarity && similarity < maxSimilarity) {
|
|
4813
|
+
candidates.push({ token: row.token, similarity });
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
candidates.sort((a, b) => {
|
|
4817
|
+
if (Math.abs(a.similarity - b.similarity) > 1e-9) {
|
|
4818
|
+
return b.similarity - a.similarity;
|
|
4819
|
+
}
|
|
4820
|
+
return a.token.slug.localeCompare(b.token.slug);
|
|
4821
|
+
});
|
|
4822
|
+
const topCandidates = candidates.slice(0, limit);
|
|
4823
|
+
const prereqIds = /* @__PURE__ */ new Set();
|
|
4824
|
+
let ancestors;
|
|
4825
|
+
if (opts.targetTokenId) {
|
|
4826
|
+
const prereqs = await getPrerequisites(db, opts.targetTokenId);
|
|
4827
|
+
for (const p of prereqs) {
|
|
4828
|
+
prereqIds.add(p.requires_id);
|
|
4829
|
+
}
|
|
4830
|
+
ancestors = await buildAncestorMap(db);
|
|
4831
|
+
}
|
|
4832
|
+
const results = [];
|
|
4833
|
+
for (const cand of topCandidates) {
|
|
4834
|
+
let alreadyPrerequisite = false;
|
|
4835
|
+
let wouldCreateCycleFlag = false;
|
|
4836
|
+
const bloomAboveTarget = cand.token.bloom_level > targetBloomLevel;
|
|
4837
|
+
if (opts.targetTokenId) {
|
|
4838
|
+
alreadyPrerequisite = prereqIds.has(cand.token.id);
|
|
4839
|
+
wouldCreateCycleFlag = await wouldCreateCycle(
|
|
4840
|
+
db,
|
|
4841
|
+
opts.targetTokenId,
|
|
4842
|
+
cand.token.id,
|
|
4843
|
+
ancestors
|
|
4844
|
+
);
|
|
4845
|
+
}
|
|
4846
|
+
results.push({
|
|
4847
|
+
token: cand.token,
|
|
4848
|
+
similarity: cand.similarity,
|
|
4849
|
+
alreadyPrerequisite,
|
|
4850
|
+
wouldCreateCycle: wouldCreateCycleFlag,
|
|
4851
|
+
bloomAboveTarget
|
|
4852
|
+
});
|
|
4853
|
+
}
|
|
4854
|
+
return results;
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4549
4857
|
// src/kernel/system/hooks.ts
|
|
4550
4858
|
import {
|
|
4551
4859
|
appendFileSync as appendFileSync3,
|
|
@@ -6092,6 +6400,21 @@ async function getLegacyRoleConfig(db, role, enabled) {
|
|
|
6092
6400
|
maxFrames: Number.isNaN(parsed) ? 100 : parsed
|
|
6093
6401
|
};
|
|
6094
6402
|
}
|
|
6403
|
+
if (role === "embedding") {
|
|
6404
|
+
const url = await getSetting(db, "llm.embedding.url") || base.url;
|
|
6405
|
+
return {
|
|
6406
|
+
enabled,
|
|
6407
|
+
url,
|
|
6408
|
+
// Kept as a literal (not imported) to avoid a client.ts <-> embedder.ts
|
|
6409
|
+
// import cycle; must match DEFAULT_EMBEDDING_MODEL in embedder.ts.
|
|
6410
|
+
model: await getSetting(db, "llm.embedding.model") || "embeddinggemma",
|
|
6411
|
+
apiKey: await getSetting(db, "llm.embedding.api_key") || base.apiKey,
|
|
6412
|
+
apiFlavor: inferApiFlavor(url),
|
|
6413
|
+
locale: base.locale,
|
|
6414
|
+
source: "legacy",
|
|
6415
|
+
local: isLocalEndpoint(url)
|
|
6416
|
+
};
|
|
6417
|
+
}
|
|
6095
6418
|
return {
|
|
6096
6419
|
enabled,
|
|
6097
6420
|
url: base.url,
|
|
@@ -10110,6 +10433,227 @@ function getCurriculumProvider(id) {
|
|
|
10110
10433
|
return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
|
|
10111
10434
|
}
|
|
10112
10435
|
|
|
10436
|
+
// src/cli/llm/embedder.ts
|
|
10437
|
+
var CANONICAL_EMBEDDING_MODEL_ID = "embeddinggemma-300m";
|
|
10438
|
+
var EMBEDDINGGEMMA_ALIASES = /* @__PURE__ */ new Set([
|
|
10439
|
+
"embeddinggemma",
|
|
10440
|
+
"embeddinggemma:300m",
|
|
10441
|
+
"embeddinggemma-300m",
|
|
10442
|
+
"embed-gemma",
|
|
10443
|
+
"embed-gemma:300m",
|
|
10444
|
+
"google/embeddinggemma-300m"
|
|
10445
|
+
]);
|
|
10446
|
+
function canonicalEmbeddingModelId(model) {
|
|
10447
|
+
const lowered = model.trim().toLowerCase();
|
|
10448
|
+
if (EMBEDDINGGEMMA_ALIASES.has(lowered)) {
|
|
10449
|
+
return CANONICAL_EMBEDDING_MODEL_ID;
|
|
10450
|
+
}
|
|
10451
|
+
return lowered;
|
|
10452
|
+
}
|
|
10453
|
+
function embeddingTextForToken(t2, model) {
|
|
10454
|
+
const isGemma = EMBEDDINGGEMMA_ALIASES.has(model.trim().toLowerCase());
|
|
10455
|
+
const content = embeddingContentForToken(t2);
|
|
10456
|
+
return isGemma ? `title: none | text: ${content}` : content;
|
|
10457
|
+
}
|
|
10458
|
+
function embeddingTextForQuery(text, model) {
|
|
10459
|
+
const isGemma = EMBEDDINGGEMMA_ALIASES.has(model.trim().toLowerCase());
|
|
10460
|
+
return isGemma ? `task: search result | query: ${text}` : text;
|
|
10461
|
+
}
|
|
10462
|
+
async function resolveUsableEmbeddingEndpoint(db) {
|
|
10463
|
+
const cfg = await getProviderForRole(db, "embedding");
|
|
10464
|
+
if (!cfg.enabled) return null;
|
|
10465
|
+
for (const endpoint of [cfg, ...cfg.fallback ? [cfg.fallback] : []]) {
|
|
10466
|
+
if (endpoint.apiFlavor !== "chat-completions") continue;
|
|
10467
|
+
const online = await isLlmOnline(endpoint.url);
|
|
10468
|
+
if (!online) continue;
|
|
10469
|
+
const availableModels = await getAvailableModels(
|
|
10470
|
+
endpoint.url,
|
|
10471
|
+
endpoint.apiKey
|
|
10472
|
+
);
|
|
10473
|
+
const modelAvailable = availableModels.length === 0 || availableModels.some(
|
|
10474
|
+
(candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
|
|
10475
|
+
);
|
|
10476
|
+
if (modelAvailable) return endpoint;
|
|
10477
|
+
}
|
|
10478
|
+
return null;
|
|
10479
|
+
}
|
|
10480
|
+
async function embedTexts(endpoint, texts) {
|
|
10481
|
+
const controller = new AbortController();
|
|
10482
|
+
const timeoutId = setTimeout(() => controller.abort(), 6e4);
|
|
10483
|
+
let res;
|
|
10484
|
+
try {
|
|
10485
|
+
res = await fetch(`${endpoint.url}/embeddings`, {
|
|
10486
|
+
method: "POST",
|
|
10487
|
+
headers: {
|
|
10488
|
+
"Content-Type": "application/json",
|
|
10489
|
+
Authorization: `Bearer ${endpoint.apiKey || DEFAULT_LLM_API_KEY}`
|
|
10490
|
+
},
|
|
10491
|
+
body: JSON.stringify({ model: endpoint.model, input: texts }),
|
|
10492
|
+
signal: controller.signal
|
|
10493
|
+
});
|
|
10494
|
+
} finally {
|
|
10495
|
+
clearTimeout(timeoutId);
|
|
10496
|
+
}
|
|
10497
|
+
if (!res.ok) {
|
|
10498
|
+
const body = await res.text().catch(() => "");
|
|
10499
|
+
throw new Error(
|
|
10500
|
+
`Embedding request failed: ${res.statusText} (${res.status}) - ${body}`
|
|
10501
|
+
);
|
|
10502
|
+
}
|
|
10503
|
+
const data = await res.json();
|
|
10504
|
+
const items = data.data ?? [];
|
|
10505
|
+
if (items.length !== texts.length) {
|
|
10506
|
+
throw new Error(
|
|
10507
|
+
`Embedding response returned ${items.length} vectors for ${texts.length} inputs`
|
|
10508
|
+
);
|
|
10509
|
+
}
|
|
10510
|
+
const ordered = new Array(texts.length);
|
|
10511
|
+
for (let i = 0; i < items.length; i++) {
|
|
10512
|
+
const item = items[i];
|
|
10513
|
+
const index = typeof item.index === "number" ? item.index : i;
|
|
10514
|
+
if (!Number.isInteger(index)) {
|
|
10515
|
+
throw new Error("Embedding response item is missing a valid index");
|
|
10516
|
+
}
|
|
10517
|
+
if (index < 0 || index >= texts.length) {
|
|
10518
|
+
throw new Error(`Embedding response index out of range: ${index}`);
|
|
10519
|
+
}
|
|
10520
|
+
if (!Array.isArray(item.embedding) || item.embedding.length === 0 || !item.embedding.every((n) => typeof n === "number" && Number.isFinite(n))) {
|
|
10521
|
+
throw new Error(
|
|
10522
|
+
`Embedding response at index ${index} is not a non-empty array of finite numbers`
|
|
10523
|
+
);
|
|
10524
|
+
}
|
|
10525
|
+
ordered[index] = item.embedding;
|
|
10526
|
+
}
|
|
10527
|
+
const firstLength = ordered[0]?.length;
|
|
10528
|
+
if (ordered.some((vec) => vec === void 0 || vec.length !== firstLength)) {
|
|
10529
|
+
throw new Error("Embedding response vectors have inconsistent lengths");
|
|
10530
|
+
}
|
|
10531
|
+
return ordered;
|
|
10532
|
+
}
|
|
10533
|
+
var EMBED_BATCH_SIZE = 16;
|
|
10534
|
+
async function ensureTokenEmbeddings(db, opts) {
|
|
10535
|
+
const endpoint = await resolveUsableEmbeddingEndpoint(db);
|
|
10536
|
+
if (!endpoint) {
|
|
10537
|
+
const reason = await describeUnavailableReason(db);
|
|
10538
|
+
return { status: "unavailable", embedded: 0, remaining: 0, reason };
|
|
10539
|
+
}
|
|
10540
|
+
const model = canonicalEmbeddingModelId(endpoint.model);
|
|
10541
|
+
const pending = await listTokensNeedingEmbedding(db, model, {
|
|
10542
|
+
limit: opts?.limit ?? 64,
|
|
10543
|
+
force: opts?.force,
|
|
10544
|
+
dims: opts?.dims
|
|
10545
|
+
});
|
|
10546
|
+
let embedded = 0;
|
|
10547
|
+
let failure;
|
|
10548
|
+
try {
|
|
10549
|
+
for (let i = 0; i < pending.length; i += EMBED_BATCH_SIZE) {
|
|
10550
|
+
const batch = pending.slice(i, i + EMBED_BATCH_SIZE);
|
|
10551
|
+
const formattedTexts = batch.map(
|
|
10552
|
+
(item) => embeddingTextForToken(item.token, endpoint.model)
|
|
10553
|
+
);
|
|
10554
|
+
const vectors = await embedTexts(
|
|
10555
|
+
{ url: endpoint.url, model: endpoint.model, apiKey: endpoint.apiKey },
|
|
10556
|
+
formattedTexts
|
|
10557
|
+
);
|
|
10558
|
+
for (const [index, item] of batch.entries()) {
|
|
10559
|
+
await upsertTokenEmbedding(db, {
|
|
10560
|
+
tokenId: item.token.id,
|
|
10561
|
+
embedding: vectors[index],
|
|
10562
|
+
model,
|
|
10563
|
+
contentHash: computeContentHash(item.text)
|
|
10564
|
+
});
|
|
10565
|
+
embedded++;
|
|
10566
|
+
}
|
|
10567
|
+
}
|
|
10568
|
+
} catch (err) {
|
|
10569
|
+
failure = err instanceof Error ? err.message : String(err);
|
|
10570
|
+
}
|
|
10571
|
+
const coverage = await getEmbeddingCoverage(db, model, { dims: opts?.dims });
|
|
10572
|
+
const remaining = coverage.missing + coverage.stale;
|
|
10573
|
+
if (failure !== void 0) {
|
|
10574
|
+
return { status: "unavailable", embedded, remaining, reason: failure };
|
|
10575
|
+
}
|
|
10576
|
+
return { status: "ok", embedded, remaining };
|
|
10577
|
+
}
|
|
10578
|
+
async function describeUnavailableReason(db) {
|
|
10579
|
+
const cfg = await getProviderForRole(db, "embedding");
|
|
10580
|
+
if (!cfg.enabled) {
|
|
10581
|
+
return "embedding role is disabled in settings (llm.enabled)";
|
|
10582
|
+
}
|
|
10583
|
+
const endpoints = [cfg, ...cfg.fallback ? [cfg.fallback] : []];
|
|
10584
|
+
const online = await Promise.all(
|
|
10585
|
+
endpoints.map((endpoint) => isLlmOnline(endpoint.url))
|
|
10586
|
+
);
|
|
10587
|
+
if (!online.some(Boolean)) {
|
|
10588
|
+
return `embedding endpoints offline (${endpoints.map((endpoint) => endpoint.url).join(", ")})`;
|
|
10589
|
+
}
|
|
10590
|
+
return `no configured embedding model is available (${endpoints.map((endpoint) => endpoint.model).join(", ")})`;
|
|
10591
|
+
}
|
|
10592
|
+
async function embedQuery(db, text) {
|
|
10593
|
+
const endpoint = await resolveUsableEmbeddingEndpoint(db);
|
|
10594
|
+
if (!endpoint) return null;
|
|
10595
|
+
try {
|
|
10596
|
+
const [vector] = await embedTexts(
|
|
10597
|
+
{ url: endpoint.url, model: endpoint.model, apiKey: endpoint.apiKey },
|
|
10598
|
+
[embeddingTextForQuery(text, endpoint.model)]
|
|
10599
|
+
);
|
|
10600
|
+
return { vector, model: canonicalEmbeddingModelId(endpoint.model) };
|
|
10601
|
+
} catch {
|
|
10602
|
+
return null;
|
|
10603
|
+
}
|
|
10604
|
+
}
|
|
10605
|
+
async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
|
|
10606
|
+
const queryText = embeddingContentForToken({
|
|
10607
|
+
concept: candidate.concept,
|
|
10608
|
+
question: candidate.question ?? null,
|
|
10609
|
+
domain: candidate.domain ?? ""
|
|
10610
|
+
});
|
|
10611
|
+
const q2 = await embed(db, queryText);
|
|
10612
|
+
if (!q2) {
|
|
10613
|
+
return [];
|
|
10614
|
+
}
|
|
10615
|
+
const coverage = await getEmbeddingCoverage(db, q2.model, {
|
|
10616
|
+
dims: q2.vector.length
|
|
10617
|
+
});
|
|
10618
|
+
const totalNeeding = coverage.missing + coverage.stale;
|
|
10619
|
+
if (totalNeeding > 0) {
|
|
10620
|
+
await ensureTokenEmbeddings(db, { limit: 100, dims: q2.vector.length });
|
|
10621
|
+
if (totalNeeding > 100) {
|
|
10622
|
+
console.warn(
|
|
10623
|
+
`Warning: Duplicate check is incomplete. ${totalNeeding - 100} tokens are still missing embeddings. Run 'zam token reembed' to complete indexing.`
|
|
10624
|
+
);
|
|
10625
|
+
}
|
|
10626
|
+
}
|
|
10627
|
+
const threshold = await resolveDedupThreshold(db);
|
|
10628
|
+
const hits = await searchTokensHybrid(db, queryText, {
|
|
10629
|
+
queryEmbedding: q2.vector,
|
|
10630
|
+
model: q2.model,
|
|
10631
|
+
limit: 1e3,
|
|
10632
|
+
vectorTopK: 1e3
|
|
10633
|
+
});
|
|
10634
|
+
const results = [];
|
|
10635
|
+
for (const hit of hits) {
|
|
10636
|
+
if (hit.similarity !== null && hit.similarity >= threshold) {
|
|
10637
|
+
results.push({
|
|
10638
|
+
slug: hit.slug,
|
|
10639
|
+
concept: hit.concept,
|
|
10640
|
+
similarity: hit.similarity
|
|
10641
|
+
});
|
|
10642
|
+
}
|
|
10643
|
+
}
|
|
10644
|
+
return results;
|
|
10645
|
+
}
|
|
10646
|
+
async function resolveDedupThreshold(db) {
|
|
10647
|
+
const thresholdStr = await getSetting(db, "search.dedup_threshold");
|
|
10648
|
+
const parsed = thresholdStr ? Number.parseFloat(thresholdStr) : Number.NaN;
|
|
10649
|
+
return Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : 0.85;
|
|
10650
|
+
}
|
|
10651
|
+
async function resolveSuggestMinSimilarity(db) {
|
|
10652
|
+
const minStr = await getSetting(db, "search.suggest_min_similarity");
|
|
10653
|
+
const parsed = minStr ? Number.parseFloat(minStr) : Number.NaN;
|
|
10654
|
+
return Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : 0.45;
|
|
10655
|
+
}
|
|
10656
|
+
|
|
10113
10657
|
// src/cli/llm/vision.ts
|
|
10114
10658
|
import { randomBytes } from "crypto";
|
|
10115
10659
|
import { readFileSync as readFileSync10 } from "fs";
|
|
@@ -10549,7 +11093,7 @@ var VALID_API_FLAVORS = [
|
|
|
10549
11093
|
"chat-completions",
|
|
10550
11094
|
"anthropic-messages"
|
|
10551
11095
|
];
|
|
10552
|
-
var VALID_ROLES = ["vision", "recall", "text"];
|
|
11096
|
+
var VALID_ROLES = ["vision", "recall", "text", "embedding"];
|
|
10553
11097
|
function upsertProviderRecord(providers, name, patch) {
|
|
10554
11098
|
const merged = { ...providers[name] ?? {} };
|
|
10555
11099
|
if (patch.url !== void 0) merged.url = patch.url;
|
|
@@ -11182,14 +11726,23 @@ async function backupDatabaseTo(db, targetDir) {
|
|
|
11182
11726
|
|
|
11183
11727
|
// src/cli/commands/bridge.ts
|
|
11184
11728
|
var isServeMode = false;
|
|
11729
|
+
var serveStdinPayload;
|
|
11185
11730
|
function jsonOut2(data) {
|
|
11186
11731
|
console.log(JSON.stringify(data, null, 2));
|
|
11187
11732
|
}
|
|
11188
11733
|
function jsonError(message) {
|
|
11734
|
+
let msg = message;
|
|
11735
|
+
if (message.startsWith('{"error":')) {
|
|
11736
|
+
try {
|
|
11737
|
+
const parsed = JSON.parse(message);
|
|
11738
|
+
msg = parsed.error;
|
|
11739
|
+
} catch {
|
|
11740
|
+
}
|
|
11741
|
+
}
|
|
11189
11742
|
if (isServeMode) {
|
|
11190
|
-
throw new Error(JSON.stringify({ error:
|
|
11743
|
+
throw new Error(JSON.stringify({ error: msg }));
|
|
11191
11744
|
}
|
|
11192
|
-
console.log(JSON.stringify({ error:
|
|
11745
|
+
console.log(JSON.stringify({ error: msg }, null, 2));
|
|
11193
11746
|
process.exit(1);
|
|
11194
11747
|
}
|
|
11195
11748
|
function parseNonNegativeIntegerOption(name, value) {
|
|
@@ -11512,7 +12065,10 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
|
|
|
11512
12065
|
});
|
|
11513
12066
|
});
|
|
11514
12067
|
});
|
|
11515
|
-
bridgeCommand.command("get-review").description("Get next review card with prompt (JSON)").option("--user <id>", "User ID (default: whoami)").option("--no-resolve", "Skip resolving the token's source_link into context").
|
|
12068
|
+
bridgeCommand.command("get-review").description("Get next review card with prompt (JSON)").option("--user <id>", "User ID (default: whoami)").option("--no-resolve", "Skip resolving the token's source_link into context").option(
|
|
12069
|
+
"--no-dynamic-question",
|
|
12070
|
+
"Use the stored question without generating a fresh LLM question"
|
|
12071
|
+
).action(async (opts) => {
|
|
11516
12072
|
await withDb2(async (db) => {
|
|
11517
12073
|
const userId = await resolveUser(opts, db, { json: true });
|
|
11518
12074
|
const queue = await buildReviewQueue(db, {
|
|
@@ -11536,7 +12092,7 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
11536
12092
|
let resolvedQuestion = item.question;
|
|
11537
12093
|
let questionSource = "original";
|
|
11538
12094
|
let questionModel;
|
|
11539
|
-
if (isLlmEnabled) {
|
|
12095
|
+
if (isLlmEnabled && opts.dynamicQuestion !== false) {
|
|
11540
12096
|
try {
|
|
11541
12097
|
const healed = await ensureHighQualityQuestion(db, {
|
|
11542
12098
|
id: item.tokenId,
|
|
@@ -11783,11 +12339,16 @@ bridgeCommand.command("analyze-monitor").description("Analyze monitor log with t
|
|
|
11783
12339
|
});
|
|
11784
12340
|
return;
|
|
11785
12341
|
}
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
12342
|
+
let raw;
|
|
12343
|
+
if (isServeMode) {
|
|
12344
|
+
raw = serveStdinPayload ?? "";
|
|
12345
|
+
} else {
|
|
12346
|
+
const chunks = [];
|
|
12347
|
+
for await (const chunk of process.stdin) {
|
|
12348
|
+
chunks.push(chunk);
|
|
12349
|
+
}
|
|
12350
|
+
raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
11789
12351
|
}
|
|
11790
|
-
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
11791
12352
|
if (!raw) {
|
|
11792
12353
|
jsonError("No input received on stdin. Pipe JSON with token patterns.");
|
|
11793
12354
|
}
|
|
@@ -11812,13 +12373,17 @@ bridgeCommand.command("analyze-monitor").description("Analyze monitor log with t
|
|
|
11812
12373
|
}
|
|
11813
12374
|
});
|
|
11814
12375
|
bridgeCommand.command("add-token").description("Create a token + card from JSON stdin").option("--user <id>", "User ID (default: whoami)").action(async (opts) => {
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
12376
|
+
await withDb2(async (db) => {
|
|
12377
|
+
let raw;
|
|
12378
|
+
if (isServeMode) {
|
|
12379
|
+
raw = serveStdinPayload ?? "";
|
|
12380
|
+
} else {
|
|
12381
|
+
const chunks = [];
|
|
12382
|
+
for await (const chunk of process.stdin) {
|
|
12383
|
+
chunks.push(chunk);
|
|
12384
|
+
}
|
|
12385
|
+
raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
11820
12386
|
}
|
|
11821
|
-
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
11822
12387
|
if (!raw) {
|
|
11823
12388
|
jsonError("No input received on stdin. Pipe JSON with token data.");
|
|
11824
12389
|
}
|
|
@@ -11831,8 +12396,12 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
|
|
|
11831
12396
|
if (!data?.slug || !data?.concept) {
|
|
11832
12397
|
jsonError("JSON must include 'slug' and 'concept' fields");
|
|
11833
12398
|
}
|
|
11834
|
-
db = await openDatabase();
|
|
11835
12399
|
const userId = await resolveUser(opts, db, { json: true });
|
|
12400
|
+
const possibleDuplicates = await findPossibleDuplicates(db, {
|
|
12401
|
+
concept: data?.concept,
|
|
12402
|
+
question: data?.question ?? null,
|
|
12403
|
+
domain: data?.domain
|
|
12404
|
+
});
|
|
11836
12405
|
const token = await createToken(db, {
|
|
11837
12406
|
slug: data?.slug,
|
|
11838
12407
|
concept: data?.concept,
|
|
@@ -11844,6 +12413,10 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
|
|
|
11844
12413
|
question: data?.question ?? null
|
|
11845
12414
|
});
|
|
11846
12415
|
const card = await ensureCard(db, token.id, userId);
|
|
12416
|
+
try {
|
|
12417
|
+
await ensureTokenEmbeddings(db, { limit: 8 });
|
|
12418
|
+
} catch {
|
|
12419
|
+
}
|
|
11847
12420
|
jsonOut2({
|
|
11848
12421
|
success: true,
|
|
11849
12422
|
token,
|
|
@@ -11854,15 +12427,190 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
|
|
|
11854
12427
|
state: card.state,
|
|
11855
12428
|
dueAt: card.due_at,
|
|
11856
12429
|
blocked: card.blocked
|
|
12430
|
+
},
|
|
12431
|
+
possible_duplicates: possibleDuplicates
|
|
12432
|
+
});
|
|
12433
|
+
});
|
|
12434
|
+
});
|
|
12435
|
+
bridgeCommand.command("relevant-tokens").description("Find tokens relevant to a given context").option("--user <id>", "User ID (default: whoami)").action(async (opts) => {
|
|
12436
|
+
await withDb2(async (db) => {
|
|
12437
|
+
let raw;
|
|
12438
|
+
if (isServeMode) {
|
|
12439
|
+
raw = serveStdinPayload ?? "";
|
|
12440
|
+
} else {
|
|
12441
|
+
const chunks = [];
|
|
12442
|
+
for await (const chunk of process.stdin) {
|
|
12443
|
+
chunks.push(chunk);
|
|
11857
12444
|
}
|
|
12445
|
+
raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
12446
|
+
}
|
|
12447
|
+
if (!raw) {
|
|
12448
|
+
jsonError("No input received on stdin. Pipe JSON with context.");
|
|
12449
|
+
}
|
|
12450
|
+
let data;
|
|
12451
|
+
try {
|
|
12452
|
+
data = JSON.parse(raw);
|
|
12453
|
+
} catch {
|
|
12454
|
+
jsonError("Invalid JSON input");
|
|
12455
|
+
}
|
|
12456
|
+
if (!data?.context || data.context.trim() === "") {
|
|
12457
|
+
jsonError("JSON must include a non-empty 'context' field");
|
|
12458
|
+
}
|
|
12459
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
12460
|
+
const truncatedContext = data.context.slice(0, 2e3);
|
|
12461
|
+
const q2 = await embedQuery(db, truncatedContext);
|
|
12462
|
+
try {
|
|
12463
|
+
await ensureTokenEmbeddings(db, {
|
|
12464
|
+
limit: 32,
|
|
12465
|
+
dims: q2?.vector.length
|
|
12466
|
+
});
|
|
12467
|
+
} catch {
|
|
12468
|
+
}
|
|
12469
|
+
let limit = data.limit ?? 10;
|
|
12470
|
+
if (typeof limit !== "number" || limit <= 0 || !Number.isInteger(limit)) {
|
|
12471
|
+
limit = 10;
|
|
12472
|
+
}
|
|
12473
|
+
if (limit > 100) {
|
|
12474
|
+
limit = 100;
|
|
12475
|
+
}
|
|
12476
|
+
const results = await searchTokensHybrid(db, truncatedContext, {
|
|
12477
|
+
queryEmbedding: q2?.vector,
|
|
12478
|
+
model: q2?.model,
|
|
12479
|
+
limit
|
|
11858
12480
|
});
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
12481
|
+
const tokens = [];
|
|
12482
|
+
for (const t2 of results) {
|
|
12483
|
+
const card = await getCard(db, t2.id, userId);
|
|
12484
|
+
tokens.push({
|
|
12485
|
+
slug: t2.slug,
|
|
12486
|
+
concept: t2.concept,
|
|
12487
|
+
domain: t2.domain,
|
|
12488
|
+
bloom_level: t2.bloom_level,
|
|
12489
|
+
score: t2.score,
|
|
12490
|
+
similarity: t2.similarity,
|
|
12491
|
+
card: card ? {
|
|
12492
|
+
state: card.state,
|
|
12493
|
+
due_at: card.due_at,
|
|
12494
|
+
blocked: card.blocked
|
|
12495
|
+
} : null
|
|
12496
|
+
});
|
|
11864
12497
|
}
|
|
11865
|
-
|
|
12498
|
+
jsonOut2({
|
|
12499
|
+
semantic: q2 !== null,
|
|
12500
|
+
tokens
|
|
12501
|
+
});
|
|
12502
|
+
});
|
|
12503
|
+
});
|
|
12504
|
+
bridgeCommand.command("suggest-foundations").description("Propose existing tokens as foundation/prerequisite candidates").option("--user <id>", "User ID (default: whoami)").action(async (_opts) => {
|
|
12505
|
+
await withDb2(async (db) => {
|
|
12506
|
+
let raw;
|
|
12507
|
+
if (isServeMode) {
|
|
12508
|
+
raw = serveStdinPayload ?? "";
|
|
12509
|
+
} else {
|
|
12510
|
+
const chunks = [];
|
|
12511
|
+
for await (const chunk of process.stdin) {
|
|
12512
|
+
chunks.push(chunk);
|
|
12513
|
+
}
|
|
12514
|
+
raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
12515
|
+
}
|
|
12516
|
+
if (!raw) {
|
|
12517
|
+
jsonError("No input received on stdin. Pipe JSON.");
|
|
12518
|
+
}
|
|
12519
|
+
let data;
|
|
12520
|
+
try {
|
|
12521
|
+
data = JSON.parse(raw);
|
|
12522
|
+
} catch {
|
|
12523
|
+
jsonError("Invalid JSON input");
|
|
12524
|
+
}
|
|
12525
|
+
let queryText = "";
|
|
12526
|
+
let targetTokenId;
|
|
12527
|
+
let targetBloomLevel;
|
|
12528
|
+
let targetJson = null;
|
|
12529
|
+
if (data?.slug !== void 0) {
|
|
12530
|
+
if (typeof data.slug !== "string" || data.slug.trim() === "") {
|
|
12531
|
+
jsonError("Invalid slug");
|
|
12532
|
+
}
|
|
12533
|
+
const token = await getTokenBySlug(db, data.slug);
|
|
12534
|
+
if (!token) {
|
|
12535
|
+
jsonError(`Token not found: ${data.slug}`);
|
|
12536
|
+
}
|
|
12537
|
+
queryText = embeddingContentForToken(token);
|
|
12538
|
+
targetTokenId = token.id;
|
|
12539
|
+
targetBloomLevel = token.bloom_level;
|
|
12540
|
+
targetJson = { slug: token.slug };
|
|
12541
|
+
} else {
|
|
12542
|
+
if (!data?.concept || typeof data.concept !== "string" || data.concept.trim() === "") {
|
|
12543
|
+
jsonError("JSON must include a non-empty 'slug' or 'concept' field");
|
|
12544
|
+
}
|
|
12545
|
+
queryText = embeddingContentForToken({
|
|
12546
|
+
concept: data.concept,
|
|
12547
|
+
question: typeof data.question === "string" ? data.question : null,
|
|
12548
|
+
domain: typeof data.domain === "string" ? data.domain : ""
|
|
12549
|
+
});
|
|
12550
|
+
if (data.bloom_level !== void 0) {
|
|
12551
|
+
if (typeof data.bloom_level !== "number" || !Number.isInteger(data.bloom_level) || data.bloom_level < 1 || data.bloom_level > 5) {
|
|
12552
|
+
jsonError("bloom_level must be an integer between 1 and 5");
|
|
12553
|
+
}
|
|
12554
|
+
targetBloomLevel = data.bloom_level;
|
|
12555
|
+
}
|
|
12556
|
+
}
|
|
12557
|
+
let limit = data?.limit ?? 5;
|
|
12558
|
+
if (typeof limit !== "number" || limit <= 0 || !Number.isInteger(limit)) {
|
|
12559
|
+
limit = 5;
|
|
12560
|
+
}
|
|
12561
|
+
if (limit > 20) {
|
|
12562
|
+
limit = 20;
|
|
12563
|
+
}
|
|
12564
|
+
const q2 = await embedQuery(db, queryText);
|
|
12565
|
+
if (q2 === null) {
|
|
12566
|
+
jsonOut2({
|
|
12567
|
+
semantic: false,
|
|
12568
|
+
target: targetJson,
|
|
12569
|
+
suggestions: []
|
|
12570
|
+
});
|
|
12571
|
+
return;
|
|
12572
|
+
}
|
|
12573
|
+
try {
|
|
12574
|
+
await ensureTokenEmbeddings(db, {
|
|
12575
|
+
limit: 100,
|
|
12576
|
+
dims: q2.vector.length
|
|
12577
|
+
});
|
|
12578
|
+
} catch {
|
|
12579
|
+
}
|
|
12580
|
+
const maxSimilarity = await resolveDedupThreshold(db);
|
|
12581
|
+
const minSimilarity = await resolveSuggestMinSimilarity(db);
|
|
12582
|
+
if (minSimilarity >= maxSimilarity) {
|
|
12583
|
+
jsonOut2({
|
|
12584
|
+
semantic: true,
|
|
12585
|
+
target: targetJson,
|
|
12586
|
+
suggestions: []
|
|
12587
|
+
});
|
|
12588
|
+
return;
|
|
12589
|
+
}
|
|
12590
|
+
const suggestions = await suggestFoundations(db, {
|
|
12591
|
+
queryEmbedding: q2.vector,
|
|
12592
|
+
model: q2.model,
|
|
12593
|
+
targetTokenId,
|
|
12594
|
+
targetBloomLevel,
|
|
12595
|
+
limit,
|
|
12596
|
+
minSimilarity,
|
|
12597
|
+
maxSimilarity
|
|
12598
|
+
});
|
|
12599
|
+
jsonOut2({
|
|
12600
|
+
semantic: true,
|
|
12601
|
+
target: targetJson,
|
|
12602
|
+
suggestions: suggestions.map((s) => ({
|
|
12603
|
+
slug: s.token.slug,
|
|
12604
|
+
concept: s.token.concept,
|
|
12605
|
+
domain: s.token.domain,
|
|
12606
|
+
bloom_level: s.token.bloom_level,
|
|
12607
|
+
similarity: s.similarity,
|
|
12608
|
+
already_prerequisite: s.alreadyPrerequisite,
|
|
12609
|
+
would_create_cycle: s.wouldCreateCycle,
|
|
12610
|
+
bloom_above_target: s.bloomAboveTarget
|
|
12611
|
+
}))
|
|
12612
|
+
});
|
|
12613
|
+
});
|
|
11866
12614
|
});
|
|
11867
12615
|
bridgeCommand.command("discover-skills").description(
|
|
11868
12616
|
"Analyze monitor logs across sessions to discover recurring patterns"
|
|
@@ -12672,12 +13420,13 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
|
|
|
12672
13420
|
});
|
|
12673
13421
|
bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
|
|
12674
13422
|
await withDb2(async (db) => {
|
|
12675
|
-
const [recall, vision, text] = await Promise.all([
|
|
13423
|
+
const [recall, vision, text, embedding] = await Promise.all([
|
|
12676
13424
|
getProviderRoleStatus(db, "recall"),
|
|
12677
13425
|
getProviderRoleStatus(db, "vision"),
|
|
12678
|
-
getProviderRoleStatus(db, "text")
|
|
13426
|
+
getProviderRoleStatus(db, "text"),
|
|
13427
|
+
getProviderRoleStatus(db, "embedding")
|
|
12679
13428
|
]);
|
|
12680
|
-
jsonOut2({ roles: { recall, vision, text } });
|
|
13429
|
+
jsonOut2({ roles: { recall, vision, text, embedding } });
|
|
12681
13430
|
});
|
|
12682
13431
|
});
|
|
12683
13432
|
bridgeCommand.command("provider-config-list").description("List provider records and role bindings (JSON)").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts) => {
|
|
@@ -12983,6 +13732,46 @@ bridgeCommand.command("get-settings").description("Get active ZAM settings (JSON
|
|
|
12983
13732
|
});
|
|
12984
13733
|
});
|
|
12985
13734
|
});
|
|
13735
|
+
async function readDatabaseUserSummaries(db) {
|
|
13736
|
+
return await db.prepare(
|
|
13737
|
+
`SELECT user_id AS id, COUNT(*) AS cardCount
|
|
13738
|
+
FROM cards
|
|
13739
|
+
GROUP BY user_id
|
|
13740
|
+
ORDER BY user_id`
|
|
13741
|
+
).all();
|
|
13742
|
+
}
|
|
13743
|
+
bridgeCommand.command("database-status").description("Show the active database target and learning profiles (JSON)").action(async () => {
|
|
13744
|
+
const target = getDatabaseTargetInfo();
|
|
13745
|
+
await withDb2(async (db) => {
|
|
13746
|
+
const userId = await getSetting(db, "user.id") ?? null;
|
|
13747
|
+
const users = await readDatabaseUserSummaries(db);
|
|
13748
|
+
jsonOut2({
|
|
13749
|
+
success: true,
|
|
13750
|
+
connected: true,
|
|
13751
|
+
target,
|
|
13752
|
+
userId,
|
|
13753
|
+
cardCount: users.find((user) => user.id === userId)?.cardCount ?? 0,
|
|
13754
|
+
users
|
|
13755
|
+
});
|
|
13756
|
+
});
|
|
13757
|
+
});
|
|
13758
|
+
bridgeCommand.command("database-select-user").description("Select an existing learning profile for this database (JSON)").requiredOption("--user <id>", "Existing user ID").action(async (opts) => {
|
|
13759
|
+
await withDb2(async (db) => {
|
|
13760
|
+
const userId = String(opts.user ?? "").trim();
|
|
13761
|
+
const users = await readDatabaseUserSummaries(db);
|
|
13762
|
+
const selected = users.find((user) => user.id === userId);
|
|
13763
|
+
if (!selected) {
|
|
13764
|
+
jsonError(`Learning profile not found: ${userId}`);
|
|
13765
|
+
return;
|
|
13766
|
+
}
|
|
13767
|
+
await setSetting(db, "user.id", userId);
|
|
13768
|
+
jsonOut2({
|
|
13769
|
+
success: true,
|
|
13770
|
+
userId,
|
|
13771
|
+
cardCount: selected.cardCount
|
|
13772
|
+
});
|
|
13773
|
+
});
|
|
13774
|
+
});
|
|
12986
13775
|
bridgeCommand.command("list-tokens").description(
|
|
12987
13776
|
"List tokens (optionally enriched with user card state for viz) (JSON)"
|
|
12988
13777
|
).option(
|
|
@@ -13707,6 +14496,13 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
13707
14496
|
requestId = req.id ?? null;
|
|
13708
14497
|
const cmd = req.cmd;
|
|
13709
14498
|
const args = req.args ?? [];
|
|
14499
|
+
if (typeof req.stdin === "string") {
|
|
14500
|
+
serveStdinPayload = req.stdin;
|
|
14501
|
+
} else if (typeof req.stdin === "object" && req.stdin !== null) {
|
|
14502
|
+
serveStdinPayload = JSON.stringify(req.stdin);
|
|
14503
|
+
} else {
|
|
14504
|
+
serveStdinPayload = void 0;
|
|
14505
|
+
}
|
|
13710
14506
|
if (!cmd) {
|
|
13711
14507
|
return JSON.stringify({
|
|
13712
14508
|
id: requestId,
|
|
@@ -13761,6 +14557,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
13761
14557
|
id: requestId,
|
|
13762
14558
|
error: `Invalid JSON request: ${err.message}`
|
|
13763
14559
|
});
|
|
14560
|
+
} finally {
|
|
14561
|
+
serveStdinPayload = void 0;
|
|
13764
14562
|
}
|
|
13765
14563
|
};
|
|
13766
14564
|
const readline = await import("readline");
|
|
@@ -16766,7 +17564,7 @@ import { Command as Command20 } from "commander";
|
|
|
16766
17564
|
var tokenCommand = new Command20("token").description(
|
|
16767
17565
|
"Manage knowledge tokens"
|
|
16768
17566
|
);
|
|
16769
|
-
tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
|
|
17567
|
+
tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--user <id>", "Owner of the personal card (default: whoami)").option("--no-card", "Register the token only; do not create a personal card").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
|
|
16770
17568
|
await withDb(async (db) => {
|
|
16771
17569
|
let question = opts.question || null;
|
|
16772
17570
|
if (!question) {
|
|
@@ -16776,6 +17574,11 @@ tokenCommand.command("register").description("Register a new knowledge token").r
|
|
|
16776
17574
|
opts.domain
|
|
16777
17575
|
);
|
|
16778
17576
|
}
|
|
17577
|
+
const possibleDuplicates = await findPossibleDuplicates(db, {
|
|
17578
|
+
concept: opts.concept,
|
|
17579
|
+
question,
|
|
17580
|
+
domain: opts.domain
|
|
17581
|
+
});
|
|
16779
17582
|
const token = await createToken(db, {
|
|
16780
17583
|
slug: opts.slug,
|
|
16781
17584
|
concept: opts.concept,
|
|
@@ -16784,9 +17587,30 @@ tokenCommand.command("register").description("Register a new knowledge token").r
|
|
|
16784
17587
|
source_link: opts.sourceLink || null,
|
|
16785
17588
|
question
|
|
16786
17589
|
});
|
|
17590
|
+
let cardUserId = null;
|
|
17591
|
+
if (opts.card !== false) {
|
|
17592
|
+
cardUserId = opts.user ?? await getSetting(db, "user.id");
|
|
17593
|
+
if (cardUserId) {
|
|
17594
|
+
await ensureCard(db, token.id, cardUserId);
|
|
17595
|
+
}
|
|
17596
|
+
}
|
|
17597
|
+
try {
|
|
17598
|
+
await ensureTokenEmbeddings(db, { limit: 8 });
|
|
17599
|
+
} catch {
|
|
17600
|
+
}
|
|
16787
17601
|
if (opts.quiet) return;
|
|
16788
17602
|
if (opts.json) {
|
|
16789
|
-
console.log(
|
|
17603
|
+
console.log(
|
|
17604
|
+
JSON.stringify(
|
|
17605
|
+
{
|
|
17606
|
+
...token,
|
|
17607
|
+
card: cardUserId ? { userId: cardUserId } : null,
|
|
17608
|
+
possible_duplicates: possibleDuplicates
|
|
17609
|
+
},
|
|
17610
|
+
null,
|
|
17611
|
+
2
|
|
17612
|
+
)
|
|
17613
|
+
);
|
|
16790
17614
|
} else {
|
|
16791
17615
|
console.log(`Registered token: ${token.slug} (${token.id})`);
|
|
16792
17616
|
console.log(` Concept: ${token.concept}`);
|
|
@@ -16796,12 +17620,81 @@ tokenCommand.command("register").description("Register a new knowledge token").r
|
|
|
16796
17620
|
if (token.source_link) {
|
|
16797
17621
|
console.log(` Source: ${token.source_link}`);
|
|
16798
17622
|
}
|
|
17623
|
+
if (cardUserId) {
|
|
17624
|
+
console.log(` Card: created for ${cardUserId}`);
|
|
17625
|
+
} else if (opts.card === false) {
|
|
17626
|
+
console.log(` Card: skipped (--no-card)`);
|
|
17627
|
+
} else {
|
|
17628
|
+
console.log(` Card: skipped (no default user set)`);
|
|
17629
|
+
}
|
|
17630
|
+
if (possibleDuplicates.length > 0) {
|
|
17631
|
+
console.log(`
|
|
17632
|
+
WARNING: Possible duplicate tokens found:`);
|
|
17633
|
+
for (const dup of possibleDuplicates) {
|
|
17634
|
+
console.log(
|
|
17635
|
+
` - ${dup.slug} (similarity: ${dup.similarity.toFixed(2)})`
|
|
17636
|
+
);
|
|
17637
|
+
}
|
|
17638
|
+
}
|
|
17639
|
+
try {
|
|
17640
|
+
const queryText = embeddingContentForToken({
|
|
17641
|
+
concept: opts.concept,
|
|
17642
|
+
question: question ?? null,
|
|
17643
|
+
domain: opts.domain ?? ""
|
|
17644
|
+
});
|
|
17645
|
+
const q2 = await embedQuery(db, queryText);
|
|
17646
|
+
if (q2) {
|
|
17647
|
+
const maxSimilarity = await resolveDedupThreshold(db);
|
|
17648
|
+
const minSimilarity = await resolveSuggestMinSimilarity(db);
|
|
17649
|
+
const suggestions = await suggestFoundations(db, {
|
|
17650
|
+
queryEmbedding: q2.vector,
|
|
17651
|
+
model: q2.model,
|
|
17652
|
+
targetTokenId: token.id,
|
|
17653
|
+
targetBloomLevel: Number(opts.bloom),
|
|
17654
|
+
limit: 3,
|
|
17655
|
+
minSimilarity,
|
|
17656
|
+
maxSimilarity
|
|
17657
|
+
});
|
|
17658
|
+
const filtered = suggestions.filter(
|
|
17659
|
+
(s) => !s.wouldCreateCycle && !s.alreadyPrerequisite
|
|
17660
|
+
);
|
|
17661
|
+
if (filtered.length > 0) {
|
|
17662
|
+
console.log(
|
|
17663
|
+
`
|
|
17664
|
+
Related existing tokens as potential foundations:`
|
|
17665
|
+
);
|
|
17666
|
+
for (const s of filtered) {
|
|
17667
|
+
const note = s.bloomAboveTarget ? " (higher bloom than target)" : "";
|
|
17668
|
+
console.log(
|
|
17669
|
+
` - ${s.token.slug} (similarity: ${s.similarity.toFixed(2)})${note}`
|
|
17670
|
+
);
|
|
17671
|
+
}
|
|
17672
|
+
console.log(
|
|
17673
|
+
`Link with: zam token prereq --token <slug> --requires <slug> (see zam token prereq --help)`
|
|
17674
|
+
);
|
|
17675
|
+
}
|
|
17676
|
+
}
|
|
17677
|
+
} catch {
|
|
17678
|
+
}
|
|
16799
17679
|
}
|
|
16800
17680
|
});
|
|
16801
17681
|
});
|
|
16802
17682
|
tokenCommand.command("find").description("Fuzzy search for tokens").requiredOption("--query <query>", "Search query").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
|
|
16803
17683
|
await withDb(async (db) => {
|
|
16804
|
-
const
|
|
17684
|
+
const q2 = await embedQuery(db, opts.query);
|
|
17685
|
+
const embRes = await ensureTokenEmbeddings(db, {
|
|
17686
|
+
limit: 32,
|
|
17687
|
+
dims: q2?.vector.length
|
|
17688
|
+
});
|
|
17689
|
+
if (embRes.status === "unavailable" && !opts.json && !opts.quiet) {
|
|
17690
|
+
console.error(
|
|
17691
|
+
`Note: semantic search unavailable (${embRes.reason}) \u2014 lexical matches only.`
|
|
17692
|
+
);
|
|
17693
|
+
}
|
|
17694
|
+
const results = await searchTokensHybrid(db, opts.query, {
|
|
17695
|
+
queryEmbedding: q2?.vector,
|
|
17696
|
+
model: q2?.model
|
|
17697
|
+
});
|
|
16805
17698
|
if (opts.quiet) return;
|
|
16806
17699
|
if (opts.json) {
|
|
16807
17700
|
console.log(JSON.stringify(results, null, 2));
|
|
@@ -16814,12 +17707,14 @@ tokenCommand.command("find").description("Fuzzy search for tokens").requiredOpti
|
|
|
16814
17707
|
console.log(`Found ${results.length} token(s):
|
|
16815
17708
|
`);
|
|
16816
17709
|
console.log(
|
|
16817
|
-
"Score Slug Concept Domain Bloom"
|
|
17710
|
+
"Score Sim Slug Concept Domain Bloom"
|
|
16818
17711
|
);
|
|
16819
|
-
console.log("\u2500".repeat(
|
|
17712
|
+
console.log("\u2500".repeat(95));
|
|
16820
17713
|
for (const t2 of results) {
|
|
17714
|
+
const scoreStr = t2.score.toFixed(3).padEnd(6);
|
|
17715
|
+
const simStr = (t2.similarity?.toFixed(2) ?? "-").padEnd(4);
|
|
16821
17716
|
console.log(
|
|
16822
|
-
`${
|
|
17717
|
+
`${scoreStr} ${simStr} ${t2.slug.padEnd(21)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
|
|
16823
17718
|
);
|
|
16824
17719
|
}
|
|
16825
17720
|
});
|
|
@@ -17047,6 +17942,56 @@ tokenCommand.command("status").description("Show full status of a token for a us
|
|
|
17047
17942
|
}
|
|
17048
17943
|
});
|
|
17049
17944
|
});
|
|
17945
|
+
tokenCommand.command("reembed").description("Backfill or refresh semantic-search embeddings for tokens").option("--all", "Force re-embed every token, including already-fresh ones").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
|
|
17946
|
+
await withDb(async (db) => {
|
|
17947
|
+
const probe = await embedQuery(db, "ZAM embedding dimension probe");
|
|
17948
|
+
const model = probe?.model ?? null;
|
|
17949
|
+
const dims = probe?.vector.length;
|
|
17950
|
+
const before = model ? await getEmbeddingCoverage(db, model, { dims }) : { tokens: 0, embedded: 0, missing: 0, stale: 0 };
|
|
17951
|
+
let embedded = 0;
|
|
17952
|
+
let reason;
|
|
17953
|
+
let force = Boolean(opts.all);
|
|
17954
|
+
while (true) {
|
|
17955
|
+
const result = await ensureTokenEmbeddings(
|
|
17956
|
+
db,
|
|
17957
|
+
force ? { force: true, limit: Number.MAX_SAFE_INTEGER, dims } : { dims }
|
|
17958
|
+
);
|
|
17959
|
+
force = false;
|
|
17960
|
+
embedded += result.embedded;
|
|
17961
|
+
if (result.status === "unavailable") {
|
|
17962
|
+
reason = result.reason;
|
|
17963
|
+
break;
|
|
17964
|
+
}
|
|
17965
|
+
if (result.remaining === 0 || result.embedded === 0) break;
|
|
17966
|
+
}
|
|
17967
|
+
if (reason !== void 0) {
|
|
17968
|
+
if (opts.quiet) {
|
|
17969
|
+
process.exit(1);
|
|
17970
|
+
}
|
|
17971
|
+
if (opts.json) {
|
|
17972
|
+
jsonOut({ error: reason, embedded });
|
|
17973
|
+
} else {
|
|
17974
|
+
console.error(`Error: ${reason}`);
|
|
17975
|
+
if (embedded > 0) {
|
|
17976
|
+
console.error(
|
|
17977
|
+
`(${embedded} token(s) were embedded before the failure)`
|
|
17978
|
+
);
|
|
17979
|
+
}
|
|
17980
|
+
}
|
|
17981
|
+
process.exit(1);
|
|
17982
|
+
}
|
|
17983
|
+
const after = await getEmbeddingCoverage(db, model, { dims });
|
|
17984
|
+
if (opts.quiet) return;
|
|
17985
|
+
const staleBefore = before.missing + before.stale;
|
|
17986
|
+
if (opts.json) {
|
|
17987
|
+
jsonOut({ embedded, model, before, after });
|
|
17988
|
+
return;
|
|
17989
|
+
}
|
|
17990
|
+
console.log(
|
|
17991
|
+
`Embedded ${embedded} tokens (${after.tokens} total, ${staleBefore} stale before) with ${model}`
|
|
17992
|
+
);
|
|
17993
|
+
});
|
|
17994
|
+
});
|
|
17050
17995
|
|
|
17051
17996
|
// src/cli/commands/ui.ts
|
|
17052
17997
|
import { spawn as spawn3, spawnSync } from "child_process";
|