zam-core 0.6.3 → 0.7.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/.agent/skills/zam/SKILL.md +7 -3
- package/.agents/skills/zam/SKILL.md +7 -3
- package/.claude/skills/zam/SKILL.md +7 -3
- package/dist/cli/index.js +752 -31
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +122 -2
- package/dist/index.js +268 -0
- 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
|
|
@@ -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,82 @@ 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
|
+
|
|
4549
4792
|
// src/kernel/system/hooks.ts
|
|
4550
4793
|
import {
|
|
4551
4794
|
appendFileSync as appendFileSync3,
|
|
@@ -6092,6 +6335,21 @@ async function getLegacyRoleConfig(db, role, enabled) {
|
|
|
6092
6335
|
maxFrames: Number.isNaN(parsed) ? 100 : parsed
|
|
6093
6336
|
};
|
|
6094
6337
|
}
|
|
6338
|
+
if (role === "embedding") {
|
|
6339
|
+
const url = await getSetting(db, "llm.embedding.url") || base.url;
|
|
6340
|
+
return {
|
|
6341
|
+
enabled,
|
|
6342
|
+
url,
|
|
6343
|
+
// Kept as a literal (not imported) to avoid a client.ts <-> embedder.ts
|
|
6344
|
+
// import cycle; must match DEFAULT_EMBEDDING_MODEL in embedder.ts.
|
|
6345
|
+
model: await getSetting(db, "llm.embedding.model") || "embeddinggemma",
|
|
6346
|
+
apiKey: await getSetting(db, "llm.embedding.api_key") || base.apiKey,
|
|
6347
|
+
apiFlavor: inferApiFlavor(url),
|
|
6348
|
+
locale: base.locale,
|
|
6349
|
+
source: "legacy",
|
|
6350
|
+
local: isLocalEndpoint(url)
|
|
6351
|
+
};
|
|
6352
|
+
}
|
|
6095
6353
|
return {
|
|
6096
6354
|
enabled,
|
|
6097
6355
|
url: base.url,
|
|
@@ -10110,6 +10368,219 @@ function getCurriculumProvider(id) {
|
|
|
10110
10368
|
return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
|
|
10111
10369
|
}
|
|
10112
10370
|
|
|
10371
|
+
// src/cli/llm/embedder.ts
|
|
10372
|
+
var CANONICAL_EMBEDDING_MODEL_ID = "embeddinggemma-300m";
|
|
10373
|
+
var EMBEDDINGGEMMA_ALIASES = /* @__PURE__ */ new Set([
|
|
10374
|
+
"embeddinggemma",
|
|
10375
|
+
"embeddinggemma:300m",
|
|
10376
|
+
"embeddinggemma-300m",
|
|
10377
|
+
"embed-gemma",
|
|
10378
|
+
"embed-gemma:300m",
|
|
10379
|
+
"google/embeddinggemma-300m"
|
|
10380
|
+
]);
|
|
10381
|
+
function canonicalEmbeddingModelId(model) {
|
|
10382
|
+
const lowered = model.trim().toLowerCase();
|
|
10383
|
+
if (EMBEDDINGGEMMA_ALIASES.has(lowered)) {
|
|
10384
|
+
return CANONICAL_EMBEDDING_MODEL_ID;
|
|
10385
|
+
}
|
|
10386
|
+
return lowered;
|
|
10387
|
+
}
|
|
10388
|
+
function embeddingTextForToken(t2, model) {
|
|
10389
|
+
const isGemma = EMBEDDINGGEMMA_ALIASES.has(model.trim().toLowerCase());
|
|
10390
|
+
const content = embeddingContentForToken(t2);
|
|
10391
|
+
return isGemma ? `title: none | text: ${content}` : content;
|
|
10392
|
+
}
|
|
10393
|
+
function embeddingTextForQuery(text, model) {
|
|
10394
|
+
const isGemma = EMBEDDINGGEMMA_ALIASES.has(model.trim().toLowerCase());
|
|
10395
|
+
return isGemma ? `task: search result | query: ${text}` : text;
|
|
10396
|
+
}
|
|
10397
|
+
async function resolveUsableEmbeddingEndpoint(db) {
|
|
10398
|
+
const cfg = await getProviderForRole(db, "embedding");
|
|
10399
|
+
if (!cfg.enabled) return null;
|
|
10400
|
+
for (const endpoint of [cfg, ...cfg.fallback ? [cfg.fallback] : []]) {
|
|
10401
|
+
if (endpoint.apiFlavor !== "chat-completions") continue;
|
|
10402
|
+
const online = await isLlmOnline(endpoint.url);
|
|
10403
|
+
if (!online) continue;
|
|
10404
|
+
const availableModels = await getAvailableModels(
|
|
10405
|
+
endpoint.url,
|
|
10406
|
+
endpoint.apiKey
|
|
10407
|
+
);
|
|
10408
|
+
const modelAvailable = availableModels.length === 0 || availableModels.some(
|
|
10409
|
+
(candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
|
|
10410
|
+
);
|
|
10411
|
+
if (modelAvailable) return endpoint;
|
|
10412
|
+
}
|
|
10413
|
+
return null;
|
|
10414
|
+
}
|
|
10415
|
+
async function embedTexts(endpoint, texts) {
|
|
10416
|
+
const controller = new AbortController();
|
|
10417
|
+
const timeoutId = setTimeout(() => controller.abort(), 6e4);
|
|
10418
|
+
let res;
|
|
10419
|
+
try {
|
|
10420
|
+
res = await fetch(`${endpoint.url}/embeddings`, {
|
|
10421
|
+
method: "POST",
|
|
10422
|
+
headers: {
|
|
10423
|
+
"Content-Type": "application/json",
|
|
10424
|
+
Authorization: `Bearer ${endpoint.apiKey || DEFAULT_LLM_API_KEY}`
|
|
10425
|
+
},
|
|
10426
|
+
body: JSON.stringify({ model: endpoint.model, input: texts }),
|
|
10427
|
+
signal: controller.signal
|
|
10428
|
+
});
|
|
10429
|
+
} finally {
|
|
10430
|
+
clearTimeout(timeoutId);
|
|
10431
|
+
}
|
|
10432
|
+
if (!res.ok) {
|
|
10433
|
+
const body = await res.text().catch(() => "");
|
|
10434
|
+
throw new Error(
|
|
10435
|
+
`Embedding request failed: ${res.statusText} (${res.status}) - ${body}`
|
|
10436
|
+
);
|
|
10437
|
+
}
|
|
10438
|
+
const data = await res.json();
|
|
10439
|
+
const items = data.data ?? [];
|
|
10440
|
+
if (items.length !== texts.length) {
|
|
10441
|
+
throw new Error(
|
|
10442
|
+
`Embedding response returned ${items.length} vectors for ${texts.length} inputs`
|
|
10443
|
+
);
|
|
10444
|
+
}
|
|
10445
|
+
const ordered = new Array(texts.length);
|
|
10446
|
+
for (let i = 0; i < items.length; i++) {
|
|
10447
|
+
const item = items[i];
|
|
10448
|
+
const index = typeof item.index === "number" ? item.index : i;
|
|
10449
|
+
if (!Number.isInteger(index)) {
|
|
10450
|
+
throw new Error("Embedding response item is missing a valid index");
|
|
10451
|
+
}
|
|
10452
|
+
if (index < 0 || index >= texts.length) {
|
|
10453
|
+
throw new Error(`Embedding response index out of range: ${index}`);
|
|
10454
|
+
}
|
|
10455
|
+
if (!Array.isArray(item.embedding) || item.embedding.length === 0 || !item.embedding.every((n) => typeof n === "number" && Number.isFinite(n))) {
|
|
10456
|
+
throw new Error(
|
|
10457
|
+
`Embedding response at index ${index} is not a non-empty array of finite numbers`
|
|
10458
|
+
);
|
|
10459
|
+
}
|
|
10460
|
+
ordered[index] = item.embedding;
|
|
10461
|
+
}
|
|
10462
|
+
const firstLength = ordered[0]?.length;
|
|
10463
|
+
if (ordered.some((vec) => vec === void 0 || vec.length !== firstLength)) {
|
|
10464
|
+
throw new Error("Embedding response vectors have inconsistent lengths");
|
|
10465
|
+
}
|
|
10466
|
+
return ordered;
|
|
10467
|
+
}
|
|
10468
|
+
var EMBED_BATCH_SIZE = 16;
|
|
10469
|
+
async function ensureTokenEmbeddings(db, opts) {
|
|
10470
|
+
const endpoint = await resolveUsableEmbeddingEndpoint(db);
|
|
10471
|
+
if (!endpoint) {
|
|
10472
|
+
const reason = await describeUnavailableReason(db);
|
|
10473
|
+
return { status: "unavailable", embedded: 0, remaining: 0, reason };
|
|
10474
|
+
}
|
|
10475
|
+
const model = canonicalEmbeddingModelId(endpoint.model);
|
|
10476
|
+
const pending = await listTokensNeedingEmbedding(db, model, {
|
|
10477
|
+
limit: opts?.limit ?? 64,
|
|
10478
|
+
force: opts?.force,
|
|
10479
|
+
dims: opts?.dims
|
|
10480
|
+
});
|
|
10481
|
+
let embedded = 0;
|
|
10482
|
+
let failure;
|
|
10483
|
+
try {
|
|
10484
|
+
for (let i = 0; i < pending.length; i += EMBED_BATCH_SIZE) {
|
|
10485
|
+
const batch = pending.slice(i, i + EMBED_BATCH_SIZE);
|
|
10486
|
+
const formattedTexts = batch.map(
|
|
10487
|
+
(item) => embeddingTextForToken(item.token, endpoint.model)
|
|
10488
|
+
);
|
|
10489
|
+
const vectors = await embedTexts(
|
|
10490
|
+
{ url: endpoint.url, model: endpoint.model, apiKey: endpoint.apiKey },
|
|
10491
|
+
formattedTexts
|
|
10492
|
+
);
|
|
10493
|
+
for (const [index, item] of batch.entries()) {
|
|
10494
|
+
await upsertTokenEmbedding(db, {
|
|
10495
|
+
tokenId: item.token.id,
|
|
10496
|
+
embedding: vectors[index],
|
|
10497
|
+
model,
|
|
10498
|
+
contentHash: computeContentHash(item.text)
|
|
10499
|
+
});
|
|
10500
|
+
embedded++;
|
|
10501
|
+
}
|
|
10502
|
+
}
|
|
10503
|
+
} catch (err) {
|
|
10504
|
+
failure = err instanceof Error ? err.message : String(err);
|
|
10505
|
+
}
|
|
10506
|
+
const coverage = await getEmbeddingCoverage(db, model, { dims: opts?.dims });
|
|
10507
|
+
const remaining = coverage.missing + coverage.stale;
|
|
10508
|
+
if (failure !== void 0) {
|
|
10509
|
+
return { status: "unavailable", embedded, remaining, reason: failure };
|
|
10510
|
+
}
|
|
10511
|
+
return { status: "ok", embedded, remaining };
|
|
10512
|
+
}
|
|
10513
|
+
async function describeUnavailableReason(db) {
|
|
10514
|
+
const cfg = await getProviderForRole(db, "embedding");
|
|
10515
|
+
if (!cfg.enabled) {
|
|
10516
|
+
return "embedding role is disabled in settings (llm.enabled)";
|
|
10517
|
+
}
|
|
10518
|
+
const endpoints = [cfg, ...cfg.fallback ? [cfg.fallback] : []];
|
|
10519
|
+
const online = await Promise.all(
|
|
10520
|
+
endpoints.map((endpoint) => isLlmOnline(endpoint.url))
|
|
10521
|
+
);
|
|
10522
|
+
if (!online.some(Boolean)) {
|
|
10523
|
+
return `embedding endpoints offline (${endpoints.map((endpoint) => endpoint.url).join(", ")})`;
|
|
10524
|
+
}
|
|
10525
|
+
return `no configured embedding model is available (${endpoints.map((endpoint) => endpoint.model).join(", ")})`;
|
|
10526
|
+
}
|
|
10527
|
+
async function embedQuery(db, text) {
|
|
10528
|
+
const endpoint = await resolveUsableEmbeddingEndpoint(db);
|
|
10529
|
+
if (!endpoint) return null;
|
|
10530
|
+
try {
|
|
10531
|
+
const [vector] = await embedTexts(
|
|
10532
|
+
{ url: endpoint.url, model: endpoint.model, apiKey: endpoint.apiKey },
|
|
10533
|
+
[embeddingTextForQuery(text, endpoint.model)]
|
|
10534
|
+
);
|
|
10535
|
+
return { vector, model: canonicalEmbeddingModelId(endpoint.model) };
|
|
10536
|
+
} catch {
|
|
10537
|
+
return null;
|
|
10538
|
+
}
|
|
10539
|
+
}
|
|
10540
|
+
async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
|
|
10541
|
+
const queryText = embeddingContentForToken({
|
|
10542
|
+
concept: candidate.concept,
|
|
10543
|
+
question: candidate.question ?? null,
|
|
10544
|
+
domain: candidate.domain ?? ""
|
|
10545
|
+
});
|
|
10546
|
+
const q2 = await embed(db, queryText);
|
|
10547
|
+
if (!q2) {
|
|
10548
|
+
return [];
|
|
10549
|
+
}
|
|
10550
|
+
const coverage = await getEmbeddingCoverage(db, q2.model, {
|
|
10551
|
+
dims: q2.vector.length
|
|
10552
|
+
});
|
|
10553
|
+
const totalNeeding = coverage.missing + coverage.stale;
|
|
10554
|
+
if (totalNeeding > 0) {
|
|
10555
|
+
await ensureTokenEmbeddings(db, { limit: 100, dims: q2.vector.length });
|
|
10556
|
+
if (totalNeeding > 100) {
|
|
10557
|
+
console.warn(
|
|
10558
|
+
`Warning: Duplicate check is incomplete. ${totalNeeding - 100} tokens are still missing embeddings. Run 'zam token reembed' to complete indexing.`
|
|
10559
|
+
);
|
|
10560
|
+
}
|
|
10561
|
+
}
|
|
10562
|
+
const thresholdStr = await getSetting(db, "search.dedup_threshold");
|
|
10563
|
+
const parsed = thresholdStr ? Number.parseFloat(thresholdStr) : Number.NaN;
|
|
10564
|
+
const threshold = Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : 0.85;
|
|
10565
|
+
const hits = await searchTokensHybrid(db, queryText, {
|
|
10566
|
+
queryEmbedding: q2.vector,
|
|
10567
|
+
model: q2.model,
|
|
10568
|
+
limit: 1e3,
|
|
10569
|
+
vectorTopK: 1e3
|
|
10570
|
+
});
|
|
10571
|
+
const results = [];
|
|
10572
|
+
for (const hit of hits) {
|
|
10573
|
+
if (hit.similarity !== null && hit.similarity >= threshold) {
|
|
10574
|
+
results.push({
|
|
10575
|
+
slug: hit.slug,
|
|
10576
|
+
concept: hit.concept,
|
|
10577
|
+
similarity: hit.similarity
|
|
10578
|
+
});
|
|
10579
|
+
}
|
|
10580
|
+
}
|
|
10581
|
+
return results;
|
|
10582
|
+
}
|
|
10583
|
+
|
|
10113
10584
|
// src/cli/llm/vision.ts
|
|
10114
10585
|
import { randomBytes } from "crypto";
|
|
10115
10586
|
import { readFileSync as readFileSync10 } from "fs";
|
|
@@ -10549,7 +11020,7 @@ var VALID_API_FLAVORS = [
|
|
|
10549
11020
|
"chat-completions",
|
|
10550
11021
|
"anthropic-messages"
|
|
10551
11022
|
];
|
|
10552
|
-
var VALID_ROLES = ["vision", "recall", "text"];
|
|
11023
|
+
var VALID_ROLES = ["vision", "recall", "text", "embedding"];
|
|
10553
11024
|
function upsertProviderRecord(providers, name, patch) {
|
|
10554
11025
|
const merged = { ...providers[name] ?? {} };
|
|
10555
11026
|
if (patch.url !== void 0) merged.url = patch.url;
|
|
@@ -11182,14 +11653,23 @@ async function backupDatabaseTo(db, targetDir) {
|
|
|
11182
11653
|
|
|
11183
11654
|
// src/cli/commands/bridge.ts
|
|
11184
11655
|
var isServeMode = false;
|
|
11656
|
+
var serveStdinPayload;
|
|
11185
11657
|
function jsonOut2(data) {
|
|
11186
11658
|
console.log(JSON.stringify(data, null, 2));
|
|
11187
11659
|
}
|
|
11188
11660
|
function jsonError(message) {
|
|
11661
|
+
let msg = message;
|
|
11662
|
+
if (message.startsWith('{"error":')) {
|
|
11663
|
+
try {
|
|
11664
|
+
const parsed = JSON.parse(message);
|
|
11665
|
+
msg = parsed.error;
|
|
11666
|
+
} catch {
|
|
11667
|
+
}
|
|
11668
|
+
}
|
|
11189
11669
|
if (isServeMode) {
|
|
11190
|
-
throw new Error(JSON.stringify({ error:
|
|
11670
|
+
throw new Error(JSON.stringify({ error: msg }));
|
|
11191
11671
|
}
|
|
11192
|
-
console.log(JSON.stringify({ error:
|
|
11672
|
+
console.log(JSON.stringify({ error: msg }, null, 2));
|
|
11193
11673
|
process.exit(1);
|
|
11194
11674
|
}
|
|
11195
11675
|
function parseNonNegativeIntegerOption(name, value) {
|
|
@@ -11512,7 +11992,10 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
|
|
|
11512
11992
|
});
|
|
11513
11993
|
});
|
|
11514
11994
|
});
|
|
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").
|
|
11995
|
+
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(
|
|
11996
|
+
"--no-dynamic-question",
|
|
11997
|
+
"Use the stored question without generating a fresh LLM question"
|
|
11998
|
+
).action(async (opts) => {
|
|
11516
11999
|
await withDb2(async (db) => {
|
|
11517
12000
|
const userId = await resolveUser(opts, db, { json: true });
|
|
11518
12001
|
const queue = await buildReviewQueue(db, {
|
|
@@ -11536,7 +12019,7 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
11536
12019
|
let resolvedQuestion = item.question;
|
|
11537
12020
|
let questionSource = "original";
|
|
11538
12021
|
let questionModel;
|
|
11539
|
-
if (isLlmEnabled) {
|
|
12022
|
+
if (isLlmEnabled && opts.dynamicQuestion !== false) {
|
|
11540
12023
|
try {
|
|
11541
12024
|
const healed = await ensureHighQualityQuestion(db, {
|
|
11542
12025
|
id: item.tokenId,
|
|
@@ -11783,11 +12266,16 @@ bridgeCommand.command("analyze-monitor").description("Analyze monitor log with t
|
|
|
11783
12266
|
});
|
|
11784
12267
|
return;
|
|
11785
12268
|
}
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
12269
|
+
let raw;
|
|
12270
|
+
if (isServeMode) {
|
|
12271
|
+
raw = serveStdinPayload ?? "";
|
|
12272
|
+
} else {
|
|
12273
|
+
const chunks = [];
|
|
12274
|
+
for await (const chunk of process.stdin) {
|
|
12275
|
+
chunks.push(chunk);
|
|
12276
|
+
}
|
|
12277
|
+
raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
11789
12278
|
}
|
|
11790
|
-
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
11791
12279
|
if (!raw) {
|
|
11792
12280
|
jsonError("No input received on stdin. Pipe JSON with token patterns.");
|
|
11793
12281
|
}
|
|
@@ -11812,13 +12300,17 @@ bridgeCommand.command("analyze-monitor").description("Analyze monitor log with t
|
|
|
11812
12300
|
}
|
|
11813
12301
|
});
|
|
11814
12302
|
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
|
-
|
|
12303
|
+
await withDb2(async (db) => {
|
|
12304
|
+
let raw;
|
|
12305
|
+
if (isServeMode) {
|
|
12306
|
+
raw = serveStdinPayload ?? "";
|
|
12307
|
+
} else {
|
|
12308
|
+
const chunks = [];
|
|
12309
|
+
for await (const chunk of process.stdin) {
|
|
12310
|
+
chunks.push(chunk);
|
|
12311
|
+
}
|
|
12312
|
+
raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
11820
12313
|
}
|
|
11821
|
-
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
11822
12314
|
if (!raw) {
|
|
11823
12315
|
jsonError("No input received on stdin. Pipe JSON with token data.");
|
|
11824
12316
|
}
|
|
@@ -11831,8 +12323,12 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
|
|
|
11831
12323
|
if (!data?.slug || !data?.concept) {
|
|
11832
12324
|
jsonError("JSON must include 'slug' and 'concept' fields");
|
|
11833
12325
|
}
|
|
11834
|
-
db = await openDatabase();
|
|
11835
12326
|
const userId = await resolveUser(opts, db, { json: true });
|
|
12327
|
+
const possibleDuplicates = await findPossibleDuplicates(db, {
|
|
12328
|
+
concept: data?.concept,
|
|
12329
|
+
question: data?.question ?? null,
|
|
12330
|
+
domain: data?.domain
|
|
12331
|
+
});
|
|
11836
12332
|
const token = await createToken(db, {
|
|
11837
12333
|
slug: data?.slug,
|
|
11838
12334
|
concept: data?.concept,
|
|
@@ -11844,6 +12340,10 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
|
|
|
11844
12340
|
question: data?.question ?? null
|
|
11845
12341
|
});
|
|
11846
12342
|
const card = await ensureCard(db, token.id, userId);
|
|
12343
|
+
try {
|
|
12344
|
+
await ensureTokenEmbeddings(db, { limit: 8 });
|
|
12345
|
+
} catch {
|
|
12346
|
+
}
|
|
11847
12347
|
jsonOut2({
|
|
11848
12348
|
success: true,
|
|
11849
12349
|
token,
|
|
@@ -11854,15 +12354,79 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
|
|
|
11854
12354
|
state: card.state,
|
|
11855
12355
|
dueAt: card.due_at,
|
|
11856
12356
|
blocked: card.blocked
|
|
12357
|
+
},
|
|
12358
|
+
possible_duplicates: possibleDuplicates
|
|
12359
|
+
});
|
|
12360
|
+
});
|
|
12361
|
+
});
|
|
12362
|
+
bridgeCommand.command("relevant-tokens").description("Find tokens relevant to a given context").option("--user <id>", "User ID (default: whoami)").action(async (opts) => {
|
|
12363
|
+
await withDb2(async (db) => {
|
|
12364
|
+
let raw;
|
|
12365
|
+
if (isServeMode) {
|
|
12366
|
+
raw = serveStdinPayload ?? "";
|
|
12367
|
+
} else {
|
|
12368
|
+
const chunks = [];
|
|
12369
|
+
for await (const chunk of process.stdin) {
|
|
12370
|
+
chunks.push(chunk);
|
|
11857
12371
|
}
|
|
12372
|
+
raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
12373
|
+
}
|
|
12374
|
+
if (!raw) {
|
|
12375
|
+
jsonError("No input received on stdin. Pipe JSON with context.");
|
|
12376
|
+
}
|
|
12377
|
+
let data;
|
|
12378
|
+
try {
|
|
12379
|
+
data = JSON.parse(raw);
|
|
12380
|
+
} catch {
|
|
12381
|
+
jsonError("Invalid JSON input");
|
|
12382
|
+
}
|
|
12383
|
+
if (!data?.context || data.context.trim() === "") {
|
|
12384
|
+
jsonError("JSON must include a non-empty 'context' field");
|
|
12385
|
+
}
|
|
12386
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
12387
|
+
const truncatedContext = data.context.slice(0, 2e3);
|
|
12388
|
+
const q2 = await embedQuery(db, truncatedContext);
|
|
12389
|
+
try {
|
|
12390
|
+
await ensureTokenEmbeddings(db, {
|
|
12391
|
+
limit: 32,
|
|
12392
|
+
dims: q2?.vector.length
|
|
12393
|
+
});
|
|
12394
|
+
} catch {
|
|
12395
|
+
}
|
|
12396
|
+
let limit = data.limit ?? 10;
|
|
12397
|
+
if (typeof limit !== "number" || limit <= 0 || !Number.isInteger(limit)) {
|
|
12398
|
+
limit = 10;
|
|
12399
|
+
}
|
|
12400
|
+
if (limit > 100) {
|
|
12401
|
+
limit = 100;
|
|
12402
|
+
}
|
|
12403
|
+
const results = await searchTokensHybrid(db, truncatedContext, {
|
|
12404
|
+
queryEmbedding: q2?.vector,
|
|
12405
|
+
model: q2?.model,
|
|
12406
|
+
limit
|
|
11858
12407
|
});
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
12408
|
+
const tokens = [];
|
|
12409
|
+
for (const t2 of results) {
|
|
12410
|
+
const card = await getCard(db, t2.id, userId);
|
|
12411
|
+
tokens.push({
|
|
12412
|
+
slug: t2.slug,
|
|
12413
|
+
concept: t2.concept,
|
|
12414
|
+
domain: t2.domain,
|
|
12415
|
+
bloom_level: t2.bloom_level,
|
|
12416
|
+
score: t2.score,
|
|
12417
|
+
similarity: t2.similarity,
|
|
12418
|
+
card: card ? {
|
|
12419
|
+
state: card.state,
|
|
12420
|
+
due_at: card.due_at,
|
|
12421
|
+
blocked: card.blocked
|
|
12422
|
+
} : null
|
|
12423
|
+
});
|
|
11864
12424
|
}
|
|
11865
|
-
|
|
12425
|
+
jsonOut2({
|
|
12426
|
+
semantic: q2 !== null,
|
|
12427
|
+
tokens
|
|
12428
|
+
});
|
|
12429
|
+
});
|
|
11866
12430
|
});
|
|
11867
12431
|
bridgeCommand.command("discover-skills").description(
|
|
11868
12432
|
"Analyze monitor logs across sessions to discover recurring patterns"
|
|
@@ -12672,12 +13236,13 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
|
|
|
12672
13236
|
});
|
|
12673
13237
|
bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
|
|
12674
13238
|
await withDb2(async (db) => {
|
|
12675
|
-
const [recall, vision, text] = await Promise.all([
|
|
13239
|
+
const [recall, vision, text, embedding] = await Promise.all([
|
|
12676
13240
|
getProviderRoleStatus(db, "recall"),
|
|
12677
13241
|
getProviderRoleStatus(db, "vision"),
|
|
12678
|
-
getProviderRoleStatus(db, "text")
|
|
13242
|
+
getProviderRoleStatus(db, "text"),
|
|
13243
|
+
getProviderRoleStatus(db, "embedding")
|
|
12679
13244
|
]);
|
|
12680
|
-
jsonOut2({ roles: { recall, vision, text } });
|
|
13245
|
+
jsonOut2({ roles: { recall, vision, text, embedding } });
|
|
12681
13246
|
});
|
|
12682
13247
|
});
|
|
12683
13248
|
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 +13548,46 @@ bridgeCommand.command("get-settings").description("Get active ZAM settings (JSON
|
|
|
12983
13548
|
});
|
|
12984
13549
|
});
|
|
12985
13550
|
});
|
|
13551
|
+
async function readDatabaseUserSummaries(db) {
|
|
13552
|
+
return await db.prepare(
|
|
13553
|
+
`SELECT user_id AS id, COUNT(*) AS cardCount
|
|
13554
|
+
FROM cards
|
|
13555
|
+
GROUP BY user_id
|
|
13556
|
+
ORDER BY user_id`
|
|
13557
|
+
).all();
|
|
13558
|
+
}
|
|
13559
|
+
bridgeCommand.command("database-status").description("Show the active database target and learning profiles (JSON)").action(async () => {
|
|
13560
|
+
const target = getDatabaseTargetInfo();
|
|
13561
|
+
await withDb2(async (db) => {
|
|
13562
|
+
const userId = await getSetting(db, "user.id") ?? null;
|
|
13563
|
+
const users = await readDatabaseUserSummaries(db);
|
|
13564
|
+
jsonOut2({
|
|
13565
|
+
success: true,
|
|
13566
|
+
connected: true,
|
|
13567
|
+
target,
|
|
13568
|
+
userId,
|
|
13569
|
+
cardCount: users.find((user) => user.id === userId)?.cardCount ?? 0,
|
|
13570
|
+
users
|
|
13571
|
+
});
|
|
13572
|
+
});
|
|
13573
|
+
});
|
|
13574
|
+
bridgeCommand.command("database-select-user").description("Select an existing learning profile for this database (JSON)").requiredOption("--user <id>", "Existing user ID").action(async (opts) => {
|
|
13575
|
+
await withDb2(async (db) => {
|
|
13576
|
+
const userId = String(opts.user ?? "").trim();
|
|
13577
|
+
const users = await readDatabaseUserSummaries(db);
|
|
13578
|
+
const selected = users.find((user) => user.id === userId);
|
|
13579
|
+
if (!selected) {
|
|
13580
|
+
jsonError(`Learning profile not found: ${userId}`);
|
|
13581
|
+
return;
|
|
13582
|
+
}
|
|
13583
|
+
await setSetting(db, "user.id", userId);
|
|
13584
|
+
jsonOut2({
|
|
13585
|
+
success: true,
|
|
13586
|
+
userId,
|
|
13587
|
+
cardCount: selected.cardCount
|
|
13588
|
+
});
|
|
13589
|
+
});
|
|
13590
|
+
});
|
|
12986
13591
|
bridgeCommand.command("list-tokens").description(
|
|
12987
13592
|
"List tokens (optionally enriched with user card state for viz) (JSON)"
|
|
12988
13593
|
).option(
|
|
@@ -13707,6 +14312,13 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
13707
14312
|
requestId = req.id ?? null;
|
|
13708
14313
|
const cmd = req.cmd;
|
|
13709
14314
|
const args = req.args ?? [];
|
|
14315
|
+
if (typeof req.stdin === "string") {
|
|
14316
|
+
serveStdinPayload = req.stdin;
|
|
14317
|
+
} else if (typeof req.stdin === "object" && req.stdin !== null) {
|
|
14318
|
+
serveStdinPayload = JSON.stringify(req.stdin);
|
|
14319
|
+
} else {
|
|
14320
|
+
serveStdinPayload = void 0;
|
|
14321
|
+
}
|
|
13710
14322
|
if (!cmd) {
|
|
13711
14323
|
return JSON.stringify({
|
|
13712
14324
|
id: requestId,
|
|
@@ -13761,6 +14373,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
13761
14373
|
id: requestId,
|
|
13762
14374
|
error: `Invalid JSON request: ${err.message}`
|
|
13763
14375
|
});
|
|
14376
|
+
} finally {
|
|
14377
|
+
serveStdinPayload = void 0;
|
|
13764
14378
|
}
|
|
13765
14379
|
};
|
|
13766
14380
|
const readline = await import("readline");
|
|
@@ -16766,7 +17380,7 @@ import { Command as Command20 } from "commander";
|
|
|
16766
17380
|
var tokenCommand = new Command20("token").description(
|
|
16767
17381
|
"Manage knowledge tokens"
|
|
16768
17382
|
);
|
|
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) => {
|
|
17383
|
+
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
17384
|
await withDb(async (db) => {
|
|
16771
17385
|
let question = opts.question || null;
|
|
16772
17386
|
if (!question) {
|
|
@@ -16776,6 +17390,11 @@ tokenCommand.command("register").description("Register a new knowledge token").r
|
|
|
16776
17390
|
opts.domain
|
|
16777
17391
|
);
|
|
16778
17392
|
}
|
|
17393
|
+
const possibleDuplicates = await findPossibleDuplicates(db, {
|
|
17394
|
+
concept: opts.concept,
|
|
17395
|
+
question,
|
|
17396
|
+
domain: opts.domain
|
|
17397
|
+
});
|
|
16779
17398
|
const token = await createToken(db, {
|
|
16780
17399
|
slug: opts.slug,
|
|
16781
17400
|
concept: opts.concept,
|
|
@@ -16784,9 +17403,30 @@ tokenCommand.command("register").description("Register a new knowledge token").r
|
|
|
16784
17403
|
source_link: opts.sourceLink || null,
|
|
16785
17404
|
question
|
|
16786
17405
|
});
|
|
17406
|
+
let cardUserId = null;
|
|
17407
|
+
if (opts.card !== false) {
|
|
17408
|
+
cardUserId = opts.user ?? await getSetting(db, "user.id");
|
|
17409
|
+
if (cardUserId) {
|
|
17410
|
+
await ensureCard(db, token.id, cardUserId);
|
|
17411
|
+
}
|
|
17412
|
+
}
|
|
17413
|
+
try {
|
|
17414
|
+
await ensureTokenEmbeddings(db, { limit: 8 });
|
|
17415
|
+
} catch {
|
|
17416
|
+
}
|
|
16787
17417
|
if (opts.quiet) return;
|
|
16788
17418
|
if (opts.json) {
|
|
16789
|
-
console.log(
|
|
17419
|
+
console.log(
|
|
17420
|
+
JSON.stringify(
|
|
17421
|
+
{
|
|
17422
|
+
...token,
|
|
17423
|
+
card: cardUserId ? { userId: cardUserId } : null,
|
|
17424
|
+
possible_duplicates: possibleDuplicates
|
|
17425
|
+
},
|
|
17426
|
+
null,
|
|
17427
|
+
2
|
|
17428
|
+
)
|
|
17429
|
+
);
|
|
16790
17430
|
} else {
|
|
16791
17431
|
console.log(`Registered token: ${token.slug} (${token.id})`);
|
|
16792
17432
|
console.log(` Concept: ${token.concept}`);
|
|
@@ -16796,12 +17436,41 @@ tokenCommand.command("register").description("Register a new knowledge token").r
|
|
|
16796
17436
|
if (token.source_link) {
|
|
16797
17437
|
console.log(` Source: ${token.source_link}`);
|
|
16798
17438
|
}
|
|
17439
|
+
if (cardUserId) {
|
|
17440
|
+
console.log(` Card: created for ${cardUserId}`);
|
|
17441
|
+
} else if (opts.card === false) {
|
|
17442
|
+
console.log(` Card: skipped (--no-card)`);
|
|
17443
|
+
} else {
|
|
17444
|
+
console.log(` Card: skipped (no default user set)`);
|
|
17445
|
+
}
|
|
17446
|
+
if (possibleDuplicates.length > 0) {
|
|
17447
|
+
console.log(`
|
|
17448
|
+
WARNING: Possible duplicate tokens found:`);
|
|
17449
|
+
for (const dup of possibleDuplicates) {
|
|
17450
|
+
console.log(
|
|
17451
|
+
` - ${dup.slug} (similarity: ${dup.similarity.toFixed(2)})`
|
|
17452
|
+
);
|
|
17453
|
+
}
|
|
17454
|
+
}
|
|
16799
17455
|
}
|
|
16800
17456
|
});
|
|
16801
17457
|
});
|
|
16802
17458
|
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
17459
|
await withDb(async (db) => {
|
|
16804
|
-
const
|
|
17460
|
+
const q2 = await embedQuery(db, opts.query);
|
|
17461
|
+
const embRes = await ensureTokenEmbeddings(db, {
|
|
17462
|
+
limit: 32,
|
|
17463
|
+
dims: q2?.vector.length
|
|
17464
|
+
});
|
|
17465
|
+
if (embRes.status === "unavailable" && !opts.json && !opts.quiet) {
|
|
17466
|
+
console.error(
|
|
17467
|
+
`Note: semantic search unavailable (${embRes.reason}) \u2014 lexical matches only.`
|
|
17468
|
+
);
|
|
17469
|
+
}
|
|
17470
|
+
const results = await searchTokensHybrid(db, opts.query, {
|
|
17471
|
+
queryEmbedding: q2?.vector,
|
|
17472
|
+
model: q2?.model
|
|
17473
|
+
});
|
|
16805
17474
|
if (opts.quiet) return;
|
|
16806
17475
|
if (opts.json) {
|
|
16807
17476
|
console.log(JSON.stringify(results, null, 2));
|
|
@@ -16814,12 +17483,14 @@ tokenCommand.command("find").description("Fuzzy search for tokens").requiredOpti
|
|
|
16814
17483
|
console.log(`Found ${results.length} token(s):
|
|
16815
17484
|
`);
|
|
16816
17485
|
console.log(
|
|
16817
|
-
"Score Slug Concept Domain Bloom"
|
|
17486
|
+
"Score Sim Slug Concept Domain Bloom"
|
|
16818
17487
|
);
|
|
16819
|
-
console.log("\u2500".repeat(
|
|
17488
|
+
console.log("\u2500".repeat(95));
|
|
16820
17489
|
for (const t2 of results) {
|
|
17490
|
+
const scoreStr = t2.score.toFixed(3).padEnd(6);
|
|
17491
|
+
const simStr = (t2.similarity?.toFixed(2) ?? "-").padEnd(4);
|
|
16821
17492
|
console.log(
|
|
16822
|
-
`${
|
|
17493
|
+
`${scoreStr} ${simStr} ${t2.slug.padEnd(21)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
|
|
16823
17494
|
);
|
|
16824
17495
|
}
|
|
16825
17496
|
});
|
|
@@ -17047,6 +17718,56 @@ tokenCommand.command("status").description("Show full status of a token for a us
|
|
|
17047
17718
|
}
|
|
17048
17719
|
});
|
|
17049
17720
|
});
|
|
17721
|
+
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) => {
|
|
17722
|
+
await withDb(async (db) => {
|
|
17723
|
+
const probe = await embedQuery(db, "ZAM embedding dimension probe");
|
|
17724
|
+
const model = probe?.model ?? null;
|
|
17725
|
+
const dims = probe?.vector.length;
|
|
17726
|
+
const before = model ? await getEmbeddingCoverage(db, model, { dims }) : { tokens: 0, embedded: 0, missing: 0, stale: 0 };
|
|
17727
|
+
let embedded = 0;
|
|
17728
|
+
let reason;
|
|
17729
|
+
let force = Boolean(opts.all);
|
|
17730
|
+
while (true) {
|
|
17731
|
+
const result = await ensureTokenEmbeddings(
|
|
17732
|
+
db,
|
|
17733
|
+
force ? { force: true, limit: Number.MAX_SAFE_INTEGER, dims } : { dims }
|
|
17734
|
+
);
|
|
17735
|
+
force = false;
|
|
17736
|
+
embedded += result.embedded;
|
|
17737
|
+
if (result.status === "unavailable") {
|
|
17738
|
+
reason = result.reason;
|
|
17739
|
+
break;
|
|
17740
|
+
}
|
|
17741
|
+
if (result.remaining === 0 || result.embedded === 0) break;
|
|
17742
|
+
}
|
|
17743
|
+
if (reason !== void 0) {
|
|
17744
|
+
if (opts.quiet) {
|
|
17745
|
+
process.exit(1);
|
|
17746
|
+
}
|
|
17747
|
+
if (opts.json) {
|
|
17748
|
+
jsonOut({ error: reason, embedded });
|
|
17749
|
+
} else {
|
|
17750
|
+
console.error(`Error: ${reason}`);
|
|
17751
|
+
if (embedded > 0) {
|
|
17752
|
+
console.error(
|
|
17753
|
+
`(${embedded} token(s) were embedded before the failure)`
|
|
17754
|
+
);
|
|
17755
|
+
}
|
|
17756
|
+
}
|
|
17757
|
+
process.exit(1);
|
|
17758
|
+
}
|
|
17759
|
+
const after = await getEmbeddingCoverage(db, model, { dims });
|
|
17760
|
+
if (opts.quiet) return;
|
|
17761
|
+
const staleBefore = before.missing + before.stale;
|
|
17762
|
+
if (opts.json) {
|
|
17763
|
+
jsonOut({ embedded, model, before, after });
|
|
17764
|
+
return;
|
|
17765
|
+
}
|
|
17766
|
+
console.log(
|
|
17767
|
+
`Embedded ${embedded} tokens (${after.tokens} total, ${staleBefore} stale before) with ${model}`
|
|
17768
|
+
);
|
|
17769
|
+
});
|
|
17770
|
+
});
|
|
17050
17771
|
|
|
17051
17772
|
// src/cli/commands/ui.ts
|
|
17052
17773
|
import { spawn as spawn3, spawnSync } from "child_process";
|