zam-core 0.10.3 → 0.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/app.js +2033 -276
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/commands/mcp.js +2623 -317
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/copilot-extension/host.bundle.js +1 -1
- package/dist/copilot-extension/manifest.json +1 -1
- package/dist/copilot-extension/mcp-client.bundle.mjs +1 -1
- package/dist/index.d.ts +110 -1
- package/dist/index.js +272 -66
- package/dist/index.js.map +1 -1
- package/dist/ui/recall-panel.html +1 -1
- package/dist/ui/studio-panel.html +1 -1
- package/dist/vscode-extension/{ZAM_Companion_0.10.3.vsix → ZAM_Companion_0.10.5.vsix} +0 -0
- package/dist/vscode-extension/manifest.json +2 -2
- package/package.json +1 -1
package/dist/cli/app.js
CHANGED
|
@@ -2176,6 +2176,56 @@ async function listPersonalCards(db, userId, options) {
|
|
|
2176
2176
|
}
|
|
2177
2177
|
return rows;
|
|
2178
2178
|
}
|
|
2179
|
+
function curriculumTopicScopeClause() {
|
|
2180
|
+
return `(t.topic_id = ? OR t.topic_id LIKE ? || '@%')`;
|
|
2181
|
+
}
|
|
2182
|
+
function tokenMatchesCurriculumTopicScope(token, provider, topicId) {
|
|
2183
|
+
if (token.provider !== provider || !token.topic_id) return false;
|
|
2184
|
+
return token.topic_id === topicId || token.topic_id.startsWith(`${topicId}@`);
|
|
2185
|
+
}
|
|
2186
|
+
async function deleteCurriculumCardForUser(db, userId, slug, provider, topicId) {
|
|
2187
|
+
const token = await getTokenBySlug(db, slug);
|
|
2188
|
+
if (!token) return false;
|
|
2189
|
+
if (!tokenMatchesCurriculumTopicScope(token, provider, topicId)) {
|
|
2190
|
+
throw new Error(
|
|
2191
|
+
`Token "${slug}" is not removable for curriculum topic "${topicId}"`
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
const card = await getCard(db, token.id, userId);
|
|
2195
|
+
if (!card) return false;
|
|
2196
|
+
await deleteCardForUser(db, token.id, userId);
|
|
2197
|
+
return true;
|
|
2198
|
+
}
|
|
2199
|
+
async function countUserCardsForCurriculumTopic(db, userId, provider, topicId) {
|
|
2200
|
+
const row = await db.prepare(
|
|
2201
|
+
`SELECT COUNT(*) AS count
|
|
2202
|
+
FROM cards c
|
|
2203
|
+
INNER JOIN tokens t ON t.id = c.token_id
|
|
2204
|
+
WHERE c.user_id = ?
|
|
2205
|
+
AND t.provider = ?
|
|
2206
|
+
AND ${curriculumTopicScopeClause()}`
|
|
2207
|
+
).get(userId, provider, topicId, topicId);
|
|
2208
|
+
return row?.count ?? 0;
|
|
2209
|
+
}
|
|
2210
|
+
async function listUserCardsForCurriculumTopic(db, userId, provider, topicId) {
|
|
2211
|
+
const rows = await db.prepare(
|
|
2212
|
+
`SELECT
|
|
2213
|
+
t.slug,
|
|
2214
|
+
t.question,
|
|
2215
|
+
t.concept,
|
|
2216
|
+
t.domain,
|
|
2217
|
+
t.bloom_level AS bloomLevel,
|
|
2218
|
+
t.symbiosis_mode AS symbiosisMode,
|
|
2219
|
+
t.topic_id AS topicId
|
|
2220
|
+
FROM cards c
|
|
2221
|
+
INNER JOIN tokens t ON t.id = c.token_id
|
|
2222
|
+
WHERE c.user_id = ?
|
|
2223
|
+
AND t.provider = ?
|
|
2224
|
+
AND ${curriculumTopicScopeClause()}
|
|
2225
|
+
ORDER BY t.slug`
|
|
2226
|
+
).all(userId, provider, topicId, topicId);
|
|
2227
|
+
return rows;
|
|
2228
|
+
}
|
|
2179
2229
|
async function importCurriculumCards(db, userId, cards) {
|
|
2180
2230
|
let createdCount = 0;
|
|
2181
2231
|
let ensuredCount = 0;
|
|
@@ -2459,81 +2509,86 @@ async function confirmFoundations(db, userId, originalSlug, proposals) {
|
|
|
2459
2509
|
});
|
|
2460
2510
|
return { createdCount, linkedCount };
|
|
2461
2511
|
}
|
|
2462
|
-
async function
|
|
2512
|
+
async function applySourceProposals(db, userId, sourceId, proposals) {
|
|
2463
2513
|
let createdCount = 0;
|
|
2464
2514
|
let linkedCount = 0;
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2515
|
+
for (const card of proposals) {
|
|
2516
|
+
const cardSourceId = card.source_id || sourceId;
|
|
2517
|
+
const source = await db.prepare("SELECT id FROM sources WHERE id = ?").get(cardSourceId);
|
|
2518
|
+
if (!source) {
|
|
2519
|
+
throw new Error(`Source not found: ${cardSourceId}`);
|
|
2520
|
+
}
|
|
2521
|
+
const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
|
|
2522
|
+
const cleanDomain = slugify(card.domain || "");
|
|
2523
|
+
const cleanBase = slugify(baseText);
|
|
2524
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
2525
|
+
if (baseSlug.length > 60) {
|
|
2526
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
2527
|
+
}
|
|
2528
|
+
if (!baseSlug) {
|
|
2529
|
+
baseSlug = "token";
|
|
2530
|
+
}
|
|
2531
|
+
let token = await getTokenBySlug(db, baseSlug);
|
|
2532
|
+
if (!token) {
|
|
2533
|
+
const finalSlug = await generateTokenSlug(
|
|
2534
|
+
db,
|
|
2535
|
+
card.domain,
|
|
2536
|
+
card.concept,
|
|
2537
|
+
card.question
|
|
2538
|
+
);
|
|
2539
|
+
const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
|
|
2540
|
+
let symbiosisMode = null;
|
|
2541
|
+
if (card.symbiosis_mode && card.symbiosis_mode !== "none") {
|
|
2542
|
+
symbiosisMode = card.symbiosis_mode;
|
|
2481
2543
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
bloom_level: bloom,
|
|
2501
|
-
context: card.excerpt || "",
|
|
2502
|
-
symbiosis_mode: symbiosisMode,
|
|
2503
|
-
question: card.question || null,
|
|
2504
|
-
provider: card.provider || null,
|
|
2544
|
+
token = await createToken(db, {
|
|
2545
|
+
slug: finalSlug,
|
|
2546
|
+
title: card.title,
|
|
2547
|
+
concept: card.concept,
|
|
2548
|
+
domain: card.domain,
|
|
2549
|
+
bloom_level: bloom,
|
|
2550
|
+
context: card.excerpt || "",
|
|
2551
|
+
symbiosis_mode: symbiosisMode,
|
|
2552
|
+
question: card.question || null,
|
|
2553
|
+
provider: card.provider || null,
|
|
2554
|
+
topic_id: card.topic_id || null
|
|
2555
|
+
});
|
|
2556
|
+
createdCount++;
|
|
2557
|
+
} else {
|
|
2558
|
+
linkedCount++;
|
|
2559
|
+
if (!token.provider && card.provider) {
|
|
2560
|
+
await updateToken(db, token.slug, {
|
|
2561
|
+
provider: card.provider,
|
|
2505
2562
|
topic_id: card.topic_id || null
|
|
2506
2563
|
});
|
|
2507
|
-
createdCount++;
|
|
2508
|
-
} else {
|
|
2509
|
-
linkedCount++;
|
|
2510
|
-
if (!token.provider && card.provider) {
|
|
2511
|
-
await updateToken(tx, token.slug, {
|
|
2512
|
-
provider: card.provider,
|
|
2513
|
-
topic_id: card.topic_id || null
|
|
2514
|
-
});
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
const existingCard = await getCard(tx, token.id, userId);
|
|
2518
|
-
if (!existingCard) {
|
|
2519
|
-
await ensureCard(tx, token.id, userId);
|
|
2520
2564
|
}
|
|
2521
|
-
await tx.prepare(
|
|
2522
|
-
`INSERT INTO token_sources (token_id, source_id, excerpt, page_number)
|
|
2523
|
-
VALUES (?, ?, ?, ?)
|
|
2524
|
-
ON CONFLICT(token_id, source_id) DO UPDATE SET
|
|
2525
|
-
excerpt = excluded.excerpt,
|
|
2526
|
-
page_number = excluded.page_number`
|
|
2527
|
-
).run(
|
|
2528
|
-
token.id,
|
|
2529
|
-
cardSourceId,
|
|
2530
|
-
card.excerpt || "",
|
|
2531
|
-
card.page_number || null
|
|
2532
|
-
);
|
|
2533
2565
|
}
|
|
2534
|
-
|
|
2566
|
+
const existingCard = await getCard(db, token.id, userId);
|
|
2567
|
+
if (!existingCard) {
|
|
2568
|
+
await ensureCard(db, token.id, userId);
|
|
2569
|
+
}
|
|
2570
|
+
await db.prepare(
|
|
2571
|
+
`INSERT INTO token_sources (token_id, source_id, excerpt, page_number)
|
|
2572
|
+
VALUES (?, ?, ?, ?)
|
|
2573
|
+
ON CONFLICT(token_id, source_id) DO UPDATE SET
|
|
2574
|
+
excerpt = excluded.excerpt,
|
|
2575
|
+
page_number = excluded.page_number`
|
|
2576
|
+
).run(
|
|
2577
|
+
token.id,
|
|
2578
|
+
cardSourceId,
|
|
2579
|
+
card.excerpt || "",
|
|
2580
|
+
card.page_number || null
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2535
2583
|
return { createdCount, linkedCount };
|
|
2536
2584
|
}
|
|
2585
|
+
async function confirmSourceImport(db, userId, sourceId, proposals) {
|
|
2586
|
+
let result = { createdCount: 0, linkedCount: 0 };
|
|
2587
|
+
await db.transaction(async (tx) => {
|
|
2588
|
+
result = await applySourceProposals(tx, userId, sourceId, proposals);
|
|
2589
|
+
});
|
|
2590
|
+
return result;
|
|
2591
|
+
}
|
|
2537
2592
|
async function assertPrerequisiteDoesNotCreateCycle(db, tokenId, prerequisiteId) {
|
|
2538
2593
|
const cycleCheck = await db.prepare(
|
|
2539
2594
|
`WITH RECURSIVE dependents(token_id) AS (
|
|
@@ -5903,6 +5958,17 @@ var init_i18n = __esm({
|
|
|
5903
5958
|
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
5904
5959
|
import { homedir as homedir6 } from "os";
|
|
5905
5960
|
import { dirname as dirname4, join as join9 } from "path";
|
|
5961
|
+
import { ulid as ulid9 } from "ulid";
|
|
5962
|
+
function emptyCapabilityFlags() {
|
|
5963
|
+
return {
|
|
5964
|
+
text: false,
|
|
5965
|
+
embedding: false,
|
|
5966
|
+
image: false,
|
|
5967
|
+
video: false,
|
|
5968
|
+
stt: false,
|
|
5969
|
+
tts: false
|
|
5970
|
+
};
|
|
5971
|
+
}
|
|
5906
5972
|
function defaultConfigPath() {
|
|
5907
5973
|
return process.env.ZAM_CONFIG_PATH || join9(homedir6(), ".zam", "config.json");
|
|
5908
5974
|
}
|
|
@@ -5946,6 +6012,116 @@ function saveMachineAiConfig(ai, path = defaultConfigPath()) {
|
|
|
5946
6012
|
config.ai = ai;
|
|
5947
6013
|
saveInstallConfig(config, path);
|
|
5948
6014
|
}
|
|
6015
|
+
function ensureMachineProviderRolesSanitized(path = defaultConfigPath()) {
|
|
6016
|
+
if (sanitizedMachineRolePaths.has(path)) return;
|
|
6017
|
+
sanitizedMachineRolePaths.add(path);
|
|
6018
|
+
const ai = getMachineAiConfig(path);
|
|
6019
|
+
if (!ai.roles?.text) return;
|
|
6020
|
+
const roles = { ...ai.roles };
|
|
6021
|
+
delete roles.text;
|
|
6022
|
+
saveMachineAiConfig({ ...ai, roles }, path);
|
|
6023
|
+
}
|
|
6024
|
+
function getMachineAiModels(path = defaultConfigPath()) {
|
|
6025
|
+
return getMachineAiConfig(path).models ?? [];
|
|
6026
|
+
}
|
|
6027
|
+
function saveMachineAiModels(models, path = defaultConfigPath()) {
|
|
6028
|
+
const ai = getMachineAiConfig(path);
|
|
6029
|
+
saveMachineAiConfig({ ...ai, models }, path);
|
|
6030
|
+
}
|
|
6031
|
+
function isAnthropicUrl(url) {
|
|
6032
|
+
try {
|
|
6033
|
+
return new URL(url).hostname.toLowerCase().endsWith("anthropic.com");
|
|
6034
|
+
} catch {
|
|
6035
|
+
return false;
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
6038
|
+
function isLocalUrl(url) {
|
|
6039
|
+
try {
|
|
6040
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
6041
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local");
|
|
6042
|
+
} catch {
|
|
6043
|
+
return false;
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
function migrateMachineRolesToModels(ai) {
|
|
6047
|
+
const providers = ai.providers ?? {};
|
|
6048
|
+
const providerNames = Object.keys(providers);
|
|
6049
|
+
if (providerNames.length === 0) return null;
|
|
6050
|
+
const roles = ai.roles ?? {};
|
|
6051
|
+
const capsByName = /* @__PURE__ */ new Map();
|
|
6052
|
+
const capsFor = (name) => {
|
|
6053
|
+
let flags = capsByName.get(name);
|
|
6054
|
+
if (!flags) {
|
|
6055
|
+
flags = emptyCapabilityFlags();
|
|
6056
|
+
capsByName.set(name, flags);
|
|
6057
|
+
}
|
|
6058
|
+
return flags;
|
|
6059
|
+
};
|
|
6060
|
+
const priorityRoles = [
|
|
6061
|
+
"recall",
|
|
6062
|
+
"text",
|
|
6063
|
+
"vision",
|
|
6064
|
+
"embedding"
|
|
6065
|
+
];
|
|
6066
|
+
for (const role of priorityRoles) {
|
|
6067
|
+
const binding = roles[role];
|
|
6068
|
+
if (!binding) continue;
|
|
6069
|
+
const capability = ROLE_TO_CAPABILITY[role];
|
|
6070
|
+
for (const ref of [binding.primary, binding.fallback]) {
|
|
6071
|
+
if (ref && providers[ref]) capsFor(ref)[capability] = true;
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
const ordered = [];
|
|
6075
|
+
const pushName = (name) => {
|
|
6076
|
+
if (name && providers[name] && !ordered.includes(name)) ordered.push(name);
|
|
6077
|
+
};
|
|
6078
|
+
for (const role of priorityRoles) {
|
|
6079
|
+
pushName(roles[role]?.primary);
|
|
6080
|
+
pushName(roles[role]?.fallback);
|
|
6081
|
+
}
|
|
6082
|
+
for (const name of providerNames) pushName(name);
|
|
6083
|
+
return ordered.map((name, index) => {
|
|
6084
|
+
const rec = providers[name];
|
|
6085
|
+
const url = rec.url ?? "";
|
|
6086
|
+
const capabilities = capsByName.get(name) ?? emptyCapabilityFlags();
|
|
6087
|
+
const entry = {
|
|
6088
|
+
id: ulid9(),
|
|
6089
|
+
label: rec.label ?? name,
|
|
6090
|
+
url,
|
|
6091
|
+
model: rec.model ?? "",
|
|
6092
|
+
local: rec.local ?? isLocalUrl(url),
|
|
6093
|
+
apiFlavor: rec.apiFlavor ?? (isAnthropicUrl(url) ? "anthropic-messages" : "chat-completions"),
|
|
6094
|
+
order: index,
|
|
6095
|
+
capabilities,
|
|
6096
|
+
detectedCapabilities: { ...capabilities }
|
|
6097
|
+
};
|
|
6098
|
+
if (rec.runner) entry.runner = rec.runner;
|
|
6099
|
+
if (rec.apiKeyRef) entry.apiKeyRef = rec.apiKeyRef;
|
|
6100
|
+
return entry;
|
|
6101
|
+
});
|
|
6102
|
+
}
|
|
6103
|
+
function ensureMachineAiModelsMigrated(path = defaultConfigPath()) {
|
|
6104
|
+
const ai = getMachineAiConfig(path);
|
|
6105
|
+
if (ai.models && ai.models.length > 0) return ai.models;
|
|
6106
|
+
if (migratedModelConfigPaths.has(path)) return ai.models ?? [];
|
|
6107
|
+
migratedModelConfigPaths.add(path);
|
|
6108
|
+
const models = migrateMachineRolesToModels(ai);
|
|
6109
|
+
if (!models) return [];
|
|
6110
|
+
saveMachineAiConfig({ ...ai, models }, path);
|
|
6111
|
+
return models;
|
|
6112
|
+
}
|
|
6113
|
+
function getAgentConnectAutoDone(path = defaultConfigPath()) {
|
|
6114
|
+
return loadInstallConfig(path).agent?.connectAutoDone === true;
|
|
6115
|
+
}
|
|
6116
|
+
function setAgentConnectAutoDone(done, path = defaultConfigPath()) {
|
|
6117
|
+
const config = loadInstallConfig(path);
|
|
6118
|
+
if (done) {
|
|
6119
|
+
config.agent = { ...config.agent ?? {}, connectAutoDone: true };
|
|
6120
|
+
} else if (config.agent) {
|
|
6121
|
+
delete config.agent.connectAutoDone;
|
|
6122
|
+
}
|
|
6123
|
+
saveInstallConfig(config, path);
|
|
6124
|
+
}
|
|
5949
6125
|
function getConfiguredWorkspaces(path = defaultConfigPath()) {
|
|
5950
6126
|
return loadInstallConfig(path).workspaces ?? [];
|
|
5951
6127
|
}
|
|
@@ -6030,9 +6206,26 @@ function setActiveWorkspaceContext(contextName, path = defaultConfigPath()) {
|
|
|
6030
6206
|
}
|
|
6031
6207
|
return false;
|
|
6032
6208
|
}
|
|
6209
|
+
var ALL_CAPABILITIES, sanitizedMachineRolePaths, ROLE_TO_CAPABILITY, migratedModelConfigPaths;
|
|
6033
6210
|
var init_install_config = __esm({
|
|
6034
6211
|
"src/kernel/system/install-config.ts"() {
|
|
6035
6212
|
"use strict";
|
|
6213
|
+
ALL_CAPABILITIES = [
|
|
6214
|
+
"text",
|
|
6215
|
+
"embedding",
|
|
6216
|
+
"image",
|
|
6217
|
+
"video",
|
|
6218
|
+
"stt",
|
|
6219
|
+
"tts"
|
|
6220
|
+
];
|
|
6221
|
+
sanitizedMachineRolePaths = /* @__PURE__ */ new Set();
|
|
6222
|
+
ROLE_TO_CAPABILITY = {
|
|
6223
|
+
recall: "text",
|
|
6224
|
+
text: "text",
|
|
6225
|
+
vision: "image",
|
|
6226
|
+
embedding: "embedding"
|
|
6227
|
+
};
|
|
6228
|
+
migratedModelConfigPaths = /* @__PURE__ */ new Set();
|
|
6036
6229
|
}
|
|
6037
6230
|
});
|
|
6038
6231
|
|
|
@@ -6548,6 +6741,7 @@ var init_update_check = __esm({
|
|
|
6548
6741
|
// src/kernel/index.ts
|
|
6549
6742
|
var kernel_exports = {};
|
|
6550
6743
|
__export(kernel_exports, {
|
|
6744
|
+
ALL_CAPABILITIES: () => ALL_CAPABILITIES,
|
|
6551
6745
|
BUILT_IN_SENSITIVE_MATCHERS: () => BUILT_IN_SENSITIVE_MATCHERS,
|
|
6552
6746
|
DEFAULT_OBSERVER_POLICY: () => DEFAULT_OBSERVER_POLICY,
|
|
6553
6747
|
DEFAULT_REVIEW_CONTEXT_MAX_CHARS: () => DEFAULT_REVIEW_CONTEXT_MAX_CHARS,
|
|
@@ -6563,6 +6757,7 @@ __export(kernel_exports, {
|
|
|
6563
6757
|
analyzeObservation: () => analyzeObservation,
|
|
6564
6758
|
appendUiObservationReport: () => appendUiObservationReport,
|
|
6565
6759
|
applySessionSynthesis: () => applySessionSynthesis,
|
|
6760
|
+
applySourceProposals: () => applySourceProposals,
|
|
6566
6761
|
assignTokenToContext: () => assignTokenToContext,
|
|
6567
6762
|
buildAncestorMap: () => buildAncestorMap,
|
|
6568
6763
|
buildReviewQueue: () => buildReviewQueue,
|
|
@@ -6578,6 +6773,7 @@ __export(kernel_exports, {
|
|
|
6578
6773
|
confirmFoundations: () => confirmFoundations,
|
|
6579
6774
|
confirmSourceImport: () => confirmSourceImport,
|
|
6580
6775
|
cosineSimilarity: () => cosineSimilarity,
|
|
6776
|
+
countUserCardsForCurriculumTopic: () => countUserCardsForCurriculumTopic,
|
|
6581
6777
|
createAgentSkill: () => createAgentSkill,
|
|
6582
6778
|
createFSRS: () => createFSRS,
|
|
6583
6779
|
createGoal: () => createGoal,
|
|
@@ -6588,6 +6784,7 @@ __export(kernel_exports, {
|
|
|
6588
6784
|
decideUpdate: () => decideUpdate,
|
|
6589
6785
|
decodeEmbedding: () => decodeEmbedding,
|
|
6590
6786
|
deleteCardForUser: () => deleteCardForUser,
|
|
6787
|
+
deleteCurriculumCardForUser: () => deleteCurriculumCardForUser,
|
|
6591
6788
|
deleteKnowledgeContext: () => deleteKnowledgeContext,
|
|
6592
6789
|
deleteSetting: () => deleteSetting,
|
|
6593
6790
|
deleteToken: () => deleteToken,
|
|
@@ -6597,9 +6794,12 @@ __export(kernel_exports, {
|
|
|
6597
6794
|
discoverSkills: () => discoverSkills,
|
|
6598
6795
|
distributeGlobalSkills: () => distributeGlobalSkills,
|
|
6599
6796
|
embeddingContentForToken: () => embeddingContentForToken,
|
|
6797
|
+
emptyCapabilityFlags: () => emptyCapabilityFlags,
|
|
6600
6798
|
encodeEmbedding: () => encodeEmbedding,
|
|
6601
6799
|
endSession: () => endSession,
|
|
6602
6800
|
ensureCard: () => ensureCard,
|
|
6801
|
+
ensureMachineAiModelsMigrated: () => ensureMachineAiModelsMigrated,
|
|
6802
|
+
ensureMachineProviderRolesSanitized: () => ensureMachineProviderRolesSanitized,
|
|
6603
6803
|
ensureMonitorDir: () => ensureMonitorDir,
|
|
6604
6804
|
ensureUiObserverDir: () => ensureUiObserverDir,
|
|
6605
6805
|
evaluateRating: () => evaluateRating,
|
|
@@ -6622,6 +6822,7 @@ __export(kernel_exports, {
|
|
|
6622
6822
|
getActiveWorkspace: () => getActiveWorkspace,
|
|
6623
6823
|
getActiveWorkspaceContext: () => getActiveWorkspaceContext,
|
|
6624
6824
|
getActiveWorkspaceId: () => getActiveWorkspaceId,
|
|
6825
|
+
getAgentConnectAutoDone: () => getAgentConnectAutoDone,
|
|
6625
6826
|
getAgentSkill: () => getAgentSkill,
|
|
6626
6827
|
getAllSettings: () => getAllSettings,
|
|
6627
6828
|
getAllSettingsDetailed: () => getAllSettingsDetailed,
|
|
@@ -6644,6 +6845,7 @@ __export(kernel_exports, {
|
|
|
6644
6845
|
getKnowledgeContextById: () => getKnowledgeContextById,
|
|
6645
6846
|
getKnowledgeContextByName: () => getKnowledgeContextByName,
|
|
6646
6847
|
getMachineAiConfig: () => getMachineAiConfig,
|
|
6848
|
+
getMachineAiModels: () => getMachineAiModels,
|
|
6647
6849
|
getMonitorDir: () => getMonitorDir,
|
|
6648
6850
|
getMonitorLogStats: () => getMonitorLogStats,
|
|
6649
6851
|
getMonitorPath: () => getMonitorPath,
|
|
@@ -6687,6 +6889,7 @@ __export(kernel_exports, {
|
|
|
6687
6889
|
listProviderApiKeyRefs: () => listProviderApiKeyRefs,
|
|
6688
6890
|
listTokens: () => listTokens,
|
|
6689
6891
|
listTokensNeedingEmbedding: () => listTokensNeedingEmbedding,
|
|
6892
|
+
listUserCardsForCurriculumTopic: () => listUserCardsForCurriculumTopic,
|
|
6690
6893
|
loadADOConfig: () => loadADOConfig,
|
|
6691
6894
|
loadCredentials: () => loadCredentials,
|
|
6692
6895
|
loadInstallConfig: () => loadInstallConfig,
|
|
@@ -6695,6 +6898,7 @@ __export(kernel_exports, {
|
|
|
6695
6898
|
matchBuiltInSensitive: () => matchBuiltInSensitive,
|
|
6696
6899
|
matchDenylist: () => matchDenylist,
|
|
6697
6900
|
matchesFilePath: () => matchesFilePath,
|
|
6901
|
+
migrateMachineRolesToModels: () => migrateMachineRolesToModels,
|
|
6698
6902
|
monitorLogExists: () => monitorLogExists,
|
|
6699
6903
|
normalizeLocale: () => normalizeLocale,
|
|
6700
6904
|
normalizePath: () => normalizePath,
|
|
@@ -6726,11 +6930,13 @@ __export(kernel_exports, {
|
|
|
6726
6930
|
saveCredentials: () => saveCredentials,
|
|
6727
6931
|
saveInstallConfig: () => saveInstallConfig,
|
|
6728
6932
|
saveMachineAiConfig: () => saveMachineAiConfig,
|
|
6933
|
+
saveMachineAiModels: () => saveMachineAiModels,
|
|
6729
6934
|
searchTokensHybrid: () => searchTokensHybrid,
|
|
6730
6935
|
serializeGoal: () => serializeGoal,
|
|
6731
6936
|
setADOCredentials: () => setADOCredentials,
|
|
6732
6937
|
setActiveWorkspaceContext: () => setActiveWorkspaceContext,
|
|
6733
6938
|
setActiveWorkspaceId: () => setActiveWorkspaceId,
|
|
6939
|
+
setAgentConnectAutoDone: () => setAgentConnectAutoDone,
|
|
6734
6940
|
setInstallChannel: () => setInstallChannel,
|
|
6735
6941
|
setInstallMode: () => setInstallMode,
|
|
6736
6942
|
setProviderApiKey: () => setProviderApiKey,
|
|
@@ -6742,6 +6948,7 @@ __export(kernel_exports, {
|
|
|
6742
6948
|
syncObserverSidecarPolicy: () => syncObserverSidecarPolicy,
|
|
6743
6949
|
t: () => t,
|
|
6744
6950
|
toSidecarPrivacyPolicy: () => toSidecarPrivacyPolicy,
|
|
6951
|
+
tokenMatchesCurriculumTopicScope: () => tokenMatchesCurriculumTopicScope,
|
|
6745
6952
|
uiObservationLogExists: () => uiObservationLogExists,
|
|
6746
6953
|
uiObservationTimeSpan: () => uiObservationTimeSpan,
|
|
6747
6954
|
unassignTokenFromContext: () => unassignTokenFromContext,
|
|
@@ -6815,11 +7022,16 @@ import { Command as Command27 } from "commander";
|
|
|
6815
7022
|
|
|
6816
7023
|
// src/cli/commands/agent.ts
|
|
6817
7024
|
init_kernel();
|
|
6818
|
-
import { existsSync as existsSync15
|
|
6819
|
-
import {
|
|
6820
|
-
import { dirname as dirname7, join as join15 } from "path";
|
|
7025
|
+
import { existsSync as existsSync15 } from "fs";
|
|
7026
|
+
import { join as join15 } from "path";
|
|
6821
7027
|
import { Command } from "commander";
|
|
6822
7028
|
|
|
7029
|
+
// src/cli/agent-connect.ts
|
|
7030
|
+
init_kernel();
|
|
7031
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
7032
|
+
import { homedir as homedir11 } from "os";
|
|
7033
|
+
import { dirname as dirname7 } from "path";
|
|
7034
|
+
|
|
6823
7035
|
// src/cli/agent-harness.ts
|
|
6824
7036
|
import { spawn } from "child_process";
|
|
6825
7037
|
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
@@ -7673,16 +7885,7 @@ function installVscodeExtension(options) {
|
|
|
7673
7885
|
};
|
|
7674
7886
|
}
|
|
7675
7887
|
|
|
7676
|
-
// src/cli/
|
|
7677
|
-
var C = {
|
|
7678
|
-
reset: "\x1B[0m",
|
|
7679
|
-
bold: "\x1B[1m",
|
|
7680
|
-
cyan: "\x1B[36m",
|
|
7681
|
-
dim: "\x1B[2m",
|
|
7682
|
-
green: "\x1B[32m",
|
|
7683
|
-
yellow: "\x1B[33m"
|
|
7684
|
-
};
|
|
7685
|
-
var SUPPORTED_AGENTS = ["opencode"];
|
|
7888
|
+
// src/cli/agent-connect.ts
|
|
7686
7889
|
var CONNECT_HARNESSES = [
|
|
7687
7890
|
"claude-code",
|
|
7688
7891
|
"claude-desktop",
|
|
@@ -7693,9 +7896,189 @@ var CONNECT_HARNESSES = [
|
|
|
7693
7896
|
"goose",
|
|
7694
7897
|
"copilot"
|
|
7695
7898
|
];
|
|
7899
|
+
var USER_SCOPED_CONNECT_HARNESSES = [
|
|
7900
|
+
"claude-desktop",
|
|
7901
|
+
"antigravity",
|
|
7902
|
+
"codex",
|
|
7903
|
+
"vscode",
|
|
7904
|
+
"opencode",
|
|
7905
|
+
"goose",
|
|
7906
|
+
"copilot"
|
|
7907
|
+
];
|
|
7908
|
+
var CONNECT_HARNESS_LABELS = {
|
|
7909
|
+
"claude-code": "Claude Code",
|
|
7910
|
+
"claude-desktop": "Claude Desktop",
|
|
7911
|
+
antigravity: "Antigravity",
|
|
7912
|
+
codex: "Codex",
|
|
7913
|
+
vscode: "VS Code",
|
|
7914
|
+
opencode: "OpenCode",
|
|
7915
|
+
goose: "Goose",
|
|
7916
|
+
copilot: "GitHub Copilot"
|
|
7917
|
+
};
|
|
7696
7918
|
function isConnectHarnessId(value) {
|
|
7697
7919
|
return CONNECT_HARNESSES.includes(value);
|
|
7698
7920
|
}
|
|
7921
|
+
function resolveDeps(deps) {
|
|
7922
|
+
const home = deps.home ?? homedir11();
|
|
7923
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
7924
|
+
const copilotHome = deps.copilotHome ?? process.env.COPILOT_HOME;
|
|
7925
|
+
return {
|
|
7926
|
+
home,
|
|
7927
|
+
cwd,
|
|
7928
|
+
copilotHome,
|
|
7929
|
+
findZam: deps.findZam ?? (() => findExecutable("zam")),
|
|
7930
|
+
detect: deps.detect ?? (() => detectInstalledConnectHarnesses({ home, copilotHome })),
|
|
7931
|
+
connectMcp: deps.connectMcp ?? connectHarnessMcp,
|
|
7932
|
+
writeConfig: deps.writeConfig ?? ((path, content) => {
|
|
7933
|
+
mkdirSync10(dirname7(path), { recursive: true });
|
|
7934
|
+
writeFileSync9(path, content, "utf-8");
|
|
7935
|
+
}),
|
|
7936
|
+
installCopilot: deps.installCopilot ?? installCopilotExtension,
|
|
7937
|
+
installVscode: deps.installVscode ?? installVscodeExtension,
|
|
7938
|
+
resolveAntigravity: deps.resolveAntigravity ?? resolveAntigravityIdeExecutable,
|
|
7939
|
+
refreshSkills: deps.refreshSkills ?? distributeGlobalSkills
|
|
7940
|
+
};
|
|
7941
|
+
}
|
|
7942
|
+
function performAgentConnect(opts = {}, deps = {}) {
|
|
7943
|
+
const d = resolveDeps(deps);
|
|
7944
|
+
const dryRun = Boolean(opts.dryRun);
|
|
7945
|
+
const detected = opts.harness ? [opts.harness] : d.detect();
|
|
7946
|
+
const foundZam = d.findZam();
|
|
7947
|
+
const zamPath = foundZam ?? "zam";
|
|
7948
|
+
const results = [];
|
|
7949
|
+
for (const harness of detected) {
|
|
7950
|
+
const label = CONNECT_HARNESS_LABELS[harness];
|
|
7951
|
+
try {
|
|
7952
|
+
const prepared = d.connectMcp(harness, {
|
|
7953
|
+
zamPath,
|
|
7954
|
+
cwd: d.cwd,
|
|
7955
|
+
home: d.home,
|
|
7956
|
+
copilotHome: d.copilotHome
|
|
7957
|
+
});
|
|
7958
|
+
let extension = null;
|
|
7959
|
+
if (harness === "copilot") {
|
|
7960
|
+
const installed = d.installCopilot({
|
|
7961
|
+
home: d.home,
|
|
7962
|
+
zamPath,
|
|
7963
|
+
dryRun
|
|
7964
|
+
});
|
|
7965
|
+
extension = {
|
|
7966
|
+
kind: "copilot",
|
|
7967
|
+
action: installed.action,
|
|
7968
|
+
location: installed.destinationDir,
|
|
7969
|
+
detail: `${installed.launch.command} ${installed.launch.args.join(" ")}`
|
|
7970
|
+
};
|
|
7971
|
+
} else if (harness === "vscode") {
|
|
7972
|
+
const installed = d.installVscode({ home: d.home, zamPath, dryRun });
|
|
7973
|
+
extension = {
|
|
7974
|
+
kind: "vscode",
|
|
7975
|
+
action: installed.action,
|
|
7976
|
+
location: installed.vsixPath,
|
|
7977
|
+
detail: installed.launchConfigPath
|
|
7978
|
+
};
|
|
7979
|
+
} else if (harness === "antigravity") {
|
|
7980
|
+
const antigravityPath = d.resolveAntigravity();
|
|
7981
|
+
if (antigravityPath) {
|
|
7982
|
+
const installed = d.installVscode({
|
|
7983
|
+
home: d.home,
|
|
7984
|
+
zamPath,
|
|
7985
|
+
codePath: antigravityPath,
|
|
7986
|
+
dryRun
|
|
7987
|
+
});
|
|
7988
|
+
extension = {
|
|
7989
|
+
kind: "vscode",
|
|
7990
|
+
action: installed.action,
|
|
7991
|
+
location: installed.vsixPath,
|
|
7992
|
+
detail: installed.launchConfigPath
|
|
7993
|
+
};
|
|
7994
|
+
}
|
|
7995
|
+
}
|
|
7996
|
+
let wrote = false;
|
|
7997
|
+
if (!prepared.alreadyConfigured && !dryRun) {
|
|
7998
|
+
d.writeConfig(prepared.path, prepared.content);
|
|
7999
|
+
wrote = true;
|
|
8000
|
+
}
|
|
8001
|
+
results.push({
|
|
8002
|
+
harness,
|
|
8003
|
+
label,
|
|
8004
|
+
path: prepared.path,
|
|
8005
|
+
content: prepared.content,
|
|
8006
|
+
alreadyConfigured: prepared.alreadyConfigured,
|
|
8007
|
+
wrote,
|
|
8008
|
+
hint: prepared.hint,
|
|
8009
|
+
extension
|
|
8010
|
+
});
|
|
8011
|
+
} catch (error) {
|
|
8012
|
+
results.push({
|
|
8013
|
+
harness,
|
|
8014
|
+
label,
|
|
8015
|
+
path: "",
|
|
8016
|
+
content: "",
|
|
8017
|
+
alreadyConfigured: false,
|
|
8018
|
+
wrote: false,
|
|
8019
|
+
hint: "",
|
|
8020
|
+
extension: null,
|
|
8021
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8022
|
+
});
|
|
8023
|
+
}
|
|
8024
|
+
}
|
|
8025
|
+
let skills = null;
|
|
8026
|
+
if (!dryRun && detected.length > 0) {
|
|
8027
|
+
const skillResults = d.refreshSkills(d.home);
|
|
8028
|
+
skills = {
|
|
8029
|
+
refreshed: skillResults.filter((result) => result.success).length,
|
|
8030
|
+
total: skillResults.length
|
|
8031
|
+
};
|
|
8032
|
+
}
|
|
8033
|
+
return {
|
|
8034
|
+
success: results.every((result) => !result.error),
|
|
8035
|
+
detected,
|
|
8036
|
+
zamPath,
|
|
8037
|
+
zamOnPath: Boolean(foundZam),
|
|
8038
|
+
results,
|
|
8039
|
+
skills
|
|
8040
|
+
};
|
|
8041
|
+
}
|
|
8042
|
+
function inspectConnectHarnesses(deps = {}) {
|
|
8043
|
+
const d = resolveDeps(deps);
|
|
8044
|
+
const foundZam = d.findZam();
|
|
8045
|
+
const zamPath = foundZam ?? "zam";
|
|
8046
|
+
const installed = new Set(d.detect());
|
|
8047
|
+
const harnesses = USER_SCOPED_CONNECT_HARNESSES.map((harness) => {
|
|
8048
|
+
const status = {
|
|
8049
|
+
harness,
|
|
8050
|
+
label: CONNECT_HARNESS_LABELS[harness],
|
|
8051
|
+
installed: installed.has(harness),
|
|
8052
|
+
configured: false,
|
|
8053
|
+
configPath: ""
|
|
8054
|
+
};
|
|
8055
|
+
try {
|
|
8056
|
+
const probe = d.connectMcp(harness, {
|
|
8057
|
+
zamPath,
|
|
8058
|
+
cwd: d.cwd,
|
|
8059
|
+
home: d.home,
|
|
8060
|
+
copilotHome: d.copilotHome
|
|
8061
|
+
});
|
|
8062
|
+
status.configured = probe.alreadyConfigured;
|
|
8063
|
+
status.configPath = probe.path;
|
|
8064
|
+
} catch (error) {
|
|
8065
|
+
status.note = error instanceof Error ? error.message : String(error);
|
|
8066
|
+
}
|
|
8067
|
+
return status;
|
|
8068
|
+
});
|
|
8069
|
+
return { zamOnPath: Boolean(foundZam), harnesses };
|
|
8070
|
+
}
|
|
8071
|
+
|
|
8072
|
+
// src/cli/commands/agent.ts
|
|
8073
|
+
var C = {
|
|
8074
|
+
reset: "\x1B[0m",
|
|
8075
|
+
bold: "\x1B[1m",
|
|
8076
|
+
cyan: "\x1B[36m",
|
|
8077
|
+
dim: "\x1B[2m",
|
|
8078
|
+
green: "\x1B[32m",
|
|
8079
|
+
yellow: "\x1B[33m"
|
|
8080
|
+
};
|
|
8081
|
+
var SUPPORTED_AGENTS = ["opencode"];
|
|
7699
8082
|
function agentsMdPresent(cwd = process.cwd()) {
|
|
7700
8083
|
return existsSync15(join15(cwd, "AGENTS.md"));
|
|
7701
8084
|
}
|
|
@@ -7823,125 +8206,69 @@ var connectCmd = new Command("connect").description(
|
|
|
7823
8206
|
explicitHarness = harnessArg;
|
|
7824
8207
|
}
|
|
7825
8208
|
}
|
|
7826
|
-
const
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
copilotHome: process.env.COPILOT_HOME
|
|
8209
|
+
const report = performAgentConnect({
|
|
8210
|
+
harness: explicitHarness,
|
|
8211
|
+
dryRun: Boolean(opts.print)
|
|
7830
8212
|
});
|
|
7831
|
-
if (
|
|
8213
|
+
if (report.detected.length === 0) {
|
|
7832
8214
|
console.error(
|
|
7833
8215
|
"No supported user-scoped agent harness was detected. Install Codex, VS Code, or another supported host, or pass a harness explicitly."
|
|
7834
8216
|
);
|
|
7835
8217
|
process.exit(1);
|
|
7836
8218
|
}
|
|
7837
|
-
|
|
7838
|
-
if (!zamPath) {
|
|
8219
|
+
if (!report.zamOnPath) {
|
|
7839
8220
|
console.warn(
|
|
7840
8221
|
`${C.yellow}Warning: 'zam' executable was not found on your PATH. Falling back to literal 'zam'.${C.reset}`
|
|
7841
8222
|
);
|
|
7842
|
-
zamPath = "zam";
|
|
7843
8223
|
}
|
|
7844
|
-
for (const
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
zamPath,
|
|
7849
|
-
cwd: process.cwd(),
|
|
7850
|
-
home,
|
|
7851
|
-
copilotHome: process.env.COPILOT_HOME
|
|
7852
|
-
});
|
|
7853
|
-
} catch (error) {
|
|
7854
|
-
console.error(
|
|
7855
|
-
`Error preparing ${harness} MCP configuration: ${error instanceof Error ? error.message : String(error)}`
|
|
7856
|
-
);
|
|
7857
|
-
process.exit(1);
|
|
7858
|
-
}
|
|
7859
|
-
let copilotExtension;
|
|
7860
|
-
let vscodeExtension;
|
|
7861
|
-
try {
|
|
7862
|
-
if (harness === "copilot") {
|
|
7863
|
-
copilotExtension = installCopilotExtension({
|
|
7864
|
-
home,
|
|
7865
|
-
zamPath,
|
|
7866
|
-
dryRun: Boolean(opts.print)
|
|
7867
|
-
});
|
|
7868
|
-
} else if (harness === "vscode") {
|
|
7869
|
-
vscodeExtension = installVscodeExtension({
|
|
7870
|
-
home,
|
|
7871
|
-
zamPath,
|
|
7872
|
-
dryRun: Boolean(opts.print)
|
|
7873
|
-
});
|
|
7874
|
-
} else if (harness === "antigravity") {
|
|
7875
|
-
const antigravityPath = resolveAntigravityIdeExecutable();
|
|
7876
|
-
if (antigravityPath) {
|
|
7877
|
-
vscodeExtension = installVscodeExtension({
|
|
7878
|
-
home,
|
|
7879
|
-
zamPath,
|
|
7880
|
-
codePath: antigravityPath,
|
|
7881
|
-
dryRun: Boolean(opts.print)
|
|
7882
|
-
});
|
|
7883
|
-
}
|
|
7884
|
-
}
|
|
7885
|
-
} catch (error) {
|
|
7886
|
-
console.error(
|
|
7887
|
-
`Error preparing ${harness} companion extension: ${error instanceof Error ? error.message : String(error)}`
|
|
7888
|
-
);
|
|
7889
|
-
process.exit(1);
|
|
8224
|
+
for (const result of report.results) {
|
|
8225
|
+
if (result.error) {
|
|
8226
|
+
console.error(`Error connecting ${result.harness}: ${result.error}`);
|
|
8227
|
+
continue;
|
|
7890
8228
|
}
|
|
7891
8229
|
if (opts.print) {
|
|
7892
|
-
if (
|
|
8230
|
+
if (report.results.length > 1)
|
|
8231
|
+
console.log(`${C.bold}${result.harness}${C.reset}`);
|
|
7893
8232
|
console.log(`Path: ${result.path}`);
|
|
7894
8233
|
console.log(`Content:
|
|
7895
8234
|
${result.content}`);
|
|
7896
|
-
if (
|
|
7897
|
-
console.log(`Extension: ${
|
|
7898
|
-
console.log(
|
|
7899
|
-
|
|
7900
|
-
);
|
|
7901
|
-
|
|
7902
|
-
if (vscodeExtension) {
|
|
7903
|
-
console.log(`Extension: ${vscodeExtension.vsixPath}`);
|
|
7904
|
-
console.log(`Launch config: ${vscodeExtension.launchConfigPath}`);
|
|
8235
|
+
if (result.extension?.kind === "copilot") {
|
|
8236
|
+
console.log(`Extension: ${result.extension.location}`);
|
|
8237
|
+
console.log(`Launch: ${result.extension.detail}`);
|
|
8238
|
+
} else if (result.extension) {
|
|
8239
|
+
console.log(`Extension: ${result.extension.location}`);
|
|
8240
|
+
console.log(`Launch config: ${result.extension.detail}`);
|
|
7905
8241
|
}
|
|
7906
8242
|
continue;
|
|
7907
8243
|
}
|
|
7908
8244
|
if (result.alreadyConfigured) {
|
|
7909
8245
|
console.log(
|
|
7910
|
-
`${C.green}\u2713${C.reset} ${harness}: MCP server 'zam' already configured in ${result.path}`
|
|
8246
|
+
`${C.green}\u2713${C.reset} ${result.harness}: MCP server 'zam' already configured in ${result.path}`
|
|
7911
8247
|
);
|
|
7912
8248
|
} else {
|
|
7913
|
-
try {
|
|
7914
|
-
mkdirSync10(dirname7(result.path), { recursive: true });
|
|
7915
|
-
writeFileSync9(result.path, result.content, "utf-8");
|
|
7916
|
-
console.log(
|
|
7917
|
-
`${C.green}\u2713${C.reset} ${harness}: wrote MCP configuration to ${result.path}`
|
|
7918
|
-
);
|
|
7919
|
-
} catch (error) {
|
|
7920
|
-
console.error(
|
|
7921
|
-
`Error writing ${harness} MCP configuration: ${error instanceof Error ? error.message : String(error)}`
|
|
7922
|
-
);
|
|
7923
|
-
process.exit(1);
|
|
7924
|
-
}
|
|
7925
|
-
}
|
|
7926
|
-
if (copilotExtension) {
|
|
7927
8249
|
console.log(
|
|
7928
|
-
`${C.green}\u2713${C.reset}
|
|
8250
|
+
`${C.green}\u2713${C.reset} ${result.harness}: wrote MCP configuration to ${result.path}`
|
|
7929
8251
|
);
|
|
7930
8252
|
}
|
|
7931
|
-
if (
|
|
8253
|
+
if (result.extension?.kind === "copilot") {
|
|
8254
|
+
console.log(
|
|
8255
|
+
`${C.green}\u2713${C.reset} Copilot MCP Apps extension ${result.extension.action} at ${result.extension.location}`
|
|
8256
|
+
);
|
|
8257
|
+
} else if (result.extension) {
|
|
7932
8258
|
console.log(
|
|
7933
|
-
`${C.green}\u2713${C.reset} ZAM Companion ${
|
|
8259
|
+
`${C.green}\u2713${C.reset} ZAM Companion ${result.extension.action} from ${result.extension.location}`
|
|
7934
8260
|
);
|
|
7935
8261
|
}
|
|
7936
8262
|
console.log(` ${C.dim}${result.hint}${C.reset}`);
|
|
7937
8263
|
}
|
|
7938
|
-
if (
|
|
7939
|
-
const skills = distributeGlobalSkills(home);
|
|
7940
|
-
const installed = skills.filter((result) => result.success).length;
|
|
8264
|
+
if (report.skills) {
|
|
7941
8265
|
console.log(
|
|
7942
|
-
`${C.green}\u2713${C.reset} Refreshed ${
|
|
8266
|
+
`${C.green}\u2713${C.reset} Refreshed ${report.skills.refreshed}/${report.skills.total} global ZAM skill installations`
|
|
7943
8267
|
);
|
|
7944
8268
|
}
|
|
8269
|
+
if (!report.success) {
|
|
8270
|
+
process.exit(1);
|
|
8271
|
+
}
|
|
7945
8272
|
});
|
|
7946
8273
|
var agentCommand = new Command("agent").description("Provision and inspect the agent that drives ZAM sessions").addCommand(installCmd).addCommand(statusCmd).addCommand(openCmd).addCommand(listCmd).addCommand(connectCmd).action(printStatus);
|
|
7947
8274
|
|
|
@@ -7953,7 +8280,7 @@ import { existsSync as existsSync19, readdirSync as readdirSync2, readFileSync a
|
|
|
7953
8280
|
import { homedir as homedir13, tmpdir as tmpdir3 } from "os";
|
|
7954
8281
|
import { join as join22, resolve as resolve5 } from "path";
|
|
7955
8282
|
import { Command as Command2 } from "commander";
|
|
7956
|
-
import { ulid as
|
|
8283
|
+
import { ulid as ulid10 } from "ulid";
|
|
7957
8284
|
|
|
7958
8285
|
// src/cli/adapters/source-reader.ts
|
|
7959
8286
|
import dns from "dns";
|
|
@@ -7965,8 +8292,11 @@ import { spawn as spawn2 } from "child_process";
|
|
|
7965
8292
|
import { existsSync as existsSync16, readFileSync as readFileSync12, statSync as statSync2 } from "fs";
|
|
7966
8293
|
var DEFAULT_LLM_URL = "http://localhost:8000/v1";
|
|
7967
8294
|
var DEFAULT_LLM_MAX_TOKENS = 1e4;
|
|
8295
|
+
var LOCAL_CURRICULUM_IMPORT_HARD_TIMEOUT_MS = 6e5;
|
|
8296
|
+
var CLOUD_CURRICULUM_IMPORT_HARD_TIMEOUT_MS = 18e4;
|
|
7968
8297
|
var RECALL_QUESTION_MAX_OUTPUT_TOKENS = 400;
|
|
7969
8298
|
var RECALL_EVALUATION_MAX_OUTPUT_TOKENS = 1200;
|
|
8299
|
+
var RECALL_DISCUSSION_MAX_OUTPUT_TOKENS = 1200;
|
|
7970
8300
|
var RECALL_ENDPOINT_CACHE_MS = 6e4;
|
|
7971
8301
|
var cachedRecallEndpoint = null;
|
|
7972
8302
|
function recallEndpointSignature(cfg) {
|
|
@@ -8040,10 +8370,61 @@ function resolveProviderApiKey(rec) {
|
|
|
8040
8370
|
}
|
|
8041
8371
|
return DEFAULT_LLM_API_KEY;
|
|
8042
8372
|
}
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8373
|
+
var ROLE_TO_CAPABILITY2 = {
|
|
8374
|
+
recall: "text",
|
|
8375
|
+
text: "text",
|
|
8376
|
+
vision: "image",
|
|
8377
|
+
embedding: "embedding"
|
|
8378
|
+
};
|
|
8379
|
+
function materializeModelEntry(entry, base, enabled, maxFrames) {
|
|
8380
|
+
const url = entry.url || base.url;
|
|
8381
|
+
const cfg = {
|
|
8382
|
+
enabled,
|
|
8383
|
+
url,
|
|
8384
|
+
model: entry.model || base.model,
|
|
8385
|
+
apiKey: entry.apiKeyRef ? getProviderApiKey(entry.apiKeyRef) ?? DEFAULT_LLM_API_KEY : DEFAULT_LLM_API_KEY,
|
|
8386
|
+
apiFlavor: entry.apiFlavor || inferApiFlavor(url),
|
|
8387
|
+
locale: base.locale,
|
|
8388
|
+
providerName: entry.id,
|
|
8389
|
+
label: entry.label,
|
|
8390
|
+
source: "machine",
|
|
8391
|
+
local: entry.local
|
|
8392
|
+
};
|
|
8393
|
+
if (entry.runner) cfg.runner = entry.runner;
|
|
8394
|
+
if (maxFrames !== void 0) cfg.maxFrames = maxFrames;
|
|
8395
|
+
return cfg;
|
|
8396
|
+
}
|
|
8397
|
+
async function resolveCapability(db, capability) {
|
|
8398
|
+
const models = getMachineAiModels();
|
|
8399
|
+
if (models.length === 0) return null;
|
|
8400
|
+
const isVisual = capability === "image" || capability === "video";
|
|
8401
|
+
const enabled = (isVisual ? await getSetting(db, "llm.vision.enabled") : await getSetting(db, "llm.enabled")) === "true";
|
|
8402
|
+
const base = await getLlmConfig(db);
|
|
8403
|
+
let maxFrames;
|
|
8404
|
+
if (isVisual) {
|
|
8405
|
+
const raw = await getSetting(db, "llm.vision.max_frames");
|
|
8406
|
+
const parsed = raw ? parseInt(raw, 10) : 100;
|
|
8407
|
+
maxFrames = Number.isNaN(parsed) ? 100 : parsed;
|
|
8408
|
+
}
|
|
8409
|
+
const eligible = [...models].sort((a, b) => a.order - b.order).filter(
|
|
8410
|
+
(entry) => entry.capabilities[capability] && entry.detectedCapabilities[capability]
|
|
8411
|
+
);
|
|
8412
|
+
if (eligible.length === 0) return null;
|
|
8413
|
+
const configs = eligible.map(
|
|
8414
|
+
(entry) => materializeModelEntry(entry, base, enabled, maxFrames)
|
|
8415
|
+
);
|
|
8416
|
+
for (let i = configs.length - 1; i > 0; i--) {
|
|
8417
|
+
configs[i - 1] = { ...configs[i - 1], fallback: configs[i] };
|
|
8418
|
+
}
|
|
8419
|
+
return configs[0];
|
|
8420
|
+
}
|
|
8421
|
+
async function getProviderForRole(db, role) {
|
|
8422
|
+
ensureMachineProviderRolesSanitized();
|
|
8423
|
+
const viaRegistry = await resolveCapability(db, ROLE_TO_CAPABILITY2[role]);
|
|
8424
|
+
if (viaRegistry) return viaRegistry;
|
|
8425
|
+
const enabled = role === "vision" ? await getSetting(db, "llm.vision.enabled") === "true" : await getSetting(db, "llm.enabled") === "true";
|
|
8426
|
+
const base = await getLegacyRoleConfig(db, role, enabled);
|
|
8427
|
+
const providers = await readJsonSetting(db, "llm.providers");
|
|
8047
8428
|
const roles = await readJsonSetting(db, "llm.roles");
|
|
8048
8429
|
const binding = roles?.[role];
|
|
8049
8430
|
let resolved = base;
|
|
@@ -8081,6 +8462,9 @@ async function getProviderForRole(db, role) {
|
|
|
8081
8462
|
) : void 0;
|
|
8082
8463
|
return { ...primary, fallback };
|
|
8083
8464
|
}
|
|
8465
|
+
if (role === "text") {
|
|
8466
|
+
return getProviderForRole(db, "recall");
|
|
8467
|
+
}
|
|
8084
8468
|
return resolved;
|
|
8085
8469
|
}
|
|
8086
8470
|
function materializeProvider(rec, base, role, meta) {
|
|
@@ -8329,6 +8713,64 @@ Evaluation:`;
|
|
|
8329
8713
|
providerName: endpoint.providerName
|
|
8330
8714
|
};
|
|
8331
8715
|
}
|
|
8716
|
+
async function discussReviewViaLLM(db, input8) {
|
|
8717
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
8718
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
8719
|
+
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
8720
|
+
const systemPrompt = `You are ZAM, a warm, precise, and encouraging skills trainer in a follow-up discussion about one flashcard.
|
|
8721
|
+
The learner has already answered, the reference answer is revealed, and your evaluation feedback was shown \u2014 nothing about this card is a spoiler anymore.
|
|
8722
|
+
|
|
8723
|
+
Guidelines:
|
|
8724
|
+
1. Answer the learner's follow-up directly and concretely in ${langName}, grounded in the card's target concept, context, and source reference.
|
|
8725
|
+
2. Stay scoped to this card and its concept. If the learner drifts to unrelated territory, answer briefly and steer back to the concept.
|
|
8726
|
+
3. Keep replies conversational and short (a few sentences) unless the learner explicitly asks for depth. Plain text only \u2014 no markdown wrapper, headers, or bullet lists.
|
|
8727
|
+
4. The self-rating is the learner's own choice. If asked, explain the FSRS scale (1 forgot, 2 hard, 3 good, 4 easy) but never pressure them toward a specific rating.`;
|
|
8728
|
+
const cardFrame = `The card under discussion:
|
|
8729
|
+
Domain: ${input8.domain}
|
|
8730
|
+
Slug: ${input8.slug}
|
|
8731
|
+
Recall Question: ${input8.question}
|
|
8732
|
+
Learner's Answer: ${input8.userAnswer}
|
|
8733
|
+
|
|
8734
|
+
Target Concept (Correct Answer): ${input8.concept}
|
|
8735
|
+
Target Context: ${input8.context || "(none)"}
|
|
8736
|
+
${input8.sourceLinkContent ? `Source Code Reference:
|
|
8737
|
+
${input8.sourceLinkContent}` : ""}`;
|
|
8738
|
+
const messages = [
|
|
8739
|
+
{ role: "system", content: systemPrompt },
|
|
8740
|
+
{ role: "user", content: cardFrame }
|
|
8741
|
+
];
|
|
8742
|
+
const feedback = input8.feedback?.trim();
|
|
8743
|
+
if (feedback) {
|
|
8744
|
+
messages.push({ role: "assistant", content: feedback });
|
|
8745
|
+
}
|
|
8746
|
+
for (const turn of input8.thread) {
|
|
8747
|
+
messages.push({ role: turn.role, content: turn.content });
|
|
8748
|
+
}
|
|
8749
|
+
messages.push({ role: "user", content: input8.message });
|
|
8750
|
+
const res = await fetchWithInteractiveTimeout(
|
|
8751
|
+
`${endpoint.url}/chat/completions`,
|
|
8752
|
+
{
|
|
8753
|
+
method: "POST",
|
|
8754
|
+
headers: {
|
|
8755
|
+
"Content-Type": "application/json",
|
|
8756
|
+
Authorization: `Bearer ${endpoint.apiKey}`
|
|
8757
|
+
},
|
|
8758
|
+
body: JSON.stringify({
|
|
8759
|
+
model: endpoint.model,
|
|
8760
|
+
messages,
|
|
8761
|
+
temperature: 0.3,
|
|
8762
|
+
max_tokens: RECALL_DISCUSSION_MAX_OUTPUT_TOKENS
|
|
8763
|
+
}),
|
|
8764
|
+
locale: cfg.locale
|
|
8765
|
+
}
|
|
8766
|
+
);
|
|
8767
|
+
const text = await readChatContent(res, "LLM discussion");
|
|
8768
|
+
return {
|
|
8769
|
+
text,
|
|
8770
|
+
model: endpoint.model,
|
|
8771
|
+
providerName: endpoint.providerName
|
|
8772
|
+
};
|
|
8773
|
+
}
|
|
8332
8774
|
var MAX_IMPORT_TEXT_CHARS = 2e5;
|
|
8333
8775
|
var VALID_GENERATED_MODES = /* @__PURE__ */ new Set(["shadowing", "copilot", "autonomy"]);
|
|
8334
8776
|
function parseGeneratedCardArray(responseText, label, limits) {
|
|
@@ -8463,6 +8905,7 @@ Target Category: ${targetCategory}
|
|
|
8463
8905
|
${sourceUrl ? `Source Reference Link: ${sourceUrl}` : ""}
|
|
8464
8906
|
|
|
8465
8907
|
JSON Array Output:`;
|
|
8908
|
+
const hardTimeoutMs = isLocalEndpoint(endpoint.url) ? LOCAL_CURRICULUM_IMPORT_HARD_TIMEOUT_MS : CLOUD_CURRICULUM_IMPORT_HARD_TIMEOUT_MS;
|
|
8466
8909
|
const res = await fetchWithInteractiveTimeout(
|
|
8467
8910
|
`${endpoint.url}/chat/completions`,
|
|
8468
8911
|
{
|
|
@@ -8480,7 +8923,9 @@ JSON Array Output:`;
|
|
|
8480
8923
|
temperature: 0.1,
|
|
8481
8924
|
max_tokens: DEFAULT_LLM_MAX_TOKENS
|
|
8482
8925
|
}),
|
|
8483
|
-
locale
|
|
8926
|
+
locale,
|
|
8927
|
+
hardTimeoutMs,
|
|
8928
|
+
timeoutMs: hardTimeoutMs
|
|
8484
8929
|
}
|
|
8485
8930
|
);
|
|
8486
8931
|
return readChatContent(res, "LLM curriculum import");
|
|
@@ -8876,7 +9321,11 @@ async function resolveUsableTextEndpoint(db) {
|
|
|
8876
9321
|
const chain = await checkProviderChain(cfg);
|
|
8877
9322
|
const selected = chain.firstUsable;
|
|
8878
9323
|
if (!selected || !isEndpointUsable(selected)) {
|
|
8879
|
-
|
|
9324
|
+
const primary = chain.primary.endpoint;
|
|
9325
|
+
const reason = !selected ? chain.primary.online ? "configured model is unavailable" : "endpoint is offline" : "endpoint is not usable";
|
|
9326
|
+
throw new Error(
|
|
9327
|
+
`No text LLM endpoint is online (${reason}; role: text; url: ${primary.url}; model: ${primary.model})`
|
|
9328
|
+
);
|
|
8880
9329
|
}
|
|
8881
9330
|
return selected.endpoint;
|
|
8882
9331
|
}
|
|
@@ -9566,7 +10015,7 @@ async function readWebLink(url) {
|
|
|
9566
10015
|
redirect: "manual",
|
|
9567
10016
|
signal: controller.signal,
|
|
9568
10017
|
headers: {
|
|
9569
|
-
"User-Agent": "ZAM-Content-Studio/0.10.
|
|
10018
|
+
"User-Agent": "ZAM-Content-Studio/0.10.4"
|
|
9570
10019
|
}
|
|
9571
10020
|
});
|
|
9572
10021
|
if (res.status >= 300 && res.status < 400) {
|
|
@@ -12731,7 +13180,7 @@ function normalizeForComparison11(str) {
|
|
|
12731
13180
|
// src/cli/curriculum/providers/lehrplanplus-bayern/manifest.ts
|
|
12732
13181
|
var LEHRPLANPLUS_BAYERN_MANIFEST = {
|
|
12733
13182
|
schoolYear: "2026/2027",
|
|
12734
|
-
capturedOn: "2026-07-
|
|
13183
|
+
capturedOn: "2026-07-12",
|
|
12735
13184
|
sourceRevision: "LehrplanPLUS Realschule \u2013 Oktober 2023",
|
|
12736
13185
|
schoolTypes: [
|
|
12737
13186
|
{ id: "grundschule", label: "Grundschule" },
|
|
@@ -12788,12 +13237,242 @@ var LEHRPLANPLUS_BAYERN_MANIFEST = {
|
|
|
12788
13237
|
]
|
|
12789
13238
|
},
|
|
12790
13239
|
tracks: {
|
|
13240
|
+
"realschule|5|sport": [
|
|
13241
|
+
{ id: "basis_sport", label: "Basissport 5" },
|
|
13242
|
+
{ id: "diff_sport", label: "Differenzierter Sport" }
|
|
13243
|
+
],
|
|
12791
13244
|
"realschule|9|mathematik": [
|
|
12792
13245
|
{ id: "wpfg1", label: "Mathematik 9 (I)" },
|
|
12793
13246
|
{ id: "wpfg2-3", label: "Mathematik 9 (II/III)" }
|
|
12794
13247
|
]
|
|
12795
13248
|
},
|
|
12796
13249
|
topics: {
|
|
13250
|
+
"realschule|5|biologie": [
|
|
13251
|
+
{ id: "lb1", label: "Prozessbezogene Kompetenzen" },
|
|
13252
|
+
{
|
|
13253
|
+
id: "lb2",
|
|
13254
|
+
label: "Biologie, die Wissenschaft von den Lebewesen",
|
|
13255
|
+
hours: 14
|
|
13256
|
+
},
|
|
13257
|
+
{
|
|
13258
|
+
id: "lb3",
|
|
13259
|
+
label: "Bau und Funktion des menschlichen K\xF6rpers",
|
|
13260
|
+
hours: 22
|
|
13261
|
+
},
|
|
13262
|
+
{
|
|
13263
|
+
id: "lb4",
|
|
13264
|
+
label: "Tiere und Pflanzen in der Umgebung des Menschen",
|
|
13265
|
+
hours: 20
|
|
13266
|
+
}
|
|
13267
|
+
],
|
|
13268
|
+
"realschule|5|deutsch": [
|
|
13269
|
+
{ id: "lb1", label: "Sprechen und Zuh\xF6ren" },
|
|
13270
|
+
{ id: "lb2", label: "Lesen \u2013 mit Texten und weiteren Medien umgehen" },
|
|
13271
|
+
{ id: "lb3", label: "Schreiben" },
|
|
13272
|
+
{
|
|
13273
|
+
id: "lb4",
|
|
13274
|
+
label: "Sprachgebrauch und Sprache untersuchen und reflektieren"
|
|
13275
|
+
}
|
|
13276
|
+
],
|
|
13277
|
+
"realschule|5|englisch": [
|
|
13278
|
+
{ id: "lb1", label: "Kommunikative Kompetenzen" },
|
|
13279
|
+
{ id: "lb2", label: "Interkulturelle Kompetenzen" },
|
|
13280
|
+
{ id: "lb3", label: "Text- und Medienkompetenzen" },
|
|
13281
|
+
{ id: "lb4", label: "Methodische Kompetenzen" },
|
|
13282
|
+
{ id: "lb5", label: "Themengebiete" }
|
|
13283
|
+
],
|
|
13284
|
+
"realschule|5|ethik": [
|
|
13285
|
+
{ id: "lb1", label: "Meine Wirklichkeit und ich", hours: 16 },
|
|
13286
|
+
{ id: "lb2", label: "Leben in der Familie", hours: 12 },
|
|
13287
|
+
{ id: "lb3", label: "Spielen", hours: 12 },
|
|
13288
|
+
{
|
|
13289
|
+
id: "lb4",
|
|
13290
|
+
label: "Feste und Riten in Religion und Brauchtum",
|
|
13291
|
+
hours: 16
|
|
13292
|
+
}
|
|
13293
|
+
],
|
|
13294
|
+
"realschule|5|evangelische-religionslehre": [
|
|
13295
|
+
{ id: "lb1", label: "Leben in Gemeinschaft" },
|
|
13296
|
+
{ id: "lb2", label: "Die Bibel \u2013 Buch des Lebens" },
|
|
13297
|
+
{
|
|
13298
|
+
id: "lb3",
|
|
13299
|
+
label: "Erfahrungen mit Gott als Begleiter auf dem Lebensweg"
|
|
13300
|
+
},
|
|
13301
|
+
{ id: "lb4", label: "Glaube wird sichtbar und hinterl\xE4sst Spuren" },
|
|
13302
|
+
{
|
|
13303
|
+
id: "lb5",
|
|
13304
|
+
label: "Sch\xF6pfung \u2013 Unsere Welt und unser Leben als Geschenk Gottes?"
|
|
13305
|
+
}
|
|
13306
|
+
],
|
|
13307
|
+
"realschule|5|geographie": [
|
|
13308
|
+
{ id: "lb1", label: "Einf\xFChrung in das Fach", hours: 8 },
|
|
13309
|
+
{ id: "lb2", label: "Planet Erde", hours: 8 },
|
|
13310
|
+
{ id: "lb3", label: "Gestalt und Gliederung der Erde", hours: 10 },
|
|
13311
|
+
{ id: "lb4", label: "Ver\xE4nderung der Erdoberfl\xE4che", hours: 10 },
|
|
13312
|
+
{
|
|
13313
|
+
id: "lb5",
|
|
13314
|
+
label: "Naturr\xE4umliche und politische Strukturen in Deutschland und Bayern",
|
|
13315
|
+
hours: 8
|
|
13316
|
+
},
|
|
13317
|
+
{ id: "lb6", label: "Anwendung im Nahraum", hours: 8 },
|
|
13318
|
+
{ id: "lb7", label: "Aktuelle geographische Fragestellung", hours: 4 }
|
|
13319
|
+
],
|
|
13320
|
+
"realschule|5|it": [
|
|
13321
|
+
{ id: "lb1", label: "Anfangsunterricht" },
|
|
13322
|
+
{ id: "lb2", label: "Aufbauunterricht" },
|
|
13323
|
+
{ id: "lb3", label: "Bilingualer Sachfachunterricht (optional)" }
|
|
13324
|
+
],
|
|
13325
|
+
"realschule|5|iu": [
|
|
13326
|
+
{ id: "lb1", label: "Miteinander leben \u2013 Eigene Aufgaben wahrnehmen" },
|
|
13327
|
+
{
|
|
13328
|
+
id: "lb2",
|
|
13329
|
+
label: "Religi\xF6ses Leben \u2013 Ausdrucksformen des Islams kennen"
|
|
13330
|
+
},
|
|
13331
|
+
{
|
|
13332
|
+
id: "lb3",
|
|
13333
|
+
label: "Glaubenslehre des Islams \u2013 Gottesvorstellungen reflektieren"
|
|
13334
|
+
},
|
|
13335
|
+
{
|
|
13336
|
+
id: "lb4",
|
|
13337
|
+
label: "Propheten \u2013 Gottes Offenbarungen beschreiben"
|
|
13338
|
+
},
|
|
13339
|
+
{
|
|
13340
|
+
id: "lb5",
|
|
13341
|
+
label: "Muhammads Leben und Wirken \u2013 Eigenschaften von Vorbildern reflektieren"
|
|
13342
|
+
},
|
|
13343
|
+
{
|
|
13344
|
+
id: "lb6",
|
|
13345
|
+
label: "Koran und Schrifttradition \u2013 Fachsprache entwickeln"
|
|
13346
|
+
},
|
|
13347
|
+
{
|
|
13348
|
+
id: "lb7",
|
|
13349
|
+
label: "Geschichte und Geographie des Islams \u2013 Historische Kontexte erl\xE4utern"
|
|
13350
|
+
},
|
|
13351
|
+
{
|
|
13352
|
+
id: "lb8",
|
|
13353
|
+
label: "Religionen und Weltanschauungen \u2013 Gemeinsamkeiten und Unterschiede vergleichen"
|
|
13354
|
+
}
|
|
13355
|
+
],
|
|
13356
|
+
"realschule|5|ir": [
|
|
13357
|
+
{
|
|
13358
|
+
id: "lb1",
|
|
13359
|
+
label: "J\xFCdischer Kalender und Jahreszyklus: Grundlagen des j\xFCdischen Kalenders, Feiertage im Jahreskreis, Chanukka und Purim",
|
|
13360
|
+
hours: 12
|
|
13361
|
+
},
|
|
13362
|
+
{
|
|
13363
|
+
id: "lb2",
|
|
13364
|
+
label: "Gebet und Ritus: Schma und Amida, die beiden Hauptgebete",
|
|
13365
|
+
hours: 10
|
|
13366
|
+
},
|
|
13367
|
+
{
|
|
13368
|
+
id: "lb3",
|
|
13369
|
+
label: "Mensch und Welt: Vertrauend einen neuen Anfang wagen",
|
|
13370
|
+
hours: 10
|
|
13371
|
+
},
|
|
13372
|
+
{
|
|
13373
|
+
id: "lb4",
|
|
13374
|
+
label: "J\xFCdische Geschichte und Philosophie: Biblische Zeit und Monotheismus",
|
|
13375
|
+
hours: 12
|
|
13376
|
+
},
|
|
13377
|
+
{
|
|
13378
|
+
id: "lb5",
|
|
13379
|
+
label: "Schriftliche Quellen \u2013 Werte: Tora und Anawa (Bescheidenheit)",
|
|
13380
|
+
hours: 12
|
|
13381
|
+
}
|
|
13382
|
+
],
|
|
13383
|
+
"realschule|5|katholische-religionslehre": [
|
|
13384
|
+
{
|
|
13385
|
+
id: "lb1",
|
|
13386
|
+
label: "Auf Gott vertrauen \u2013 einen neuen Anfang wagen",
|
|
13387
|
+
hours: 10
|
|
13388
|
+
},
|
|
13389
|
+
{
|
|
13390
|
+
id: "lb2",
|
|
13391
|
+
label: "\u201EUmsorge mich mit deiner Liebe\u201C \u2013 beten und meditieren",
|
|
13392
|
+
hours: 10
|
|
13393
|
+
},
|
|
13394
|
+
{
|
|
13395
|
+
id: "lb3",
|
|
13396
|
+
label: "Erfahrungen mit Gott \u2013 Die Heilige Schrift",
|
|
13397
|
+
hours: 12
|
|
13398
|
+
},
|
|
13399
|
+
{
|
|
13400
|
+
id: "lb4",
|
|
13401
|
+
label: '\u201EIn jenen Tagen trat einer auf" \u2013 Jesus im Blickwinkel seiner Zeit und Umwelt',
|
|
13402
|
+
hours: 12
|
|
13403
|
+
},
|
|
13404
|
+
{
|
|
13405
|
+
id: "lb5",
|
|
13406
|
+
label: "Leben in der Pfarrgemeinde \u2013 Eingebundensein in die Kirche",
|
|
13407
|
+
hours: 12
|
|
13408
|
+
}
|
|
13409
|
+
],
|
|
13410
|
+
"realschule|5|kunst": [
|
|
13411
|
+
{
|
|
13412
|
+
id: "lb1",
|
|
13413
|
+
label: "Bildnerische Auseinandersetzung mit Wirklichkeit und Fantasie",
|
|
13414
|
+
hours: 44
|
|
13415
|
+
},
|
|
13416
|
+
{ id: "lb2", label: "Bildende Kunst", hours: 20 },
|
|
13417
|
+
{ id: "lb3", label: "Angewandte Kunst", hours: 20 }
|
|
13418
|
+
],
|
|
13419
|
+
"realschule|5|mathematik": [
|
|
13420
|
+
{ id: "lb1", label: "Nat\xFCrliche Zahlen", hours: 50 },
|
|
13421
|
+
{ id: "lb2", label: "Ganze Zahlen", hours: 20 },
|
|
13422
|
+
{
|
|
13423
|
+
id: "lb3",
|
|
13424
|
+
label: "Geometrische Grundvorstellungen und Grundbegriffe",
|
|
13425
|
+
hours: 30
|
|
13426
|
+
},
|
|
13427
|
+
{ id: "lb4", label: "Gr\xF6\xDFen", hours: 20 },
|
|
13428
|
+
{
|
|
13429
|
+
id: "lb5",
|
|
13430
|
+
label: "Umfang und Fl\xE4cheninhalt ebener Figuren",
|
|
13431
|
+
hours: 15
|
|
13432
|
+
},
|
|
13433
|
+
{ id: "lb6", label: "Auswertung von Daten", hours: 5 }
|
|
13434
|
+
],
|
|
13435
|
+
"realschule|5|musik": [
|
|
13436
|
+
{ id: "lb1", label: "Sprechen \u2013 Singen \u2013 Musizieren", hours: 20 },
|
|
13437
|
+
{ id: "lb2", label: "Musik \u2013 Mensch \u2013 Zeit", hours: 10 },
|
|
13438
|
+
{ id: "lb3", label: "Bewegung \u2013 Tanz \u2013 Szene", hours: 10 },
|
|
13439
|
+
{ id: "lb4", label: "Musik und ihre Grundlagen", hours: 16 }
|
|
13440
|
+
],
|
|
13441
|
+
"realschule|5|or": [
|
|
13442
|
+
{ id: "lb1", label: "Miteinander leben", hours: 10 },
|
|
13443
|
+
{ id: "lb2", label: "Von Gott und zu Gott sprechen", hours: 10 },
|
|
13444
|
+
{ id: "lb3", label: "Die Bibel", hours: 12 },
|
|
13445
|
+
{ id: "lb4", label: "Ursprung der Kirche", hours: 12 },
|
|
13446
|
+
{ id: "lb5", label: "Kirche vor Ort", hours: 12 }
|
|
13447
|
+
],
|
|
13448
|
+
"realschule|5|sport|basis_sport": [
|
|
13449
|
+
{ id: "lb1", label: "Gesundheit und Fitness" },
|
|
13450
|
+
{ id: "lb2", label: "Fairness/Kooperation/Selbstkompetenz" },
|
|
13451
|
+
{ id: "lb3", label: "Freizeit und Umwelt" },
|
|
13452
|
+
{ id: "lb4", label: "Sportliche Handlungsfelder" }
|
|
13453
|
+
],
|
|
13454
|
+
"realschule|5|sport|diff_sport": [
|
|
13455
|
+
{ id: "lb1", label: "Bewegungsk\xFCnste" },
|
|
13456
|
+
{ id: "lb2", label: "Radsport" },
|
|
13457
|
+
{ id: "lb3", label: "Rhythmische Sportgymnastik" },
|
|
13458
|
+
{ id: "lb4", label: "Sportklettern" }
|
|
13459
|
+
],
|
|
13460
|
+
"realschule|5|textiles-gestalten": [
|
|
13461
|
+
{ id: "lb1", label: "Eine textile Fl\xE4che bilden \u2013 Filzen", hours: 18 },
|
|
13462
|
+
{ id: "lb2", label: "Eine textile Fl\xE4che bilden \u2013 H\xE4keln", hours: 18 },
|
|
13463
|
+
{ id: "lb3", label: "Eine textile Fl\xE4che bilden \u2013 Weben", hours: 24 },
|
|
13464
|
+
{ id: "lb4", label: "Eine textile Fl\xE4che bilden \u2013 Kn\xFCpfen", hours: 24 },
|
|
13465
|
+
{
|
|
13466
|
+
id: "lb5",
|
|
13467
|
+
label: "Eine textile Fl\xE4che verarbeiten \u2013 Handn\xE4hen, Maschinenn\xE4hen",
|
|
13468
|
+
hours: 42
|
|
13469
|
+
}
|
|
13470
|
+
],
|
|
13471
|
+
"realschule|5|werken": [
|
|
13472
|
+
{ id: "lb1", label: "Arbeiten mit dem Werkstoff Holz", hours: 28 },
|
|
13473
|
+
{ id: "lb2", label: "Arbeiten mit Papierwerkstoffen", hours: 28 },
|
|
13474
|
+
{ id: "lb3", label: "Arbeiten mit plastischen Massen", hours: 28 }
|
|
13475
|
+
],
|
|
12797
13476
|
"realschule|9|mathematik|wpfg1": [
|
|
12798
13477
|
{ id: "lb1", label: "Reelle Zahlen", hours: 10 },
|
|
12799
13478
|
{ id: "lb2", label: "Zentrische Streckung", hours: 17 },
|
|
@@ -12835,6 +13514,24 @@ var LEHRPLANPLUS_BAYERN_MANIFEST = {
|
|
|
12835
13514
|
]
|
|
12836
13515
|
},
|
|
12837
13516
|
contentUrls: {
|
|
13517
|
+
"realschule|5|biologie": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/biologie/inhalt/fachlehrplaene",
|
|
13518
|
+
"realschule|5|deutsch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/deutsch/inhalt/fachlehrplaene",
|
|
13519
|
+
"realschule|5|englisch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/englisch/inhalt/fachlehrplaene",
|
|
13520
|
+
"realschule|5|ethik": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/ethik/inhalt/fachlehrplaene",
|
|
13521
|
+
"realschule|5|evangelische-religionslehre": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/evangelische-religionslehre/inhalt/fachlehrplaene",
|
|
13522
|
+
"realschule|5|geographie": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/geographie/inhalt/fachlehrplaene",
|
|
13523
|
+
"realschule|5|it": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/it/inhalt/fachlehrplaene",
|
|
13524
|
+
"realschule|5|iu": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/iu/inhalt/fachlehrplaene",
|
|
13525
|
+
"realschule|5|ir": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/ir/inhalt/fachlehrplaene",
|
|
13526
|
+
"realschule|5|katholische-religionslehre": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/katholische-religionslehre/inhalt/fachlehrplaene",
|
|
13527
|
+
"realschule|5|kunst": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/kunst/inhalt/fachlehrplaene",
|
|
13528
|
+
"realschule|5|mathematik": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/mathematik/inhalt/fachlehrplaene",
|
|
13529
|
+
"realschule|5|musik": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/musik/inhalt/fachlehrplaene",
|
|
13530
|
+
"realschule|5|or": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/or/inhalt/fachlehrplaene",
|
|
13531
|
+
"realschule|5|sport|basis_sport": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/sport/inhalt/fachlehrplaene?w_schulart=realschule&wt_1=schulart&w_fach=sport&wt_2=fach&w_jgs=5&wt_3=jgs&w_auspraegung=basis_sport",
|
|
13532
|
+
"realschule|5|sport|diff_sport": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/sport/inhalt/fachlehrplaene?w_schulart=realschule&wt_1=schulart&w_fach=sport&wt_2=fach&w_jgs=5&wt_3=jgs&w_auspraegung=diff_sport",
|
|
13533
|
+
"realschule|5|textiles-gestalten": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/textiles-gestalten/inhalt/fachlehrplaene",
|
|
13534
|
+
"realschule|5|werken": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/werken/inhalt/fachlehrplaene",
|
|
12838
13535
|
"realschule|9|mathematik|wpfg1": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/mathematik/inhalt/fachlehrplaene?w_schulart=realschule&wt_1=schulart&w_fach=mathematik&wt_2=fach&w_jgs=9&wt_3=jgs&w_auspraegung=wpfg1",
|
|
12839
13536
|
"realschule|9|mathematik|wpfg2-3": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/mathematik/inhalt/fachlehrplaene?w_schulart=realschule&wt_1=schulart&w_fach=mathematik&wt_2=fach&w_jgs=9&wt_3=jgs&w_auspraegung=wpfg2-3",
|
|
12840
13537
|
"realschule|9|deutsch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/deutsch/inhalt/fachlehrplaene",
|
|
@@ -12846,6 +13543,95 @@ var LEHRPLANPLUS_BAYERN_MANIFEST = {
|
|
|
12846
13543
|
function levelKey(schoolType, grade, subject, track) {
|
|
12847
13544
|
return track ? `${schoolType}|${grade}|${subject}|${track}` : `${schoolType}|${grade}|${subject}`;
|
|
12848
13545
|
}
|
|
13546
|
+
function parseHtmlSections(html) {
|
|
13547
|
+
const chunks = html.split('<div id="thema_');
|
|
13548
|
+
const sections = [];
|
|
13549
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
13550
|
+
const chunk = chunks[i];
|
|
13551
|
+
const quoteIdx = chunk.indexOf('"');
|
|
13552
|
+
if (quoteIdx === -1) continue;
|
|
13553
|
+
const id = chunk.slice(0, quoteIdx);
|
|
13554
|
+
const classStart = chunk.indexOf('class="');
|
|
13555
|
+
if (classStart === -1) continue;
|
|
13556
|
+
const classEnd = chunk.indexOf('"', classStart + 7);
|
|
13557
|
+
const classContent = chunk.slice(classStart + 7, classEnd);
|
|
13558
|
+
const lvlMatch = classContent.match(/headline_lvl(\d+)/);
|
|
13559
|
+
if (!lvlMatch) continue;
|
|
13560
|
+
const level = parseInt(lvlMatch[1], 10);
|
|
13561
|
+
const contentHtml = chunk.slice(classEnd + 1);
|
|
13562
|
+
const headerMatch = contentHtml.match(
|
|
13563
|
+
/<a[^>]*class="paragraph_toggle"[^>]*>([\s\S]*?)<\/a>/i
|
|
13564
|
+
);
|
|
13565
|
+
const headerText = headerMatch ? cleanHtmlText12(headerMatch[1]).trim() : "";
|
|
13566
|
+
sections.push({
|
|
13567
|
+
id,
|
|
13568
|
+
level,
|
|
13569
|
+
headerText,
|
|
13570
|
+
contentHtml
|
|
13571
|
+
});
|
|
13572
|
+
}
|
|
13573
|
+
return sections;
|
|
13574
|
+
}
|
|
13575
|
+
function resolveTopicLabel(topicId) {
|
|
13576
|
+
for (const key of Object.keys(LEHRPLANPLUS_BAYERN_MANIFEST.topics)) {
|
|
13577
|
+
const list = LEHRPLANPLUS_BAYERN_MANIFEST.topics[key];
|
|
13578
|
+
const match = list.find(
|
|
13579
|
+
(t2) => t2.id === topicId || `${key}#${t2.id}` === topicId
|
|
13580
|
+
);
|
|
13581
|
+
if (match) {
|
|
13582
|
+
return match.label;
|
|
13583
|
+
}
|
|
13584
|
+
}
|
|
13585
|
+
return null;
|
|
13586
|
+
}
|
|
13587
|
+
function findTopicSectionHtml(sections, topicId) {
|
|
13588
|
+
const label = resolveTopicLabel(topicId);
|
|
13589
|
+
if (!label) return null;
|
|
13590
|
+
const normalizedLabel = normalizeForComparison12(label);
|
|
13591
|
+
const lvl1Index = sections.findIndex((s) => {
|
|
13592
|
+
if (s.level !== 1) return false;
|
|
13593
|
+
const normalizedHeader = normalizeForComparison12(s.headerText);
|
|
13594
|
+
return normalizedHeader.includes(normalizedLabel);
|
|
13595
|
+
});
|
|
13596
|
+
if (lvl1Index === -1) return null;
|
|
13597
|
+
const collectedSections = [sections[lvl1Index]];
|
|
13598
|
+
for (let i = lvl1Index + 1; i < sections.length; i++) {
|
|
13599
|
+
if (sections[i].level === 1) {
|
|
13600
|
+
break;
|
|
13601
|
+
}
|
|
13602
|
+
collectedSections.push(sections[i]);
|
|
13603
|
+
}
|
|
13604
|
+
return collectedSections.map((s) => s.contentHtml).join("\n");
|
|
13605
|
+
}
|
|
13606
|
+
function isExcludedCompetenceItem(liAttrs, text) {
|
|
13607
|
+
if (/plus_servicematerialien|plus_ueberg_ziele/i.test(liAttrs)) {
|
|
13608
|
+
return true;
|
|
13609
|
+
}
|
|
13610
|
+
const normalized = text.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").trim();
|
|
13611
|
+
if (/^\+?\s*servicematerialien/.test(normalized)) return true;
|
|
13612
|
+
if (/^\+?\s*uebergreifende\s+ziele/.test(normalized)) return true;
|
|
13613
|
+
return false;
|
|
13614
|
+
}
|
|
13615
|
+
function extractCompetenceItems(topicHtml) {
|
|
13616
|
+
const items = [];
|
|
13617
|
+
const abschBlocks = topicHtml.match(/<div class="thema_absch">[\s\S]*?<\/div>/gi) ?? [];
|
|
13618
|
+
for (const block of abschBlocks) {
|
|
13619
|
+
const liRegex = /<li([^>]*)>([\s\S]*?)<\/li>/gi;
|
|
13620
|
+
let match = liRegex.exec(block);
|
|
13621
|
+
while (match !== null) {
|
|
13622
|
+
const text = cleanHtmlText12(match[2]).trim();
|
|
13623
|
+
if (text.length > 0 && !isExcludedCompetenceItem(match[1], text)) {
|
|
13624
|
+
items.push(text);
|
|
13625
|
+
}
|
|
13626
|
+
match = liRegex.exec(block);
|
|
13627
|
+
}
|
|
13628
|
+
}
|
|
13629
|
+
return items;
|
|
13630
|
+
}
|
|
13631
|
+
function truncateLabel(text, maxLen = 72) {
|
|
13632
|
+
if (text.length <= maxLen) return text;
|
|
13633
|
+
return `${text.slice(0, maxLen - 1).trim()}\u2026`;
|
|
13634
|
+
}
|
|
12849
13635
|
var lehrplanplusBayernProvider = {
|
|
12850
13636
|
id: "lehrplanplus-bayern",
|
|
12851
13637
|
country: "DE",
|
|
@@ -12891,68 +13677,42 @@ var lehrplanplusBayernProvider = {
|
|
|
12891
13677
|
};
|
|
12892
13678
|
},
|
|
12893
13679
|
extractTopics(html, topicIds) {
|
|
12894
|
-
const
|
|
12895
|
-
const sections = [];
|
|
12896
|
-
for (let i = 1; i < chunks.length; i++) {
|
|
12897
|
-
const chunk = chunks[i];
|
|
12898
|
-
const quoteIdx = chunk.indexOf('"');
|
|
12899
|
-
if (quoteIdx === -1) continue;
|
|
12900
|
-
const id = chunk.slice(0, quoteIdx);
|
|
12901
|
-
const classStart = chunk.indexOf('class="');
|
|
12902
|
-
if (classStart === -1) continue;
|
|
12903
|
-
const classEnd = chunk.indexOf('"', classStart + 7);
|
|
12904
|
-
const classContent = chunk.slice(classStart + 7, classEnd);
|
|
12905
|
-
const lvlMatch = classContent.match(/headline_lvl(\d+)/);
|
|
12906
|
-
if (!lvlMatch) continue;
|
|
12907
|
-
const level = parseInt(lvlMatch[1], 10);
|
|
12908
|
-
const contentHtml = chunk.slice(classEnd + 1);
|
|
12909
|
-
const headerMatch = contentHtml.match(
|
|
12910
|
-
/<a[^>]*class="paragraph_toggle"[^>]*>([\s\S]*?)<\/a>/i
|
|
12911
|
-
);
|
|
12912
|
-
const headerText = headerMatch ? cleanHtmlText12(headerMatch[1]).trim() : "";
|
|
12913
|
-
sections.push({
|
|
12914
|
-
id,
|
|
12915
|
-
level,
|
|
12916
|
-
headerText,
|
|
12917
|
-
contentHtml
|
|
12918
|
-
});
|
|
12919
|
-
}
|
|
13680
|
+
const sections = parseHtmlSections(html);
|
|
12920
13681
|
const results = {};
|
|
12921
13682
|
for (const topicId of topicIds) {
|
|
12922
|
-
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
const match = list.find(
|
|
12926
|
-
(t2) => t2.id === topicId || `${key}#${t2.id}` === topicId
|
|
12927
|
-
);
|
|
12928
|
-
if (match) {
|
|
12929
|
-
label = match.label;
|
|
12930
|
-
break;
|
|
12931
|
-
}
|
|
13683
|
+
const topicHtml = findTopicSectionHtml(sections, topicId);
|
|
13684
|
+
if (topicHtml) {
|
|
13685
|
+
results[topicId] = cleanHtmlText12(topicHtml);
|
|
12932
13686
|
}
|
|
12933
|
-
if (!label) {
|
|
12934
|
-
continue;
|
|
12935
|
-
}
|
|
12936
|
-
const normalizedLabel = normalizeForComparison12(label);
|
|
12937
|
-
const lvl1Index = sections.findIndex((s) => {
|
|
12938
|
-
if (s.level !== 1) return false;
|
|
12939
|
-
const normalizedHeader = normalizeForComparison12(s.headerText);
|
|
12940
|
-
return normalizedHeader.includes(normalizedLabel);
|
|
12941
|
-
});
|
|
12942
|
-
if (lvl1Index === -1) {
|
|
12943
|
-
continue;
|
|
12944
|
-
}
|
|
12945
|
-
const collectedSections = [sections[lvl1Index]];
|
|
12946
|
-
for (let i = lvl1Index + 1; i < sections.length; i++) {
|
|
12947
|
-
if (sections[i].level === 1) {
|
|
12948
|
-
break;
|
|
12949
|
-
}
|
|
12950
|
-
collectedSections.push(sections[i]);
|
|
12951
|
-
}
|
|
12952
|
-
const fullHtml = collectedSections.map((s) => s.contentHtml).join("\n");
|
|
12953
|
-
results[topicId] = cleanHtmlText12(fullHtml);
|
|
12954
13687
|
}
|
|
12955
13688
|
return results;
|
|
13689
|
+
},
|
|
13690
|
+
extractSubTopics(html, topicId) {
|
|
13691
|
+
const sections = parseHtmlSections(html);
|
|
13692
|
+
const topicHtml = findTopicSectionHtml(sections, topicId);
|
|
13693
|
+
if (!topicHtml) return [];
|
|
13694
|
+
const header = sections.find((s) => {
|
|
13695
|
+
if (s.level !== 1) return false;
|
|
13696
|
+
const label = resolveTopicLabel(topicId);
|
|
13697
|
+
if (!label) return false;
|
|
13698
|
+
return normalizeForComparison12(s.headerText).includes(
|
|
13699
|
+
normalizeForComparison12(label)
|
|
13700
|
+
);
|
|
13701
|
+
});
|
|
13702
|
+
const headerText = header?.headerText ?? resolveTopicLabel(topicId) ?? "";
|
|
13703
|
+
const competenceItems = extractCompetenceItems(topicHtml);
|
|
13704
|
+
if (competenceItems.length === 0) return [];
|
|
13705
|
+
return competenceItems.map((item, index) => {
|
|
13706
|
+
const text = `${headerText}
|
|
13707
|
+
|
|
13708
|
+
${item}`;
|
|
13709
|
+
return {
|
|
13710
|
+
id: `ku${index + 1}`,
|
|
13711
|
+
label: truncateLabel(item),
|
|
13712
|
+
textLength: text.length,
|
|
13713
|
+
text
|
|
13714
|
+
};
|
|
13715
|
+
});
|
|
12956
13716
|
}
|
|
12957
13717
|
};
|
|
12958
13718
|
function cleanHtmlText12(html) {
|
|
@@ -13491,6 +14251,127 @@ function getCurriculumProvider(id) {
|
|
|
13491
14251
|
return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
|
|
13492
14252
|
}
|
|
13493
14253
|
|
|
14254
|
+
// src/cli/llm/capability-probe.ts
|
|
14255
|
+
init_kernel();
|
|
14256
|
+
var EMBEDDING_MODEL_HINTS = [
|
|
14257
|
+
"embed",
|
|
14258
|
+
"text-embedding",
|
|
14259
|
+
"bge-",
|
|
14260
|
+
"gte-",
|
|
14261
|
+
"nomic",
|
|
14262
|
+
"mxbai"
|
|
14263
|
+
];
|
|
14264
|
+
var VISION_MODEL_HINTS = [
|
|
14265
|
+
"vision",
|
|
14266
|
+
"-vl",
|
|
14267
|
+
"vl-",
|
|
14268
|
+
"vlm",
|
|
14269
|
+
"llava",
|
|
14270
|
+
"gpt-4o",
|
|
14271
|
+
"gpt-4.1",
|
|
14272
|
+
"gpt-5",
|
|
14273
|
+
"gemini",
|
|
14274
|
+
"pixtral",
|
|
14275
|
+
"minicpm-v",
|
|
14276
|
+
"internvl",
|
|
14277
|
+
"moondream",
|
|
14278
|
+
"llama-3.2",
|
|
14279
|
+
"llama3.2",
|
|
14280
|
+
// Xiaomi MiMo(-VL) is multimodal; the plain "mimo-v*" tag carries no "-vl".
|
|
14281
|
+
"mimo"
|
|
14282
|
+
];
|
|
14283
|
+
function matchesAny(id, hints) {
|
|
14284
|
+
const lower = id.toLowerCase();
|
|
14285
|
+
return hints.some((hint) => lower.includes(hint));
|
|
14286
|
+
}
|
|
14287
|
+
function catalogHasModel(catalog, model) {
|
|
14288
|
+
const lower = model.toLowerCase();
|
|
14289
|
+
return catalog.some((id) => id.toLowerCase() === lower);
|
|
14290
|
+
}
|
|
14291
|
+
function classifyCapabilities(entry, catalog, catalogKnown, dimProbeEmbedding = false) {
|
|
14292
|
+
const detected = emptyCapabilityFlags();
|
|
14293
|
+
if (entry.apiFlavor === "anthropic-messages") {
|
|
14294
|
+
detected.text = true;
|
|
14295
|
+
detected.image = true;
|
|
14296
|
+
return detected;
|
|
14297
|
+
}
|
|
14298
|
+
const looksEmbedding = matchesAny(entry.model, EMBEDDING_MODEL_HINTS);
|
|
14299
|
+
const looksVision = matchesAny(entry.model, VISION_MODEL_HINTS);
|
|
14300
|
+
const inCatalog = catalogHasModel(catalog, entry.model);
|
|
14301
|
+
detected.embedding = looksEmbedding || dimProbeEmbedding;
|
|
14302
|
+
detected.image = looksVision;
|
|
14303
|
+
detected.text = !detected.embedding && (inCatalog || !catalogKnown);
|
|
14304
|
+
return detected;
|
|
14305
|
+
}
|
|
14306
|
+
function resolveApiKey(apiKeyRef) {
|
|
14307
|
+
if (!apiKeyRef) return DEFAULT_LLM_API_KEY;
|
|
14308
|
+
return getProviderApiKey(apiKeyRef) ?? DEFAULT_LLM_API_KEY;
|
|
14309
|
+
}
|
|
14310
|
+
async function probeModelCapabilities(entry, opts = {}) {
|
|
14311
|
+
const apiKey = resolveApiKey(entry.apiKeyRef);
|
|
14312
|
+
if (entry.apiFlavor === "anthropic-messages") {
|
|
14313
|
+
const online2 = await isLlmOnline(entry.url);
|
|
14314
|
+
return {
|
|
14315
|
+
reachable: online2,
|
|
14316
|
+
catalog: [],
|
|
14317
|
+
detected: classifyCapabilities(entry, [], false)
|
|
14318
|
+
};
|
|
14319
|
+
}
|
|
14320
|
+
const online = await isLlmOnline(entry.url);
|
|
14321
|
+
if (!online) {
|
|
14322
|
+
return { reachable: false, catalog: [], detected: emptyCapabilityFlags() };
|
|
14323
|
+
}
|
|
14324
|
+
const catalog = await getAvailableModels(entry.url, apiKey);
|
|
14325
|
+
const catalogKnown = catalog.length > 0;
|
|
14326
|
+
let dimProbeEmbedding = false;
|
|
14327
|
+
const looksEmbedding = matchesAny(entry.model, EMBEDDING_MODEL_HINTS);
|
|
14328
|
+
if (opts.embeddingDimProbe && !catalogKnown && !looksEmbedding) {
|
|
14329
|
+
try {
|
|
14330
|
+
const [vector] = await embedTexts(
|
|
14331
|
+
{ url: entry.url, model: entry.model, apiKey },
|
|
14332
|
+
["capability probe"]
|
|
14333
|
+
);
|
|
14334
|
+
dimProbeEmbedding = Array.isArray(vector) && vector.length > 0;
|
|
14335
|
+
} catch {
|
|
14336
|
+
dimProbeEmbedding = false;
|
|
14337
|
+
}
|
|
14338
|
+
}
|
|
14339
|
+
return {
|
|
14340
|
+
reachable: true,
|
|
14341
|
+
catalog,
|
|
14342
|
+
detected: classifyCapabilities(
|
|
14343
|
+
entry,
|
|
14344
|
+
catalog,
|
|
14345
|
+
catalogKnown,
|
|
14346
|
+
dimProbeEmbedding
|
|
14347
|
+
)
|
|
14348
|
+
};
|
|
14349
|
+
}
|
|
14350
|
+
function reconcileCapabilities(userSelected, detected) {
|
|
14351
|
+
const result = emptyCapabilityFlags();
|
|
14352
|
+
for (const key of Object.keys(result)) {
|
|
14353
|
+
result[key] = userSelected[key] && detected[key];
|
|
14354
|
+
}
|
|
14355
|
+
return result;
|
|
14356
|
+
}
|
|
14357
|
+
function validateModelSave(entry, probe, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
14358
|
+
if (!probe.reachable) {
|
|
14359
|
+
return {
|
|
14360
|
+
ok: false,
|
|
14361
|
+
error: "Endpoint is unreachable \u2014 cannot verify capabilities. Bring it online and retry."
|
|
14362
|
+
};
|
|
14363
|
+
}
|
|
14364
|
+
return {
|
|
14365
|
+
ok: true,
|
|
14366
|
+
entry: {
|
|
14367
|
+
...entry,
|
|
14368
|
+
capabilities: reconcileCapabilities(entry.capabilities, probe.detected),
|
|
14369
|
+
detectedCapabilities: probe.detected,
|
|
14370
|
+
probedAt: now()
|
|
14371
|
+
}
|
|
14372
|
+
};
|
|
14373
|
+
}
|
|
14374
|
+
|
|
13494
14375
|
// src/cli/llm/vision.ts
|
|
13495
14376
|
init_kernel();
|
|
13496
14377
|
import { randomBytes } from "crypto";
|
|
@@ -13964,6 +14845,12 @@ function bindRoleProviders(roles, role, primary, fallback) {
|
|
|
13964
14845
|
if (fallback) binding.fallback = fallback;
|
|
13965
14846
|
return { ...roles, [role]: binding };
|
|
13966
14847
|
}
|
|
14848
|
+
function unbindRole(roles, role) {
|
|
14849
|
+
if (!(role in roles)) return roles;
|
|
14850
|
+
const next = { ...roles };
|
|
14851
|
+
delete next[role];
|
|
14852
|
+
return next;
|
|
14853
|
+
}
|
|
13967
14854
|
function maskSecret(key) {
|
|
13968
14855
|
return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
|
|
13969
14856
|
}
|
|
@@ -16042,12 +16929,15 @@ bridgeCommand.command("provider-config-bind").description("Bind providers to an
|
|
|
16042
16929
|
await withProviderScope(machine, async (db) => {
|
|
16043
16930
|
const providers = await readScopedProviders(db, machine);
|
|
16044
16931
|
const roles = await readScopedRoles(db, machine);
|
|
16045
|
-
|
|
16932
|
+
let nextRoles = bindRoleProviders(
|
|
16046
16933
|
roles,
|
|
16047
16934
|
opts.role,
|
|
16048
16935
|
opts.primary,
|
|
16049
16936
|
opts.fallback
|
|
16050
16937
|
);
|
|
16938
|
+
if (opts.role === "recall") {
|
|
16939
|
+
nextRoles = unbindRole(nextRoles, "text");
|
|
16940
|
+
}
|
|
16051
16941
|
await writeScopedRoles(db, machine, nextRoles);
|
|
16052
16942
|
const binding = nextRoles[opts.role];
|
|
16053
16943
|
const primary = providers[opts.primary];
|
|
@@ -16079,8 +16969,198 @@ bridgeCommand.command("list-models").description("List models exposed by an LLM
|
|
|
16079
16969
|
const models = await getAvailableModels(opts.url, apiKey);
|
|
16080
16970
|
jsonOut2({ models });
|
|
16081
16971
|
});
|
|
16082
|
-
|
|
16083
|
-
|
|
16972
|
+
function modelRow(entry) {
|
|
16973
|
+
return {
|
|
16974
|
+
id: entry.id,
|
|
16975
|
+
label: entry.label,
|
|
16976
|
+
url: entry.url,
|
|
16977
|
+
model: entry.model,
|
|
16978
|
+
local: entry.local,
|
|
16979
|
+
apiFlavor: entry.apiFlavor,
|
|
16980
|
+
runner: entry.runner,
|
|
16981
|
+
order: entry.order,
|
|
16982
|
+
capabilities: entry.capabilities,
|
|
16983
|
+
detectedCapabilities: entry.detectedCapabilities,
|
|
16984
|
+
probedAt: entry.probedAt,
|
|
16985
|
+
apiKeyRef: entry.apiKeyRef,
|
|
16986
|
+
keyState: entry.apiKeyRef ? getProviderApiKey(entry.apiKeyRef) ? "set" : "missing" : "none"
|
|
16987
|
+
};
|
|
16988
|
+
}
|
|
16989
|
+
function parseCapabilityFlags(json) {
|
|
16990
|
+
const flags = emptyCapabilityFlags();
|
|
16991
|
+
if (!json) return flags;
|
|
16992
|
+
let parsed;
|
|
16993
|
+
try {
|
|
16994
|
+
parsed = JSON.parse(json);
|
|
16995
|
+
} catch {
|
|
16996
|
+
jsonError("Invalid --capabilities JSON");
|
|
16997
|
+
}
|
|
16998
|
+
if (parsed && typeof parsed === "object") {
|
|
16999
|
+
const record = parsed;
|
|
17000
|
+
for (const key of Object.keys(flags)) {
|
|
17001
|
+
if (record[key] === true) flags[key] = true;
|
|
17002
|
+
}
|
|
17003
|
+
}
|
|
17004
|
+
return flags;
|
|
17005
|
+
}
|
|
17006
|
+
function urlLooksLocal(url) {
|
|
17007
|
+
return /localhost|127\.0\.0\.1|\[::1\]|::1/.test(url);
|
|
17008
|
+
}
|
|
17009
|
+
bridgeCommand.command("model-list").description("List the machine-local capability model registry (JSON)").action(() => {
|
|
17010
|
+
ensureMachineAiModelsMigrated();
|
|
17011
|
+
const models = [...getMachineAiModels()].sort((a, b) => a.order - b.order);
|
|
17012
|
+
jsonOut2({ models: models.map(modelRow) });
|
|
17013
|
+
});
|
|
17014
|
+
bridgeCommand.command("model-probe").description("Detect capabilities of an endpoint via metadata (JSON)").requiredOption("--url <url>", "Endpoint base URL").requiredOption("--model <model>", "Model id").option(
|
|
17015
|
+
"--flavor <flavor>",
|
|
17016
|
+
`Wire protocol: ${VALID_API_FLAVORS.join(" | ")}`
|
|
17017
|
+
).option("--key-ref <ref>", "Credential reference for API key").option("--embedding-dim-probe", "Allow one cheap /v1/embeddings dim probe").action(async (opts) => {
|
|
17018
|
+
if (opts.flavor && !VALID_API_FLAVORS.includes(opts.flavor)) {
|
|
17019
|
+
jsonError(`Invalid --flavor: ${opts.flavor}.`);
|
|
17020
|
+
}
|
|
17021
|
+
const apiFlavor = opts.flavor ?? inferApiFlavor(opts.url);
|
|
17022
|
+
const probe = await probeModelCapabilities(
|
|
17023
|
+
{
|
|
17024
|
+
url: opts.url,
|
|
17025
|
+
model: opts.model,
|
|
17026
|
+
apiFlavor,
|
|
17027
|
+
apiKeyRef: opts.keyRef
|
|
17028
|
+
},
|
|
17029
|
+
{ embeddingDimProbe: opts.embeddingDimProbe === true }
|
|
17030
|
+
);
|
|
17031
|
+
jsonOut2({
|
|
17032
|
+
reachable: probe.reachable,
|
|
17033
|
+
catalog: probe.catalog,
|
|
17034
|
+
detected: probe.detected
|
|
17035
|
+
});
|
|
17036
|
+
});
|
|
17037
|
+
bridgeCommand.command("model-upsert").description(
|
|
17038
|
+
"Add or update a registry entry; probes before persisting (JSON)"
|
|
17039
|
+
).option("--id <id>", "Existing entry id (omit to create)").option("--label <label>", "Human label").option("--url <url>", "Endpoint base URL").option("--model <model>", "Model id").option(
|
|
17040
|
+
"--flavor <flavor>",
|
|
17041
|
+
`Wire protocol: ${VALID_API_FLAVORS.join(" | ")}`
|
|
17042
|
+
).option("--local", "Mark as local endpoint").option("--no-local", "Mark as cloud/non-local endpoint").option("--runner <runner>", "Local runner hint").option("--key-ref <ref>", "Credential reference for API key").option("--capabilities <json>", "JSON object of user-selected capabilities").option("--order <n>", "Explicit sort order").action(async (opts, command) => {
|
|
17043
|
+
if (opts.flavor && !VALID_API_FLAVORS.includes(opts.flavor)) {
|
|
17044
|
+
jsonError(`Invalid --flavor: ${opts.flavor}.`);
|
|
17045
|
+
}
|
|
17046
|
+
const models = getMachineAiModels();
|
|
17047
|
+
const existingIndex = opts.id ? models.findIndex((m) => m.id === opts.id) : -1;
|
|
17048
|
+
if (opts.id && existingIndex < 0) jsonError(`No such model: ${opts.id}`);
|
|
17049
|
+
const prev = existingIndex >= 0 ? models[existingIndex] : void 0;
|
|
17050
|
+
const url = opts.url ?? prev?.url ?? "";
|
|
17051
|
+
if (!url) jsonError("--url is required");
|
|
17052
|
+
const model = opts.model ?? prev?.model ?? "";
|
|
17053
|
+
if (!model) jsonError("--model is required");
|
|
17054
|
+
const apiFlavor = opts.flavor ?? prev?.apiFlavor ?? inferApiFlavor(url);
|
|
17055
|
+
const local = command.getOptionValueSource("local") === "cli" ? opts.local === true : prev?.local ?? urlLooksLocal(url);
|
|
17056
|
+
const order = opts.order !== void 0 ? Number.parseInt(opts.order, 10) : prev?.order ?? models.length;
|
|
17057
|
+
const candidate = {
|
|
17058
|
+
id: opts.id ?? ulid10(),
|
|
17059
|
+
label: opts.label ?? prev?.label ?? model,
|
|
17060
|
+
url,
|
|
17061
|
+
model,
|
|
17062
|
+
local,
|
|
17063
|
+
apiFlavor,
|
|
17064
|
+
order,
|
|
17065
|
+
capabilities: opts.capabilities ? parseCapabilityFlags(opts.capabilities) : prev?.capabilities ?? emptyCapabilityFlags(),
|
|
17066
|
+
detectedCapabilities: prev?.detectedCapabilities ?? emptyCapabilityFlags()
|
|
17067
|
+
};
|
|
17068
|
+
const runner = opts.runner ?? prev?.runner;
|
|
17069
|
+
if (runner) candidate.runner = runner;
|
|
17070
|
+
const apiKeyRef = opts.keyRef ?? prev?.apiKeyRef;
|
|
17071
|
+
if (apiKeyRef) candidate.apiKeyRef = apiKeyRef;
|
|
17072
|
+
const probe = await probeModelCapabilities(candidate, {
|
|
17073
|
+
embeddingDimProbe: true
|
|
17074
|
+
});
|
|
17075
|
+
const validation = validateModelSave(candidate, probe);
|
|
17076
|
+
if (!validation.ok || !validation.entry) {
|
|
17077
|
+
jsonError(validation.error ?? "Model could not be saved.");
|
|
17078
|
+
}
|
|
17079
|
+
const next = [...models];
|
|
17080
|
+
if (existingIndex >= 0) next[existingIndex] = validation.entry;
|
|
17081
|
+
else next.push(validation.entry);
|
|
17082
|
+
saveMachineAiModels(next);
|
|
17083
|
+
jsonOut2({
|
|
17084
|
+
ok: true,
|
|
17085
|
+
model: modelRow(validation.entry),
|
|
17086
|
+
probe: { reachable: probe.reachable, detected: probe.detected }
|
|
17087
|
+
});
|
|
17088
|
+
});
|
|
17089
|
+
bridgeCommand.command("model-reprobe").description("Re-run capability detection for an entry; may widen (JSON)").requiredOption("--id <id>", "Registry entry id").action(async (opts) => {
|
|
17090
|
+
const models = getMachineAiModels();
|
|
17091
|
+
const index = models.findIndex((m) => m.id === opts.id);
|
|
17092
|
+
if (index < 0) jsonError(`No such model: ${opts.id}`);
|
|
17093
|
+
const entry = models[index];
|
|
17094
|
+
const probe = await probeModelCapabilities(entry, {
|
|
17095
|
+
embeddingDimProbe: true
|
|
17096
|
+
});
|
|
17097
|
+
const validation = validateModelSave(entry, probe);
|
|
17098
|
+
if (!validation.ok || !validation.entry) {
|
|
17099
|
+
jsonError(validation.error ?? "Re-probe failed.");
|
|
17100
|
+
}
|
|
17101
|
+
const next = [...models];
|
|
17102
|
+
next[index] = validation.entry;
|
|
17103
|
+
saveMachineAiModels(next);
|
|
17104
|
+
jsonOut2({
|
|
17105
|
+
ok: true,
|
|
17106
|
+
model: modelRow(validation.entry),
|
|
17107
|
+
probe: { reachable: probe.reachable, detected: probe.detected }
|
|
17108
|
+
});
|
|
17109
|
+
});
|
|
17110
|
+
bridgeCommand.command("model-remove").description("Remove a registry entry (JSON)").requiredOption("--id <id>", "Registry entry id").action((opts) => {
|
|
17111
|
+
const models = getMachineAiModels();
|
|
17112
|
+
const next = models.filter((m) => m.id !== opts.id);
|
|
17113
|
+
if (next.length === models.length) jsonError(`No such model: ${opts.id}`);
|
|
17114
|
+
next.sort((a, b) => a.order - b.order).forEach((m, i) => {
|
|
17115
|
+
m.order = i;
|
|
17116
|
+
});
|
|
17117
|
+
saveMachineAiModels(next);
|
|
17118
|
+
jsonOut2({ ok: true, id: opts.id, models: next.map(modelRow) });
|
|
17119
|
+
});
|
|
17120
|
+
bridgeCommand.command("model-reorder").description("Set registry order from an ordered id list (JSON)").requiredOption("--ids <json>", "JSON array of entry ids in desired order").action((opts) => {
|
|
17121
|
+
let ids;
|
|
17122
|
+
try {
|
|
17123
|
+
ids = JSON.parse(opts.ids);
|
|
17124
|
+
} catch {
|
|
17125
|
+
jsonError("Invalid --ids JSON");
|
|
17126
|
+
return;
|
|
17127
|
+
}
|
|
17128
|
+
if (!Array.isArray(ids)) jsonError("--ids must be a JSON array");
|
|
17129
|
+
const models = getMachineAiModels();
|
|
17130
|
+
const rank = new Map(ids.map((id, i) => [id, i]));
|
|
17131
|
+
const next = [...models].sort((a, b) => {
|
|
17132
|
+
const ra = rank.get(a.id) ?? ids.length + a.order;
|
|
17133
|
+
const rb = rank.get(b.id) ?? ids.length + b.order;
|
|
17134
|
+
return ra - rb;
|
|
17135
|
+
});
|
|
17136
|
+
next.forEach((m, i) => {
|
|
17137
|
+
m.order = i;
|
|
17138
|
+
});
|
|
17139
|
+
saveMachineAiModels(next);
|
|
17140
|
+
jsonOut2({ ok: true, models: next.map(modelRow) });
|
|
17141
|
+
});
|
|
17142
|
+
bridgeCommand.command("model-set-capabilities").description(
|
|
17143
|
+
"Set user-enabled capabilities within the detected ceiling (JSON)"
|
|
17144
|
+
).requiredOption("--id <id>", "Registry entry id").requiredOption(
|
|
17145
|
+
"--capabilities <json>",
|
|
17146
|
+
"JSON object of desired capabilities"
|
|
17147
|
+
).action((opts) => {
|
|
17148
|
+
const models = getMachineAiModels();
|
|
17149
|
+
const index = models.findIndex((m) => m.id === opts.id);
|
|
17150
|
+
if (index < 0) jsonError(`No such model: ${opts.id}`);
|
|
17151
|
+
const requested = parseCapabilityFlags(opts.capabilities);
|
|
17152
|
+
const detected = models[index].detectedCapabilities;
|
|
17153
|
+
const capabilities = emptyCapabilityFlags();
|
|
17154
|
+
for (const key of Object.keys(capabilities)) {
|
|
17155
|
+
capabilities[key] = requested[key] && detected[key];
|
|
17156
|
+
}
|
|
17157
|
+
const next = [...models];
|
|
17158
|
+
next[index] = { ...models[index], capabilities };
|
|
17159
|
+
saveMachineAiModels(next);
|
|
17160
|
+
jsonOut2({ ok: true, model: modelRow(next[index]) });
|
|
17161
|
+
});
|
|
17162
|
+
bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for an endpoint URL (JSON)").requiredOption("--url <url>", "Endpoint base URL").action((opts) => {
|
|
17163
|
+
jsonOut2({ recommendation: getCloudModelRecommendation(opts.url) });
|
|
16084
17164
|
});
|
|
16085
17165
|
bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
|
|
16086
17166
|
const profile = getSystemProfile();
|
|
@@ -16224,6 +17304,93 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
16224
17304
|
}
|
|
16225
17305
|
});
|
|
16226
17306
|
});
|
|
17307
|
+
function parseDiscussionThread(raw) {
|
|
17308
|
+
if (!raw) return [];
|
|
17309
|
+
let parsed;
|
|
17310
|
+
try {
|
|
17311
|
+
parsed = JSON.parse(raw);
|
|
17312
|
+
} catch {
|
|
17313
|
+
throw new Error("Invalid --thread: not valid JSON");
|
|
17314
|
+
}
|
|
17315
|
+
if (!Array.isArray(parsed)) {
|
|
17316
|
+
throw new Error("Invalid --thread: expected a JSON array of turns");
|
|
17317
|
+
}
|
|
17318
|
+
return parsed.map((turn, index) => {
|
|
17319
|
+
const candidate = turn;
|
|
17320
|
+
if (candidate?.role !== "user" && candidate?.role !== "assistant" || typeof candidate?.content !== "string") {
|
|
17321
|
+
throw new Error(
|
|
17322
|
+
`Invalid --thread: turn ${index} must be {"role": "user"|"assistant", "content": string}`
|
|
17323
|
+
);
|
|
17324
|
+
}
|
|
17325
|
+
return { role: candidate.role, content: candidate.content };
|
|
17326
|
+
});
|
|
17327
|
+
}
|
|
17328
|
+
bridgeCommand.command("discuss-review").description(
|
|
17329
|
+
"Answer one turn of the post-reveal follow-up discussion about a card (JSON)"
|
|
17330
|
+
).requiredOption("--slug <slug>", "Token slug").requiredOption("--concept <concept>", "Target concept text").requiredOption("--domain <domain>", "Token domain").requiredOption("--bloom-level <level>", "Bloom taxonomy level").requiredOption("--question <question>", "Question prompt presented").requiredOption("--user-answer <answer>", "User's typed answer").requiredOption("--message <text>", "The learner's newest discussion turn").option("--feedback <text>", "AI feedback already shown for this answer").option(
|
|
17331
|
+
"--thread <json>",
|
|
17332
|
+
'Prior turns, oldest first, as JSON: [{"role":"user"|"assistant","content":"\u2026"},\u2026]'
|
|
17333
|
+
).option("--context <context>", "Optional token context details").option("--source-link <link>", "Optional source link").option(
|
|
17334
|
+
"--source-content <content>",
|
|
17335
|
+
"Pre-resolved source reference content (skips re-fetch when set)"
|
|
17336
|
+
).action(async (opts) => {
|
|
17337
|
+
await withDb2(async (db) => {
|
|
17338
|
+
const isEnabled = await getSetting(db, "llm.enabled") === "true";
|
|
17339
|
+
if (!isEnabled) {
|
|
17340
|
+
jsonOut2({
|
|
17341
|
+
success: false,
|
|
17342
|
+
error: "LLM integration is disabled",
|
|
17343
|
+
reply: ""
|
|
17344
|
+
});
|
|
17345
|
+
return;
|
|
17346
|
+
}
|
|
17347
|
+
let thread;
|
|
17348
|
+
try {
|
|
17349
|
+
thread = parseDiscussionThread(opts.thread);
|
|
17350
|
+
} catch (err) {
|
|
17351
|
+
jsonOut2({
|
|
17352
|
+
success: false,
|
|
17353
|
+
error: err.message,
|
|
17354
|
+
reply: ""
|
|
17355
|
+
});
|
|
17356
|
+
return;
|
|
17357
|
+
}
|
|
17358
|
+
let resolvedContextContent = opts.sourceContent ?? null;
|
|
17359
|
+
if (resolvedContextContent == null && opts.sourceLink) {
|
|
17360
|
+
try {
|
|
17361
|
+
const resolved = await resolveReviewContext(opts.sourceLink);
|
|
17362
|
+
resolvedContextContent = resolved?.content ?? null;
|
|
17363
|
+
} catch {
|
|
17364
|
+
}
|
|
17365
|
+
}
|
|
17366
|
+
try {
|
|
17367
|
+
const result = await discussReviewViaLLM(db, {
|
|
17368
|
+
slug: opts.slug,
|
|
17369
|
+
concept: opts.concept,
|
|
17370
|
+
domain: opts.domain,
|
|
17371
|
+
bloomLevel: Number(opts.bloomLevel),
|
|
17372
|
+
context: opts.context,
|
|
17373
|
+
question: opts.question,
|
|
17374
|
+
userAnswer: opts.userAnswer,
|
|
17375
|
+
sourceLinkContent: resolvedContextContent,
|
|
17376
|
+
feedback: opts.feedback ?? null,
|
|
17377
|
+
thread,
|
|
17378
|
+
message: opts.message
|
|
17379
|
+
});
|
|
17380
|
+
jsonOut2({
|
|
17381
|
+
success: true,
|
|
17382
|
+
reply: result.text,
|
|
17383
|
+
replyModel: result.model
|
|
17384
|
+
});
|
|
17385
|
+
} catch (err) {
|
|
17386
|
+
jsonOut2({
|
|
17387
|
+
success: false,
|
|
17388
|
+
error: err.message,
|
|
17389
|
+
reply: ""
|
|
17390
|
+
});
|
|
17391
|
+
}
|
|
17392
|
+
});
|
|
17393
|
+
});
|
|
16227
17394
|
bridgeCommand.command("desktop-bootstrap").description("Initialize first-run desktop state (JSON)").option("--user <id>", "Preferred user ID when none is configured").action(async (opts) => {
|
|
16228
17395
|
await withDb2(async (db) => {
|
|
16229
17396
|
const userId = await ensureDefaultUser(db, opts.user);
|
|
@@ -16252,6 +17419,54 @@ bridgeCommand.command("get-settings").description("Get active ZAM settings (JSON
|
|
|
16252
17419
|
});
|
|
16253
17420
|
});
|
|
16254
17421
|
});
|
|
17422
|
+
bridgeCommand.command("agent-harness-status").description(
|
|
17423
|
+
"Detect installed agent harnesses and their ZAM MCP configuration state (JSON)"
|
|
17424
|
+
).action(() => {
|
|
17425
|
+
const report = inspectConnectHarnesses();
|
|
17426
|
+
jsonOut2({
|
|
17427
|
+
success: true,
|
|
17428
|
+
zamOnPath: report.zamOnPath,
|
|
17429
|
+
harnesses: report.harnesses
|
|
17430
|
+
});
|
|
17431
|
+
});
|
|
17432
|
+
bridgeCommand.command("agent-connect").description(
|
|
17433
|
+
"Run the idempotent agent-connect flow for one or all detected harnesses (JSON)"
|
|
17434
|
+
).option("--harness <id>", "Explicit harness id (default: all detected)").option(
|
|
17435
|
+
"--auto-once",
|
|
17436
|
+
"First-run mode: skip when the auto-connect marker is already set; set the marker after a run that detected at least one harness"
|
|
17437
|
+
).action(async (opts) => {
|
|
17438
|
+
if (opts.harness && !isConnectHarnessId(opts.harness)) {
|
|
17439
|
+
jsonOut2({
|
|
17440
|
+
success: false,
|
|
17441
|
+
error: `Unsupported harness: ${opts.harness}`
|
|
17442
|
+
});
|
|
17443
|
+
return;
|
|
17444
|
+
}
|
|
17445
|
+
const harness = opts.harness;
|
|
17446
|
+
const run = () => {
|
|
17447
|
+
const report = performAgentConnect({ harness });
|
|
17448
|
+
return {
|
|
17449
|
+
success: report.success,
|
|
17450
|
+
detected: report.detected,
|
|
17451
|
+
zamOnPath: report.zamOnPath,
|
|
17452
|
+
results: report.results.map(({ content: _content, ...rest }) => rest),
|
|
17453
|
+
skills: report.skills
|
|
17454
|
+
};
|
|
17455
|
+
};
|
|
17456
|
+
if (opts.autoOnce) {
|
|
17457
|
+
if (getAgentConnectAutoDone()) {
|
|
17458
|
+
jsonOut2({ success: true, skipped: true });
|
|
17459
|
+
return;
|
|
17460
|
+
}
|
|
17461
|
+
const payload = run();
|
|
17462
|
+
if (payload.detected.length > 0) {
|
|
17463
|
+
setAgentConnectAutoDone(true);
|
|
17464
|
+
}
|
|
17465
|
+
jsonOut2(payload);
|
|
17466
|
+
return;
|
|
17467
|
+
}
|
|
17468
|
+
jsonOut2(run());
|
|
17469
|
+
});
|
|
16255
17470
|
async function readDatabaseUserSummaries(db) {
|
|
16256
17471
|
return await db.prepare(
|
|
16257
17472
|
`SELECT user_id AS id, COUNT(*) AS cardCount
|
|
@@ -16683,7 +17898,10 @@ bridgeCommand.command("personal-card-delete").description("Hard-delete a token a
|
|
|
16683
17898
|
});
|
|
16684
17899
|
});
|
|
16685
17900
|
});
|
|
16686
|
-
bridgeCommand.command("personal-card-import-curriculum").description("Parse curriculum text using LLM and import cards (JSON)").option("--user <id>", "User ID (default: whoami)").
|
|
17901
|
+
bridgeCommand.command("personal-card-import-curriculum").description("Parse curriculum text using LLM and import cards (JSON)").option("--user <id>", "User ID (default: whoami)").option("--text <text>", "Curriculum syllabus/content text").option(
|
|
17902
|
+
"--sourceId <id>",
|
|
17903
|
+
"Read curriculum text from a sources row (avoids large IPC payloads)"
|
|
17904
|
+
).requiredOption(
|
|
16687
17905
|
"--domain <domain>",
|
|
16688
17906
|
"Default category/domain for imported cards"
|
|
16689
17907
|
).option("--source <url>", "Provenance source link or URL").option("--preview", "Return parsed cards without saving them").option(
|
|
@@ -16697,6 +17915,17 @@ bridgeCommand.command("personal-card-import-curriculum").description("Parse curr
|
|
|
16697
17915
|
).action(async (opts) => {
|
|
16698
17916
|
await withDb2(async (db) => {
|
|
16699
17917
|
const userId = await resolveUser(opts, db, { json: true });
|
|
17918
|
+
let curriculumText = opts.text;
|
|
17919
|
+
if (opts.sourceId) {
|
|
17920
|
+
const row = await db.prepare("SELECT content FROM sources WHERE id = ?").get(opts.sourceId);
|
|
17921
|
+
if (!row) {
|
|
17922
|
+
jsonError(`Source not found: ${opts.sourceId}`);
|
|
17923
|
+
}
|
|
17924
|
+
curriculumText = row.content ?? "";
|
|
17925
|
+
}
|
|
17926
|
+
if (!curriculumText?.trim()) {
|
|
17927
|
+
jsonError("Curriculum text is required (--text or --sourceId)");
|
|
17928
|
+
}
|
|
16700
17929
|
const contextNames = parseKnowledgeContextNames2(
|
|
16701
17930
|
opts.knowledgeContext || []
|
|
16702
17931
|
);
|
|
@@ -16704,7 +17933,7 @@ bridgeCommand.command("personal-card-import-curriculum").description("Parse curr
|
|
|
16704
17933
|
const firstContext = contexts[0]?.name;
|
|
16705
17934
|
const cards = await importCurriculumViaLLM(
|
|
16706
17935
|
db,
|
|
16707
|
-
|
|
17936
|
+
curriculumText,
|
|
16708
17937
|
opts.domain,
|
|
16709
17938
|
opts.source || null,
|
|
16710
17939
|
{ knowledgeContext: firstContext }
|
|
@@ -16873,7 +18102,7 @@ bridgeCommand.command("personal-source-import").description(
|
|
|
16873
18102
|
} else {
|
|
16874
18103
|
throw new Error(`Invalid source type: ${opts.type}`);
|
|
16875
18104
|
}
|
|
16876
|
-
const sourceId =
|
|
18105
|
+
const sourceId = ulid10();
|
|
16877
18106
|
await db.prepare(
|
|
16878
18107
|
`INSERT INTO sources (id, type, uri, content)
|
|
16879
18108
|
VALUES (?, ?, ?, ?)
|
|
@@ -17058,10 +18287,86 @@ async function fetchRawHtml(url) {
|
|
|
17058
18287
|
clearTimeout(timeoutId);
|
|
17059
18288
|
}
|
|
17060
18289
|
}
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
18290
|
+
var CURRICULUM_TOPIC_URI_PREFIX = "zam-curriculum-topic://";
|
|
18291
|
+
async function extractAndStoreCurriculumTopics(db, provider, topics) {
|
|
18292
|
+
const topicsByUri = /* @__PURE__ */ new Map();
|
|
18293
|
+
for (const topic of topics) {
|
|
18294
|
+
const resolved = provider.resolveTopic(topic);
|
|
18295
|
+
const list = topicsByUri.get(resolved.uri) || [];
|
|
18296
|
+
list.push(topic);
|
|
18297
|
+
topicsByUri.set(resolved.uri, list);
|
|
18298
|
+
}
|
|
18299
|
+
const extracted = [];
|
|
18300
|
+
for (const [uri, uriTopics] of topicsByUri.entries()) {
|
|
18301
|
+
const rawHtml = await fetchRawHtml(uri);
|
|
18302
|
+
let extractedTexts = {};
|
|
18303
|
+
if (provider.extractTopics) {
|
|
18304
|
+
const fullTopicIds = uriTopics.map((t2) => `${t2.sourceRef}#${t2.id}`);
|
|
18305
|
+
extractedTexts = provider.extractTopics(rawHtml, fullTopicIds);
|
|
18306
|
+
} else {
|
|
18307
|
+
const cleanText = cleanHtml(rawHtml);
|
|
18308
|
+
for (const t2 of uriTopics) {
|
|
18309
|
+
extractedTexts[`${t2.sourceRef}#${t2.id}`] = cleanText;
|
|
18310
|
+
}
|
|
18311
|
+
}
|
|
18312
|
+
const pageCleanedText = cleanHtml(rawHtml);
|
|
18313
|
+
const sourceId = ulid10();
|
|
18314
|
+
await db.prepare(
|
|
18315
|
+
`INSERT INTO sources (id, type, uri, content)
|
|
18316
|
+
VALUES (?, 'web', ?, ?)
|
|
18317
|
+
ON CONFLICT(uri) DO UPDATE SET
|
|
18318
|
+
type = excluded.type,
|
|
18319
|
+
content = excluded.content`
|
|
18320
|
+
).run(sourceId, uri, pageCleanedText);
|
|
18321
|
+
const record = await db.prepare("SELECT id FROM sources WHERE uri = ?").get(uri);
|
|
18322
|
+
for (const topic of uriTopics) {
|
|
18323
|
+
const resolved = provider.resolveTopic(topic);
|
|
18324
|
+
const text = extractedTexts[resolved.topicId] || "";
|
|
18325
|
+
const topicUri = `${CURRICULUM_TOPIC_URI_PREFIX}${resolved.topicId}`;
|
|
18326
|
+
const topicSourceId = ulid10();
|
|
18327
|
+
await db.prepare(
|
|
18328
|
+
`INSERT INTO sources (id, type, uri, content)
|
|
18329
|
+
VALUES (?, 'web', ?, ?)
|
|
18330
|
+
ON CONFLICT(uri) DO UPDATE SET
|
|
18331
|
+
content = excluded.content`
|
|
18332
|
+
).run(topicSourceId, topicUri, text);
|
|
18333
|
+
const topicRecord = await db.prepare("SELECT id FROM sources WHERE uri = ?").get(topicUri);
|
|
18334
|
+
extracted.push({
|
|
18335
|
+
topicId: resolved.topicId,
|
|
18336
|
+
uri,
|
|
18337
|
+
sourceId: record.id,
|
|
18338
|
+
topicSourceId: topicRecord.id,
|
|
18339
|
+
textLength: text.length
|
|
18340
|
+
});
|
|
18341
|
+
}
|
|
18342
|
+
}
|
|
18343
|
+
return extracted;
|
|
18344
|
+
}
|
|
18345
|
+
async function assignConfirmedProposalContexts(db, proposals, contexts) {
|
|
18346
|
+
for (const p of proposals) {
|
|
18347
|
+
const baseText = p.question && p.question.trim().length > 0 ? p.question : p.concept;
|
|
18348
|
+
const cleanDomain = slugify(p.domain || "");
|
|
18349
|
+
const cleanBase = slugify(baseText);
|
|
18350
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
18351
|
+
if (baseSlug.length > 60) {
|
|
18352
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
18353
|
+
}
|
|
18354
|
+
if (!baseSlug) {
|
|
18355
|
+
baseSlug = "token";
|
|
18356
|
+
}
|
|
18357
|
+
const token = await getTokenBySlug(db, baseSlug);
|
|
18358
|
+
if (token) {
|
|
18359
|
+
for (const context of contexts) {
|
|
18360
|
+
await assignTokenToContext(db, token.id, context.id);
|
|
18361
|
+
}
|
|
18362
|
+
}
|
|
18363
|
+
}
|
|
18364
|
+
}
|
|
18365
|
+
bridgeCommand.command("curriculum-import-status").description(
|
|
18366
|
+
"Check which selected curriculum topics are already imported for a user (JSON)"
|
|
18367
|
+
).requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topics <json>", "JSON array of selected topic nodes").option("--user <id>", "User ID (default: whoami)").action(async (opts) => {
|
|
17064
18368
|
await withDb2(async (db) => {
|
|
18369
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
17065
18370
|
const provider = getCurriculumProvider(opts.provider);
|
|
17066
18371
|
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
17067
18372
|
let topics;
|
|
@@ -17071,47 +18376,446 @@ bridgeCommand.command("curriculum-extract-topics").description(
|
|
|
17071
18376
|
jsonError("Invalid --topics JSON");
|
|
17072
18377
|
return;
|
|
17073
18378
|
}
|
|
17074
|
-
const
|
|
18379
|
+
const status = [];
|
|
17075
18380
|
for (const topic of topics) {
|
|
17076
|
-
|
|
17077
|
-
const list = topicsByUri.get(resolved.uri) || [];
|
|
17078
|
-
list.push(topic);
|
|
17079
|
-
topicsByUri.set(resolved.uri, list);
|
|
17080
|
-
}
|
|
17081
|
-
const extracted = [];
|
|
17082
|
-
for (const [uri, uriTopics] of topicsByUri.entries()) {
|
|
17083
|
-
const rawHtml = await fetchRawHtml(uri);
|
|
17084
|
-
let extractedTexts = {};
|
|
17085
|
-
if (provider.extractTopics) {
|
|
17086
|
-
const fullTopicIds = uriTopics.map((t2) => `${t2.sourceRef}#${t2.id}`);
|
|
17087
|
-
extractedTexts = provider.extractTopics(rawHtml, fullTopicIds);
|
|
17088
|
-
} else {
|
|
17089
|
-
const cleanText = cleanHtml(rawHtml);
|
|
17090
|
-
for (const t2 of uriTopics) {
|
|
17091
|
-
extractedTexts[`${t2.sourceRef}#${t2.id}`] = cleanText;
|
|
17092
|
-
}
|
|
17093
|
-
}
|
|
17094
|
-
const pageCleanedText = cleanHtml(rawHtml);
|
|
17095
|
-
const sourceId = ulid9();
|
|
17096
|
-
await db.prepare(
|
|
17097
|
-
`INSERT INTO sources (id, type, uri, content)
|
|
17098
|
-
VALUES (?, 'web', ?, ?)
|
|
17099
|
-
ON CONFLICT(uri) DO UPDATE SET
|
|
17100
|
-
type = excluded.type,
|
|
17101
|
-
content = excluded.content`
|
|
17102
|
-
).run(sourceId, uri, pageCleanedText);
|
|
17103
|
-
const record = await db.prepare("SELECT id FROM sources WHERE uri = ?").get(uri);
|
|
17104
|
-
for (const topic of uriTopics) {
|
|
18381
|
+
try {
|
|
17105
18382
|
const resolved = provider.resolveTopic(topic);
|
|
17106
|
-
const
|
|
17107
|
-
|
|
18383
|
+
const cardCount = await countUserCardsForCurriculumTopic(
|
|
18384
|
+
db,
|
|
18385
|
+
userId,
|
|
18386
|
+
provider.id,
|
|
18387
|
+
resolved.topicId
|
|
18388
|
+
);
|
|
18389
|
+
status.push({
|
|
17108
18390
|
topicId: resolved.topicId,
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
18391
|
+
shortId: topic.id,
|
|
18392
|
+
cardCount,
|
|
18393
|
+
alreadyImported: cardCount > 0
|
|
18394
|
+
});
|
|
18395
|
+
} catch (err) {
|
|
18396
|
+
status.push({
|
|
18397
|
+
topicId: topic.id,
|
|
18398
|
+
shortId: topic.id,
|
|
18399
|
+
cardCount: 0,
|
|
18400
|
+
alreadyImported: false,
|
|
18401
|
+
error: err.message || String(err)
|
|
18402
|
+
});
|
|
18403
|
+
}
|
|
18404
|
+
}
|
|
18405
|
+
jsonOut2({ success: true, status });
|
|
18406
|
+
});
|
|
18407
|
+
});
|
|
18408
|
+
bridgeCommand.command("curriculum-import-topic").description(
|
|
18409
|
+
"Extract, LLM-preview, and confirm-import one curriculum topic server-side (JSON)"
|
|
18410
|
+
).requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topic <json>", "Single topic node JSON").requiredOption("--domain <domain>", "Subject label for imported card domain").option("--user <id>", "User ID (default: whoami)").option(
|
|
18411
|
+
"--knowledge-context <context>",
|
|
18412
|
+
"Assign confirmed tokens to a knowledge context (repeatable)",
|
|
18413
|
+
(val, memo) => {
|
|
18414
|
+
memo.push(val);
|
|
18415
|
+
return memo;
|
|
18416
|
+
},
|
|
18417
|
+
[]
|
|
18418
|
+
).action(async (opts) => {
|
|
18419
|
+
await withDb2(async (db) => {
|
|
18420
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
18421
|
+
const provider = getCurriculumProvider(opts.provider);
|
|
18422
|
+
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
18423
|
+
let topic;
|
|
18424
|
+
try {
|
|
18425
|
+
topic = JSON.parse(opts.topic);
|
|
18426
|
+
} catch {
|
|
18427
|
+
jsonError("Invalid --topic JSON");
|
|
18428
|
+
return;
|
|
18429
|
+
}
|
|
18430
|
+
const extracted = await extractAndStoreCurriculumTopics(db, provider, [
|
|
18431
|
+
topic
|
|
18432
|
+
]);
|
|
18433
|
+
const item = extracted[0];
|
|
18434
|
+
if (!item || item.textLength === 0) {
|
|
18435
|
+
jsonError(`No curriculum text extracted for topic "${topic.id}"`);
|
|
18436
|
+
}
|
|
18437
|
+
const contextNames = parseKnowledgeContextNames2(
|
|
18438
|
+
opts.knowledgeContext || []
|
|
18439
|
+
);
|
|
18440
|
+
const contexts = await resolveKnowledgeContexts2(db, contextNames);
|
|
18441
|
+
const firstContext = contexts[0]?.name;
|
|
18442
|
+
const topicRow = await db.prepare("SELECT content FROM sources WHERE id = ?").get(item.topicSourceId);
|
|
18443
|
+
const curriculumText = topicRow.content ?? "";
|
|
18444
|
+
const cards = await importCurriculumViaLLM(
|
|
18445
|
+
db,
|
|
18446
|
+
curriculumText,
|
|
18447
|
+
opts.domain,
|
|
18448
|
+
item.uri,
|
|
18449
|
+
{ knowledgeContext: firstContext }
|
|
18450
|
+
);
|
|
18451
|
+
if (cards.length === 0) {
|
|
18452
|
+
jsonError(`No cards were generated for ${item.topicId}`);
|
|
18453
|
+
}
|
|
18454
|
+
const proposals = cards.map((card) => ({
|
|
18455
|
+
question: card.question,
|
|
18456
|
+
concept: card.concept,
|
|
18457
|
+
domain: card.domain,
|
|
18458
|
+
title: card.title,
|
|
18459
|
+
bloom_level: card.bloom_level,
|
|
18460
|
+
symbiosis_mode: card.symbiosis_mode || "none",
|
|
18461
|
+
excerpt: card.context || "",
|
|
18462
|
+
page_number: null,
|
|
18463
|
+
provider: provider.id,
|
|
18464
|
+
topic_id: item.topicId
|
|
18465
|
+
}));
|
|
18466
|
+
const result = await confirmSourceImport(
|
|
18467
|
+
db,
|
|
18468
|
+
userId,
|
|
18469
|
+
item.sourceId,
|
|
18470
|
+
proposals
|
|
18471
|
+
);
|
|
18472
|
+
await assignConfirmedProposalContexts(db, proposals, contexts);
|
|
18473
|
+
jsonOut2({
|
|
18474
|
+
success: true,
|
|
18475
|
+
topicId: item.topicId,
|
|
18476
|
+
proposalCount: proposals.length,
|
|
18477
|
+
createdCount: result.createdCount,
|
|
18478
|
+
ensuredCount: result.linkedCount
|
|
18479
|
+
});
|
|
18480
|
+
});
|
|
18481
|
+
});
|
|
18482
|
+
function proposalBaseSlug(domain, question, concept) {
|
|
18483
|
+
const baseText = question?.trim() ? question : concept;
|
|
18484
|
+
const cleanDomain = slugify(domain || "");
|
|
18485
|
+
const cleanBase = slugify(baseText);
|
|
18486
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
18487
|
+
if (baseSlug.length > 60) {
|
|
18488
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
18489
|
+
}
|
|
18490
|
+
return baseSlug || "token";
|
|
18491
|
+
}
|
|
18492
|
+
async function executeCurriculumConfirmOperations(db, userId, operations, contexts) {
|
|
18493
|
+
let createdCount = 0;
|
|
18494
|
+
let ensuredCount = 0;
|
|
18495
|
+
let removedCount = 0;
|
|
18496
|
+
await db.transaction(async (tx) => {
|
|
18497
|
+
for (const op of operations) {
|
|
18498
|
+
if (op.create.length > 0) {
|
|
18499
|
+
const result = await applySourceProposals(
|
|
18500
|
+
tx,
|
|
18501
|
+
userId,
|
|
18502
|
+
op.sourceId,
|
|
18503
|
+
op.create
|
|
18504
|
+
);
|
|
18505
|
+
createdCount += result.createdCount;
|
|
18506
|
+
ensuredCount += result.linkedCount;
|
|
18507
|
+
await assignConfirmedProposalContexts(tx, op.create, contexts);
|
|
18508
|
+
}
|
|
18509
|
+
for (const slug of op.removeSlugs) {
|
|
18510
|
+
if (await deleteCurriculumCardForUser(
|
|
18511
|
+
tx,
|
|
18512
|
+
userId,
|
|
18513
|
+
slug,
|
|
18514
|
+
op.provider,
|
|
18515
|
+
op.topicId
|
|
18516
|
+
)) {
|
|
18517
|
+
removedCount++;
|
|
18518
|
+
}
|
|
18519
|
+
}
|
|
18520
|
+
}
|
|
18521
|
+
});
|
|
18522
|
+
return { createdCount, ensuredCount, removedCount };
|
|
18523
|
+
}
|
|
18524
|
+
bridgeCommand.command("curriculum-list-subtopics").description(
|
|
18525
|
+
"List finer units inside a Lernbereich for chunked import (JSON)"
|
|
18526
|
+
).requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topic <json>", "Single topic node JSON").action(async (opts) => {
|
|
18527
|
+
try {
|
|
18528
|
+
const provider = getCurriculumProvider(opts.provider);
|
|
18529
|
+
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
18530
|
+
if (!provider.extractSubTopics) {
|
|
18531
|
+
jsonOut2({ success: true, subTopics: [] });
|
|
18532
|
+
return;
|
|
18533
|
+
}
|
|
18534
|
+
let topic;
|
|
18535
|
+
try {
|
|
18536
|
+
topic = JSON.parse(opts.topic);
|
|
18537
|
+
} catch {
|
|
18538
|
+
jsonError("Invalid --topic JSON");
|
|
18539
|
+
return;
|
|
18540
|
+
}
|
|
18541
|
+
const resolved = provider.resolveTopic(topic);
|
|
18542
|
+
const rawHtml = await fetchRawHtml(resolved.uri);
|
|
18543
|
+
const subTopics = provider.extractSubTopics(rawHtml, resolved.topicId).map(({ id, label, textLength }) => ({ id, label, textLength }));
|
|
18544
|
+
jsonOut2({ success: true, topicId: resolved.topicId, subTopics });
|
|
18545
|
+
} catch (err) {
|
|
18546
|
+
jsonError(err.message || String(err));
|
|
18547
|
+
}
|
|
18548
|
+
});
|
|
18549
|
+
bridgeCommand.command("curriculum-preview-topic").description(
|
|
18550
|
+
"Preview importable cards for one topic (existing + LLM proposals) (JSON)"
|
|
18551
|
+
).requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topic <json>", "Single topic node JSON").requiredOption("--domain <domain>", "Subject label for imported card domain").option("--subTopics <json>", "JSON array of sub-topic ids to include").option("--user <id>", "User ID (default: whoami)").option(
|
|
18552
|
+
"--knowledge-context <context>",
|
|
18553
|
+
"Knowledge context for LLM prompt (repeatable)",
|
|
18554
|
+
(val, memo) => {
|
|
18555
|
+
memo.push(val);
|
|
18556
|
+
return memo;
|
|
18557
|
+
},
|
|
18558
|
+
[]
|
|
18559
|
+
).action(async (opts) => {
|
|
18560
|
+
await withDb2(async (db) => {
|
|
18561
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
18562
|
+
const provider = getCurriculumProvider(opts.provider);
|
|
18563
|
+
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
18564
|
+
let topic;
|
|
18565
|
+
try {
|
|
18566
|
+
topic = JSON.parse(opts.topic);
|
|
18567
|
+
} catch {
|
|
18568
|
+
jsonError("Invalid --topic JSON");
|
|
18569
|
+
return;
|
|
18570
|
+
}
|
|
18571
|
+
const resolved = provider.resolveTopic(topic);
|
|
18572
|
+
const extracted = await extractAndStoreCurriculumTopics(db, provider, [
|
|
18573
|
+
topic
|
|
18574
|
+
]);
|
|
18575
|
+
const item = extracted[0];
|
|
18576
|
+
if (!item || item.textLength === 0) {
|
|
18577
|
+
jsonError(`No curriculum text extracted for topic "${topic.id}"`);
|
|
18578
|
+
}
|
|
18579
|
+
const contextNames = parseKnowledgeContextNames2(
|
|
18580
|
+
opts.knowledgeContext || []
|
|
18581
|
+
);
|
|
18582
|
+
const contexts = await resolveKnowledgeContexts2(db, contextNames);
|
|
18583
|
+
const firstContext = contexts[0]?.name;
|
|
18584
|
+
let subTopicIds;
|
|
18585
|
+
if (opts.subTopics) {
|
|
18586
|
+
try {
|
|
18587
|
+
subTopicIds = JSON.parse(opts.subTopics);
|
|
18588
|
+
} catch {
|
|
18589
|
+
jsonError("Invalid --subTopics JSON");
|
|
18590
|
+
return;
|
|
18591
|
+
}
|
|
18592
|
+
}
|
|
18593
|
+
const rawHtml = await fetchRawHtml(item.uri);
|
|
18594
|
+
const chunks = [];
|
|
18595
|
+
if (provider.extractSubTopics) {
|
|
18596
|
+
const subTopics = provider.extractSubTopics(rawHtml, resolved.topicId);
|
|
18597
|
+
const selected = subTopicIds && subTopicIds.length > 0 ? subTopics.filter((st) => subTopicIds.includes(st.id)) : subTopics;
|
|
18598
|
+
if (selected.length > 0) {
|
|
18599
|
+
for (const st of selected) {
|
|
18600
|
+
chunks.push({ subTopicId: st.id, text: st.text });
|
|
18601
|
+
}
|
|
18602
|
+
}
|
|
18603
|
+
}
|
|
18604
|
+
if (chunks.length === 0) {
|
|
18605
|
+
const topicRow = await db.prepare("SELECT content FROM sources WHERE id = ?").get(item.topicSourceId);
|
|
18606
|
+
chunks.push({
|
|
18607
|
+
subTopicId: null,
|
|
18608
|
+
text: topicRow.content ?? ""
|
|
18609
|
+
});
|
|
18610
|
+
}
|
|
18611
|
+
const existingCards = await listUserCardsForCurriculumTopic(
|
|
18612
|
+
db,
|
|
18613
|
+
userId,
|
|
18614
|
+
provider.id,
|
|
18615
|
+
resolved.topicId
|
|
18616
|
+
);
|
|
18617
|
+
const existingSlugs = new Set(existingCards.map((c) => c.slug));
|
|
18618
|
+
const items = [];
|
|
18619
|
+
for (const card of existingCards) {
|
|
18620
|
+
items.push({
|
|
18621
|
+
id: `existing:${card.slug}`,
|
|
18622
|
+
slug: card.slug,
|
|
18623
|
+
question: card.question ?? "",
|
|
18624
|
+
concept: card.concept,
|
|
18625
|
+
domain: card.domain,
|
|
18626
|
+
bloom_level: card.bloomLevel,
|
|
18627
|
+
symbiosis_mode: card.symbiosisMode ?? "none",
|
|
18628
|
+
excerpt: "",
|
|
18629
|
+
isExisting: true,
|
|
18630
|
+
selected: true,
|
|
18631
|
+
subTopicId: card.topicId?.includes("@") ? card.topicId.split("@").pop() ?? null : null,
|
|
18632
|
+
parentTopicId: resolved.topicId
|
|
18633
|
+
});
|
|
18634
|
+
}
|
|
18635
|
+
let proposalIndex = 0;
|
|
18636
|
+
for (const chunk of chunks) {
|
|
18637
|
+
if (!chunk.text.trim()) continue;
|
|
18638
|
+
const cards = await importCurriculumViaLLM(
|
|
18639
|
+
db,
|
|
18640
|
+
chunk.text,
|
|
18641
|
+
opts.domain,
|
|
18642
|
+
item.uri,
|
|
18643
|
+
{ knowledgeContext: firstContext }
|
|
18644
|
+
);
|
|
18645
|
+
const scopedTopicId = chunk.subTopicId ? `${resolved.topicId}@${chunk.subTopicId}` : resolved.topicId;
|
|
18646
|
+
for (const card of cards) {
|
|
18647
|
+
const slug = proposalBaseSlug(
|
|
18648
|
+
card.domain,
|
|
18649
|
+
card.question,
|
|
18650
|
+
card.concept
|
|
18651
|
+
);
|
|
18652
|
+
if (existingSlugs.has(slug)) {
|
|
18653
|
+
continue;
|
|
18654
|
+
}
|
|
18655
|
+
const proposal = {
|
|
18656
|
+
question: card.question,
|
|
18657
|
+
concept: card.concept,
|
|
18658
|
+
domain: card.domain,
|
|
18659
|
+
bloom_level: card.bloom_level,
|
|
18660
|
+
symbiosis_mode: card.symbiosis_mode || "none",
|
|
18661
|
+
excerpt: card.context || "",
|
|
18662
|
+
page_number: null,
|
|
18663
|
+
provider: provider.id,
|
|
18664
|
+
topic_id: scopedTopicId
|
|
18665
|
+
};
|
|
18666
|
+
items.push({
|
|
18667
|
+
id: `new:${proposalIndex++}`,
|
|
18668
|
+
slug: null,
|
|
18669
|
+
question: proposal.question,
|
|
18670
|
+
concept: proposal.concept,
|
|
18671
|
+
domain: proposal.domain,
|
|
18672
|
+
bloom_level: proposal.bloom_level,
|
|
18673
|
+
symbiosis_mode: proposal.symbiosis_mode,
|
|
18674
|
+
excerpt: proposal.excerpt,
|
|
18675
|
+
isExisting: false,
|
|
18676
|
+
selected: false,
|
|
18677
|
+
subTopicId: chunk.subTopicId,
|
|
18678
|
+
parentTopicId: resolved.topicId,
|
|
18679
|
+
proposal
|
|
17112
18680
|
});
|
|
17113
18681
|
}
|
|
17114
18682
|
}
|
|
18683
|
+
if (items.length === 0) {
|
|
18684
|
+
jsonError(`No importable cards for topic "${topic.id}"`);
|
|
18685
|
+
}
|
|
18686
|
+
jsonOut2({
|
|
18687
|
+
success: true,
|
|
18688
|
+
topicId: resolved.topicId,
|
|
18689
|
+
sourceId: item.sourceId,
|
|
18690
|
+
items
|
|
18691
|
+
});
|
|
18692
|
+
});
|
|
18693
|
+
});
|
|
18694
|
+
bridgeCommand.command("curriculum-confirm-topic").description(
|
|
18695
|
+
"Create selected new cards and remove deselected existing cards (JSON)"
|
|
18696
|
+
).requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topicId <id>", "Resolved curriculum topic id").requiredOption("--sourceId <id>", "Parent page source database ID").requiredOption(
|
|
18697
|
+
"--create <json>",
|
|
18698
|
+
"JSON array of SourceProposalInput objects to import"
|
|
18699
|
+
).requiredOption(
|
|
18700
|
+
"--removeSlugs <json>",
|
|
18701
|
+
"JSON array of token slugs whose user cards should be deleted"
|
|
18702
|
+
).option("--user <id>", "User ID (default: whoami)").option(
|
|
18703
|
+
"--knowledge-context <context>",
|
|
18704
|
+
"Assign confirmed tokens to a knowledge context (repeatable)",
|
|
18705
|
+
(val, memo) => {
|
|
18706
|
+
memo.push(val);
|
|
18707
|
+
return memo;
|
|
18708
|
+
},
|
|
18709
|
+
[]
|
|
18710
|
+
).action(async (opts) => {
|
|
18711
|
+
await withDb2(async (db) => {
|
|
18712
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
18713
|
+
const provider = getCurriculumProvider(opts.provider);
|
|
18714
|
+
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
18715
|
+
let create;
|
|
18716
|
+
let removeSlugs;
|
|
18717
|
+
try {
|
|
18718
|
+
create = JSON.parse(opts.create);
|
|
18719
|
+
removeSlugs = JSON.parse(opts.removeSlugs);
|
|
18720
|
+
} catch {
|
|
18721
|
+
jsonError("Invalid --create or --removeSlugs JSON");
|
|
18722
|
+
return;
|
|
18723
|
+
}
|
|
18724
|
+
const contextNames = parseKnowledgeContextNames2(
|
|
18725
|
+
opts.knowledgeContext || []
|
|
18726
|
+
);
|
|
18727
|
+
const contexts = await resolveKnowledgeContexts2(db, contextNames);
|
|
18728
|
+
try {
|
|
18729
|
+
const result = await executeCurriculumConfirmOperations(
|
|
18730
|
+
db,
|
|
18731
|
+
userId,
|
|
18732
|
+
[
|
|
18733
|
+
{
|
|
18734
|
+
sourceId: opts.sourceId,
|
|
18735
|
+
provider: provider.id,
|
|
18736
|
+
topicId: opts.topicId,
|
|
18737
|
+
create,
|
|
18738
|
+
removeSlugs
|
|
18739
|
+
}
|
|
18740
|
+
],
|
|
18741
|
+
contexts
|
|
18742
|
+
);
|
|
18743
|
+
jsonOut2({ success: true, ...result });
|
|
18744
|
+
} catch (err) {
|
|
18745
|
+
jsonError(err.message || String(err));
|
|
18746
|
+
}
|
|
18747
|
+
});
|
|
18748
|
+
});
|
|
18749
|
+
bridgeCommand.command("curriculum-confirm-batch").description("Atomically confirm multiple curriculum topic selections (JSON)").requiredOption(
|
|
18750
|
+
"--operations <json>",
|
|
18751
|
+
"JSON array of {sourceId, provider, topicId, create, removeSlugs}"
|
|
18752
|
+
).option("--user <id>", "User ID (default: whoami)").option(
|
|
18753
|
+
"--knowledge-context <context>",
|
|
18754
|
+
"Assign confirmed tokens to a knowledge context (repeatable)",
|
|
18755
|
+
(val, memo) => {
|
|
18756
|
+
memo.push(val);
|
|
18757
|
+
return memo;
|
|
18758
|
+
},
|
|
18759
|
+
[]
|
|
18760
|
+
).action(async (opts) => {
|
|
18761
|
+
await withDb2(async (db) => {
|
|
18762
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
18763
|
+
let operations;
|
|
18764
|
+
try {
|
|
18765
|
+
operations = JSON.parse(opts.operations);
|
|
18766
|
+
} catch {
|
|
18767
|
+
jsonError("Invalid --operations JSON");
|
|
18768
|
+
return;
|
|
18769
|
+
}
|
|
18770
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
18771
|
+
jsonError("--operations must be a non-empty JSON array");
|
|
18772
|
+
}
|
|
18773
|
+
for (const op of operations) {
|
|
18774
|
+
if (!op?.sourceId || !op?.provider || !op?.topicId) {
|
|
18775
|
+
jsonError("Each operation requires sourceId, provider, and topicId");
|
|
18776
|
+
}
|
|
18777
|
+
if (!getCurriculumProvider(op.provider)) {
|
|
18778
|
+
jsonError(`Unknown curriculum provider: ${op.provider}`);
|
|
18779
|
+
}
|
|
18780
|
+
if (!Array.isArray(op.create) || !Array.isArray(op.removeSlugs)) {
|
|
18781
|
+
jsonError("Each operation requires create and removeSlugs arrays");
|
|
18782
|
+
}
|
|
18783
|
+
}
|
|
18784
|
+
const contextNames = parseKnowledgeContextNames2(
|
|
18785
|
+
opts.knowledgeContext || []
|
|
18786
|
+
);
|
|
18787
|
+
const contexts = await resolveKnowledgeContexts2(db, contextNames);
|
|
18788
|
+
try {
|
|
18789
|
+
const result = await executeCurriculumConfirmOperations(
|
|
18790
|
+
db,
|
|
18791
|
+
userId,
|
|
18792
|
+
operations,
|
|
18793
|
+
contexts
|
|
18794
|
+
);
|
|
18795
|
+
jsonOut2({ success: true, ...result });
|
|
18796
|
+
} catch (err) {
|
|
18797
|
+
jsonError(err.message || String(err));
|
|
18798
|
+
}
|
|
18799
|
+
});
|
|
18800
|
+
});
|
|
18801
|
+
bridgeCommand.command("curriculum-extract-topics").description(
|
|
18802
|
+
"Fetch and extract specific texts for selected curriculum topics (JSON)"
|
|
18803
|
+
).requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topics <json>", "JSON array of selected topic nodes").action(async (opts) => {
|
|
18804
|
+
await withDb2(async (db) => {
|
|
18805
|
+
const provider = getCurriculumProvider(opts.provider);
|
|
18806
|
+
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
18807
|
+
let topics;
|
|
18808
|
+
try {
|
|
18809
|
+
topics = JSON.parse(opts.topics);
|
|
18810
|
+
} catch {
|
|
18811
|
+
jsonError("Invalid --topics JSON");
|
|
18812
|
+
return;
|
|
18813
|
+
}
|
|
18814
|
+
const extracted = await extractAndStoreCurriculumTopics(
|
|
18815
|
+
db,
|
|
18816
|
+
provider,
|
|
18817
|
+
topics
|
|
18818
|
+
);
|
|
17115
18819
|
jsonOut2({ success: true, extracted });
|
|
17116
18820
|
});
|
|
17117
18821
|
});
|
|
@@ -17345,6 +19049,9 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
17345
19049
|
output: process.stdout,
|
|
17346
19050
|
terminal: false
|
|
17347
19051
|
});
|
|
19052
|
+
process.on("unhandledRejection", (reason) => {
|
|
19053
|
+
logDiag(`unhandledRejection: ${String(reason)}`);
|
|
19054
|
+
});
|
|
17348
19055
|
let pending = Promise.resolve();
|
|
17349
19056
|
rl.on("line", (line) => {
|
|
17350
19057
|
if (!line.trim()) return;
|
|
@@ -17352,6 +19059,15 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
17352
19059
|
const response = await processRequest(line);
|
|
17353
19060
|
process.stdout.write(`${response}
|
|
17354
19061
|
`);
|
|
19062
|
+
}).catch((err) => {
|
|
19063
|
+
logDiag(`serve request failed: ${err.message || err}`);
|
|
19064
|
+
try {
|
|
19065
|
+
process.stdout.write(
|
|
19066
|
+
`${JSON.stringify({ id: null, error: err.message || String(err) })}
|
|
19067
|
+
`
|
|
19068
|
+
);
|
|
19069
|
+
} catch {
|
|
19070
|
+
}
|
|
17355
19071
|
});
|
|
17356
19072
|
});
|
|
17357
19073
|
});
|
|
@@ -21227,9 +22943,49 @@ async function activateMachineProviderConfig(db) {
|
|
|
21227
22943
|
` activate ${providerCount} machine-local provider(s) from ~/.zam/config.json`
|
|
21228
22944
|
);
|
|
21229
22945
|
}
|
|
22946
|
+
function connectDetectedAgentHarnesses(skip, dryRun) {
|
|
22947
|
+
if (skip) return;
|
|
22948
|
+
try {
|
|
22949
|
+
const report = performAgentConnect({ dryRun });
|
|
22950
|
+
if (report.detected.length === 0) {
|
|
22951
|
+
console.log(
|
|
22952
|
+
" skip no supported agent harness detected (zam agent connect <harness> configures one explicitly)"
|
|
22953
|
+
);
|
|
22954
|
+
return;
|
|
22955
|
+
}
|
|
22956
|
+
for (const result of report.results) {
|
|
22957
|
+
if (result.error) {
|
|
22958
|
+
console.warn(` warn ${result.harness}: ${result.error}`);
|
|
22959
|
+
} else if (dryRun) {
|
|
22960
|
+
console.log(
|
|
22961
|
+
` plan ${result.harness}: would ensure MCP config at ${result.path}`
|
|
22962
|
+
);
|
|
22963
|
+
} else if (result.alreadyConfigured) {
|
|
22964
|
+
console.log(
|
|
22965
|
+
` skip ${result.harness}: MCP already configured (${result.path})`
|
|
22966
|
+
);
|
|
22967
|
+
} else {
|
|
22968
|
+
console.log(
|
|
22969
|
+
` wire ${result.harness}: MCP configured (${result.path})`
|
|
22970
|
+
);
|
|
22971
|
+
}
|
|
22972
|
+
}
|
|
22973
|
+
if (report.skills) {
|
|
22974
|
+
console.log(
|
|
22975
|
+
` wire global ZAM skill: ${report.skills.refreshed}/${report.skills.total} locations`
|
|
22976
|
+
);
|
|
22977
|
+
}
|
|
22978
|
+
} catch (err) {
|
|
22979
|
+
console.warn(` warn agent connect: ${err.message}`);
|
|
22980
|
+
}
|
|
22981
|
+
}
|
|
21230
22982
|
var setupCommand = new Command18("setup").description(
|
|
21231
22983
|
"Link ZAM skill directories into this workspace and initialize the database"
|
|
21232
|
-
).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option(
|
|
22984
|
+
).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option(
|
|
22985
|
+
"--skip-agent-connect",
|
|
22986
|
+
"skip wiring detected agent harnesses via MCP",
|
|
22987
|
+
false
|
|
22988
|
+
).option("--target <path>", "repository/workspace directory to set up").option(
|
|
21233
22989
|
"--agents <list>",
|
|
21234
22990
|
"comma-separated agents to wire: all, claude, copilot, codex, agent"
|
|
21235
22991
|
).option(
|
|
@@ -21256,6 +23012,7 @@ var setupCommand = new Command18("setup").description(
|
|
|
21256
23012
|
dryRun: opts.dryRun
|
|
21257
23013
|
});
|
|
21258
23014
|
await initDatabase(opts.skipInit || opts.dryRun);
|
|
23015
|
+
connectDetectedAgentHarnesses(opts.skipAgentConnect, opts.dryRun);
|
|
21259
23016
|
if (agents.has("claude")) {
|
|
21260
23017
|
writeClaudeMd(opts.skipClaudeMd, target, {
|
|
21261
23018
|
dryRun: opts.dryRun,
|