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/commands/mcp.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 (
|
|
@@ -5481,20 +5536,20 @@ import { homedir as homedir5 } from "os";
|
|
|
5481
5536
|
import { join as join8 } from "path";
|
|
5482
5537
|
import { fileURLToPath } from "url";
|
|
5483
5538
|
function getPackageSkillPath(agent = "default") {
|
|
5484
|
-
const
|
|
5539
|
+
const packageRoot4 = [
|
|
5485
5540
|
fileURLToPath(new URL("../../..", import.meta.url)),
|
|
5486
5541
|
fileURLToPath(new URL("../..", import.meta.url)),
|
|
5487
5542
|
fileURLToPath(new URL("..", import.meta.url))
|
|
5488
5543
|
].find((candidate) => existsSync7(join8(candidate, "package.json"))) ?? "";
|
|
5489
|
-
if (!
|
|
5544
|
+
if (!packageRoot4) return "";
|
|
5490
5545
|
if (agent === "codex") {
|
|
5491
|
-
const codexPath = join8(
|
|
5546
|
+
const codexPath = join8(packageRoot4, ".agents", "skills", "zam", "SKILL.md");
|
|
5492
5547
|
if (existsSync7(codexPath)) return codexPath;
|
|
5493
5548
|
return "";
|
|
5494
5549
|
}
|
|
5495
5550
|
if (agent === "claude") {
|
|
5496
5551
|
const claudePath = join8(
|
|
5497
|
-
|
|
5552
|
+
packageRoot4,
|
|
5498
5553
|
".claude",
|
|
5499
5554
|
"skills",
|
|
5500
5555
|
"zam",
|
|
@@ -5503,9 +5558,9 @@ function getPackageSkillPath(agent = "default") {
|
|
|
5503
5558
|
if (existsSync7(claudePath)) return claudePath;
|
|
5504
5559
|
return "";
|
|
5505
5560
|
}
|
|
5506
|
-
let path = join8(
|
|
5561
|
+
let path = join8(packageRoot4, ".agent", "skills", "zam", "SKILL.md");
|
|
5507
5562
|
if (existsSync7(path)) return path;
|
|
5508
|
-
path = join8(
|
|
5563
|
+
path = join8(packageRoot4, ".claude", "skills", "zam", "SKILL.md");
|
|
5509
5564
|
if (existsSync7(path)) return path;
|
|
5510
5565
|
return "";
|
|
5511
5566
|
}
|
|
@@ -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,
|
|
@@ -6809,9 +7016,9 @@ var init_kernel = __esm({
|
|
|
6809
7016
|
|
|
6810
7017
|
// src/cli/commands/mcp.ts
|
|
6811
7018
|
init_kernel();
|
|
6812
|
-
import { existsSync as
|
|
6813
|
-
import { dirname as
|
|
6814
|
-
import { fileURLToPath as
|
|
7019
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
|
|
7020
|
+
import { dirname as dirname11, join as join23 } from "path";
|
|
7021
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
6815
7022
|
import {
|
|
6816
7023
|
RESOURCE_MIME_TYPE,
|
|
6817
7024
|
registerAppResource,
|
|
@@ -6852,8 +7059,11 @@ import { spawn } from "child_process";
|
|
|
6852
7059
|
import { existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
|
|
6853
7060
|
var DEFAULT_LLM_URL = "http://localhost:8000/v1";
|
|
6854
7061
|
var DEFAULT_LLM_MAX_TOKENS = 1e4;
|
|
7062
|
+
var LOCAL_CURRICULUM_IMPORT_HARD_TIMEOUT_MS = 6e5;
|
|
7063
|
+
var CLOUD_CURRICULUM_IMPORT_HARD_TIMEOUT_MS = 18e4;
|
|
6855
7064
|
var RECALL_QUESTION_MAX_OUTPUT_TOKENS = 400;
|
|
6856
7065
|
var RECALL_EVALUATION_MAX_OUTPUT_TOKENS = 1200;
|
|
7066
|
+
var RECALL_DISCUSSION_MAX_OUTPUT_TOKENS = 1200;
|
|
6857
7067
|
var RECALL_ENDPOINT_CACHE_MS = 6e4;
|
|
6858
7068
|
var cachedRecallEndpoint = null;
|
|
6859
7069
|
function recallEndpointSignature(cfg) {
|
|
@@ -6927,7 +7137,58 @@ function resolveProviderApiKey(rec) {
|
|
|
6927
7137
|
}
|
|
6928
7138
|
return DEFAULT_LLM_API_KEY;
|
|
6929
7139
|
}
|
|
7140
|
+
var ROLE_TO_CAPABILITY2 = {
|
|
7141
|
+
recall: "text",
|
|
7142
|
+
text: "text",
|
|
7143
|
+
vision: "image",
|
|
7144
|
+
embedding: "embedding"
|
|
7145
|
+
};
|
|
7146
|
+
function materializeModelEntry(entry, base, enabled, maxFrames) {
|
|
7147
|
+
const url = entry.url || base.url;
|
|
7148
|
+
const cfg = {
|
|
7149
|
+
enabled,
|
|
7150
|
+
url,
|
|
7151
|
+
model: entry.model || base.model,
|
|
7152
|
+
apiKey: entry.apiKeyRef ? getProviderApiKey(entry.apiKeyRef) ?? DEFAULT_LLM_API_KEY : DEFAULT_LLM_API_KEY,
|
|
7153
|
+
apiFlavor: entry.apiFlavor || inferApiFlavor(url),
|
|
7154
|
+
locale: base.locale,
|
|
7155
|
+
providerName: entry.id,
|
|
7156
|
+
label: entry.label,
|
|
7157
|
+
source: "machine",
|
|
7158
|
+
local: entry.local
|
|
7159
|
+
};
|
|
7160
|
+
if (entry.runner) cfg.runner = entry.runner;
|
|
7161
|
+
if (maxFrames !== void 0) cfg.maxFrames = maxFrames;
|
|
7162
|
+
return cfg;
|
|
7163
|
+
}
|
|
7164
|
+
async function resolveCapability(db, capability) {
|
|
7165
|
+
const models = getMachineAiModels();
|
|
7166
|
+
if (models.length === 0) return null;
|
|
7167
|
+
const isVisual = capability === "image" || capability === "video";
|
|
7168
|
+
const enabled = (isVisual ? await getSetting(db, "llm.vision.enabled") : await getSetting(db, "llm.enabled")) === "true";
|
|
7169
|
+
const base = await getLlmConfig(db);
|
|
7170
|
+
let maxFrames;
|
|
7171
|
+
if (isVisual) {
|
|
7172
|
+
const raw = await getSetting(db, "llm.vision.max_frames");
|
|
7173
|
+
const parsed = raw ? parseInt(raw, 10) : 100;
|
|
7174
|
+
maxFrames = Number.isNaN(parsed) ? 100 : parsed;
|
|
7175
|
+
}
|
|
7176
|
+
const eligible = [...models].sort((a, b) => a.order - b.order).filter(
|
|
7177
|
+
(entry) => entry.capabilities[capability] && entry.detectedCapabilities[capability]
|
|
7178
|
+
);
|
|
7179
|
+
if (eligible.length === 0) return null;
|
|
7180
|
+
const configs = eligible.map(
|
|
7181
|
+
(entry) => materializeModelEntry(entry, base, enabled, maxFrames)
|
|
7182
|
+
);
|
|
7183
|
+
for (let i = configs.length - 1; i > 0; i--) {
|
|
7184
|
+
configs[i - 1] = { ...configs[i - 1], fallback: configs[i] };
|
|
7185
|
+
}
|
|
7186
|
+
return configs[0];
|
|
7187
|
+
}
|
|
6930
7188
|
async function getProviderForRole(db, role) {
|
|
7189
|
+
ensureMachineProviderRolesSanitized();
|
|
7190
|
+
const viaRegistry = await resolveCapability(db, ROLE_TO_CAPABILITY2[role]);
|
|
7191
|
+
if (viaRegistry) return viaRegistry;
|
|
6931
7192
|
const enabled = role === "vision" ? await getSetting(db, "llm.vision.enabled") === "true" : await getSetting(db, "llm.enabled") === "true";
|
|
6932
7193
|
const base = await getLegacyRoleConfig(db, role, enabled);
|
|
6933
7194
|
const providers = await readJsonSetting(db, "llm.providers");
|
|
@@ -6968,6 +7229,9 @@ async function getProviderForRole(db, role) {
|
|
|
6968
7229
|
) : void 0;
|
|
6969
7230
|
return { ...primary, fallback };
|
|
6970
7231
|
}
|
|
7232
|
+
if (role === "text") {
|
|
7233
|
+
return getProviderForRole(db, "recall");
|
|
7234
|
+
}
|
|
6971
7235
|
return resolved;
|
|
6972
7236
|
}
|
|
6973
7237
|
function materializeProvider(rec, base, role, meta) {
|
|
@@ -7216,6 +7480,64 @@ Evaluation:`;
|
|
|
7216
7480
|
providerName: endpoint.providerName
|
|
7217
7481
|
};
|
|
7218
7482
|
}
|
|
7483
|
+
async function discussReviewViaLLM(db, input) {
|
|
7484
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
7485
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
7486
|
+
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
7487
|
+
const systemPrompt = `You are ZAM, a warm, precise, and encouraging skills trainer in a follow-up discussion about one flashcard.
|
|
7488
|
+
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.
|
|
7489
|
+
|
|
7490
|
+
Guidelines:
|
|
7491
|
+
1. Answer the learner's follow-up directly and concretely in ${langName}, grounded in the card's target concept, context, and source reference.
|
|
7492
|
+
2. Stay scoped to this card and its concept. If the learner drifts to unrelated territory, answer briefly and steer back to the concept.
|
|
7493
|
+
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.
|
|
7494
|
+
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.`;
|
|
7495
|
+
const cardFrame = `The card under discussion:
|
|
7496
|
+
Domain: ${input.domain}
|
|
7497
|
+
Slug: ${input.slug}
|
|
7498
|
+
Recall Question: ${input.question}
|
|
7499
|
+
Learner's Answer: ${input.userAnswer}
|
|
7500
|
+
|
|
7501
|
+
Target Concept (Correct Answer): ${input.concept}
|
|
7502
|
+
Target Context: ${input.context || "(none)"}
|
|
7503
|
+
${input.sourceLinkContent ? `Source Code Reference:
|
|
7504
|
+
${input.sourceLinkContent}` : ""}`;
|
|
7505
|
+
const messages = [
|
|
7506
|
+
{ role: "system", content: systemPrompt },
|
|
7507
|
+
{ role: "user", content: cardFrame }
|
|
7508
|
+
];
|
|
7509
|
+
const feedback = input.feedback?.trim();
|
|
7510
|
+
if (feedback) {
|
|
7511
|
+
messages.push({ role: "assistant", content: feedback });
|
|
7512
|
+
}
|
|
7513
|
+
for (const turn of input.thread) {
|
|
7514
|
+
messages.push({ role: turn.role, content: turn.content });
|
|
7515
|
+
}
|
|
7516
|
+
messages.push({ role: "user", content: input.message });
|
|
7517
|
+
const res = await fetchWithInteractiveTimeout(
|
|
7518
|
+
`${endpoint.url}/chat/completions`,
|
|
7519
|
+
{
|
|
7520
|
+
method: "POST",
|
|
7521
|
+
headers: {
|
|
7522
|
+
"Content-Type": "application/json",
|
|
7523
|
+
Authorization: `Bearer ${endpoint.apiKey}`
|
|
7524
|
+
},
|
|
7525
|
+
body: JSON.stringify({
|
|
7526
|
+
model: endpoint.model,
|
|
7527
|
+
messages,
|
|
7528
|
+
temperature: 0.3,
|
|
7529
|
+
max_tokens: RECALL_DISCUSSION_MAX_OUTPUT_TOKENS
|
|
7530
|
+
}),
|
|
7531
|
+
locale: cfg.locale
|
|
7532
|
+
}
|
|
7533
|
+
);
|
|
7534
|
+
const text = await readChatContent(res, "LLM discussion");
|
|
7535
|
+
return {
|
|
7536
|
+
text,
|
|
7537
|
+
model: endpoint.model,
|
|
7538
|
+
providerName: endpoint.providerName
|
|
7539
|
+
};
|
|
7540
|
+
}
|
|
7219
7541
|
var MAX_IMPORT_TEXT_CHARS = 2e5;
|
|
7220
7542
|
var VALID_GENERATED_MODES = /* @__PURE__ */ new Set(["shadowing", "copilot", "autonomy"]);
|
|
7221
7543
|
function parseGeneratedCardArray(responseText, label, limits) {
|
|
@@ -7350,6 +7672,7 @@ Target Category: ${targetCategory}
|
|
|
7350
7672
|
${sourceUrl ? `Source Reference Link: ${sourceUrl}` : ""}
|
|
7351
7673
|
|
|
7352
7674
|
JSON Array Output:`;
|
|
7675
|
+
const hardTimeoutMs = isLocalEndpoint(endpoint.url) ? LOCAL_CURRICULUM_IMPORT_HARD_TIMEOUT_MS : CLOUD_CURRICULUM_IMPORT_HARD_TIMEOUT_MS;
|
|
7353
7676
|
const res = await fetchWithInteractiveTimeout(
|
|
7354
7677
|
`${endpoint.url}/chat/completions`,
|
|
7355
7678
|
{
|
|
@@ -7367,7 +7690,9 @@ JSON Array Output:`;
|
|
|
7367
7690
|
temperature: 0.1,
|
|
7368
7691
|
max_tokens: DEFAULT_LLM_MAX_TOKENS
|
|
7369
7692
|
}),
|
|
7370
|
-
locale
|
|
7693
|
+
locale,
|
|
7694
|
+
hardTimeoutMs,
|
|
7695
|
+
timeoutMs: hardTimeoutMs
|
|
7371
7696
|
}
|
|
7372
7697
|
);
|
|
7373
7698
|
return readChatContent(res, "LLM curriculum import");
|
|
@@ -7763,7 +8088,11 @@ async function resolveUsableTextEndpoint(db) {
|
|
|
7763
8088
|
const chain = await checkProviderChain(cfg);
|
|
7764
8089
|
const selected = chain.firstUsable;
|
|
7765
8090
|
if (!selected || !isEndpointUsable(selected)) {
|
|
7766
|
-
|
|
8091
|
+
const primary = chain.primary.endpoint;
|
|
8092
|
+
const reason = !selected ? chain.primary.online ? "configured model is unavailable" : "endpoint is offline" : "endpoint is not usable";
|
|
8093
|
+
throw new Error(
|
|
8094
|
+
`No text LLM endpoint is online (${reason}; role: text; url: ${primary.url}; model: ${primary.model})`
|
|
8095
|
+
);
|
|
7767
8096
|
}
|
|
7768
8097
|
return selected.endpoint;
|
|
7769
8098
|
}
|
|
@@ -7803,7 +8132,7 @@ async function prepareRecallChain(db, opts) {
|
|
|
7803
8132
|
} else {
|
|
7804
8133
|
spawnLocalRunner(endpoint.url, endpoint.model, endpoint.runner);
|
|
7805
8134
|
while (Date.now() < deadline) {
|
|
7806
|
-
await new Promise((
|
|
8135
|
+
await new Promise((resolve6) => setTimeout(resolve6, 1e3));
|
|
7807
8136
|
if (await isLlmOnline(endpoint.url)) {
|
|
7808
8137
|
online = true;
|
|
7809
8138
|
break;
|
|
@@ -8096,7 +8425,7 @@ async function startLocalRunner(url, model, locale, hint) {
|
|
|
8096
8425
|
let attempts = 0;
|
|
8097
8426
|
const dotsPerLine = 30;
|
|
8098
8427
|
while (true) {
|
|
8099
|
-
await new Promise((
|
|
8428
|
+
await new Promise((resolve6) => setTimeout(resolve6, 500));
|
|
8100
8429
|
if (await isLlmOnline(url)) {
|
|
8101
8430
|
if (attempts > 0) process.stdout.write("\n");
|
|
8102
8431
|
return true;
|
|
@@ -8156,8 +8485,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
|
8156
8485
|
const dotsPerLine = 30;
|
|
8157
8486
|
while (true) {
|
|
8158
8487
|
let timeoutId;
|
|
8159
|
-
const timeoutPromise = new Promise((
|
|
8160
|
-
timeoutId = setTimeout(() =>
|
|
8488
|
+
const timeoutPromise = new Promise((resolve6) => {
|
|
8489
|
+
timeoutId = setTimeout(() => resolve6("timeout"), timeoutMs);
|
|
8161
8490
|
});
|
|
8162
8491
|
const dotsInterval = setInterval(() => {
|
|
8163
8492
|
process.stdout.write(".");
|
|
@@ -9442,7 +9771,7 @@ async function updateCheck(params) {
|
|
|
9442
9771
|
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
9443
9772
|
import { homedir as homedir9 } from "os";
|
|
9444
9773
|
import { dirname as dirname6, join as join14 } from "path";
|
|
9445
|
-
import { ulid as
|
|
9774
|
+
import { ulid as ulid10 } from "ulid";
|
|
9446
9775
|
function getUiIntentPath(home = homedir9()) {
|
|
9447
9776
|
return join14(home, ".zam", "ui-intent.json");
|
|
9448
9777
|
}
|
|
@@ -9458,7 +9787,7 @@ function compactStringInput(input) {
|
|
|
9458
9787
|
}
|
|
9459
9788
|
async function writeUiIntent(app, input = {}, opts = {}) {
|
|
9460
9789
|
const path = opts.path ?? process.env.ZAM_UI_INTENT_PATH ?? getUiIntentPath();
|
|
9461
|
-
const id = opts.id ??
|
|
9790
|
+
const id = opts.id ?? ulid10();
|
|
9462
9791
|
const intent = {
|
|
9463
9792
|
version: 1,
|
|
9464
9793
|
id,
|
|
@@ -9501,13 +9830,13 @@ async function publishUiIntent(app, input = {}, opts = {}) {
|
|
|
9501
9830
|
|
|
9502
9831
|
// src/cli/commands/bridge.ts
|
|
9503
9832
|
init_kernel();
|
|
9504
|
-
import { execFileSync as
|
|
9833
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
9505
9834
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
9506
|
-
import { existsSync as
|
|
9507
|
-
import { homedir as
|
|
9508
|
-
import { join as
|
|
9835
|
+
import { existsSync as existsSync18, readdirSync as readdirSync2, readFileSync as readFileSync16, rmSync as rmSync3 } from "fs";
|
|
9836
|
+
import { homedir as homedir14, tmpdir as tmpdir3 } from "os";
|
|
9837
|
+
import { join as join22, resolve as resolve5 } from "path";
|
|
9509
9838
|
import { Command } from "commander";
|
|
9510
|
-
import { ulid as
|
|
9839
|
+
import { ulid as ulid11 } from "ulid";
|
|
9511
9840
|
|
|
9512
9841
|
// src/cli/adapters/source-reader.ts
|
|
9513
9842
|
import dns from "dns";
|
|
@@ -9584,7 +9913,7 @@ async function readWebLink(url) {
|
|
|
9584
9913
|
redirect: "manual",
|
|
9585
9914
|
signal: controller.signal,
|
|
9586
9915
|
headers: {
|
|
9587
|
-
"User-Agent": "ZAM-Content-Studio/0.10.
|
|
9916
|
+
"User-Agent": "ZAM-Content-Studio/0.10.4"
|
|
9588
9917
|
}
|
|
9589
9918
|
});
|
|
9590
9919
|
if (res.status >= 300 && res.status < 400) {
|
|
@@ -9647,6 +9976,12 @@ async function readImageOCR(db, imagePath) {
|
|
|
9647
9976
|
return extractTextFromScanViaLLM(db, imagePath);
|
|
9648
9977
|
}
|
|
9649
9978
|
|
|
9979
|
+
// src/cli/agent-connect.ts
|
|
9980
|
+
init_kernel();
|
|
9981
|
+
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
|
|
9982
|
+
import { homedir as homedir13 } from "os";
|
|
9983
|
+
import { dirname as dirname9 } from "path";
|
|
9984
|
+
|
|
9650
9985
|
// src/cli/agent-harness.ts
|
|
9651
9986
|
import { spawn as spawn2 } from "child_process";
|
|
9652
9987
|
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
|
|
@@ -9967,46 +10302,750 @@ function launchHarness(harness, opts) {
|
|
|
9967
10302
|
console.log(`Launched ${harness.label} in ${opts.workspace}`);
|
|
9968
10303
|
}
|
|
9969
10304
|
}
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
const
|
|
9976
|
-
|
|
10305
|
+
function detectInstalledConnectHarnesses(options = {}) {
|
|
10306
|
+
const home = options.home ?? homedir10();
|
|
10307
|
+
const platform = options.platform ?? process.platform;
|
|
10308
|
+
const find = options.find ?? findExecutable;
|
|
10309
|
+
const exists = options.exists ?? existsSync14;
|
|
10310
|
+
const detected = [];
|
|
10311
|
+
const hasCommandOrPath = (command, paths = []) => Boolean(find(command)) || paths.some((path) => exists(path));
|
|
10312
|
+
if (hasCommandOrPath("codex", [
|
|
10313
|
+
join16(home, ".codex"),
|
|
10314
|
+
...platform === "darwin" ? ["/Applications/Codex.app"] : platform === "win32" ? [join16(home, "AppData", "Local", "Programs", "Codex")] : []
|
|
10315
|
+
])) {
|
|
10316
|
+
detected.push("codex");
|
|
10317
|
+
}
|
|
10318
|
+
const vscodePaths = platform === "darwin" ? [
|
|
10319
|
+
"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
|
|
10320
|
+
join16(
|
|
10321
|
+
home,
|
|
10322
|
+
"Applications",
|
|
10323
|
+
"Visual Studio Code.app",
|
|
10324
|
+
"Contents",
|
|
10325
|
+
"Resources",
|
|
10326
|
+
"app",
|
|
10327
|
+
"bin",
|
|
10328
|
+
"code"
|
|
10329
|
+
)
|
|
10330
|
+
] : platform === "win32" ? [
|
|
10331
|
+
join16(
|
|
10332
|
+
home,
|
|
10333
|
+
"AppData",
|
|
10334
|
+
"Local",
|
|
10335
|
+
"Programs",
|
|
10336
|
+
"Microsoft VS Code",
|
|
10337
|
+
"bin",
|
|
10338
|
+
"code.cmd"
|
|
10339
|
+
)
|
|
10340
|
+
] : ["/usr/bin/code", "/usr/local/bin/code", "/snap/bin/code"];
|
|
10341
|
+
if (hasCommandOrPath("code", vscodePaths)) detected.push("vscode");
|
|
10342
|
+
const copilotHome = options.copilotHome ?? join16(home, ".copilot");
|
|
10343
|
+
if (hasCommandOrPath("copilot", [
|
|
10344
|
+
copilotHome,
|
|
10345
|
+
...platform === "darwin" ? ["/Applications/GitHub Copilot.app"] : []
|
|
10346
|
+
])) {
|
|
10347
|
+
detected.push("copilot");
|
|
10348
|
+
}
|
|
10349
|
+
if (hasCommandOrPath("opencode", [join16(home, ".config", "opencode")])) {
|
|
10350
|
+
detected.push("opencode");
|
|
10351
|
+
}
|
|
10352
|
+
if (hasCommandOrPath("goose", [join16(home, ".config", "goose")])) {
|
|
10353
|
+
detected.push("goose");
|
|
10354
|
+
}
|
|
10355
|
+
if (hasCommandOrPath("antigravity", [
|
|
10356
|
+
join16(home, ".gemini", "config"),
|
|
10357
|
+
...platform === "darwin" ? [
|
|
10358
|
+
join16(
|
|
10359
|
+
home,
|
|
10360
|
+
".antigravity-ide",
|
|
10361
|
+
"antigravity-ide",
|
|
10362
|
+
"bin",
|
|
10363
|
+
"antigravity-ide"
|
|
10364
|
+
),
|
|
10365
|
+
"/Applications/Antigravity.app",
|
|
10366
|
+
"/Applications/Antigravity IDE.app"
|
|
10367
|
+
] : []
|
|
10368
|
+
]) || find("antigravity-ide")) {
|
|
10369
|
+
detected.push("antigravity");
|
|
10370
|
+
}
|
|
10371
|
+
const claudeDesktopPath = platform === "darwin" ? "/Applications/Claude.app" : platform === "win32" ? join16(home, "AppData", "Local", "AnthropicClaude") : join16(home, ".config", "Claude");
|
|
10372
|
+
if (exists(claudeDesktopPath)) detected.push("claude-desktop");
|
|
10373
|
+
return detected;
|
|
10374
|
+
}
|
|
10375
|
+
function parseMcpJsonConfig(path, content) {
|
|
10376
|
+
let parsed;
|
|
9977
10377
|
try {
|
|
9978
|
-
|
|
9979
|
-
} catch {
|
|
9980
|
-
|
|
10378
|
+
parsed = JSON.parse(content);
|
|
10379
|
+
} catch (error) {
|
|
10380
|
+
throw new Error(
|
|
10381
|
+
`Cannot update ${path}: existing file is not valid JSON (${error instanceof Error ? error.message : String(error)})`
|
|
10382
|
+
);
|
|
10383
|
+
}
|
|
10384
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
10385
|
+
throw new Error(`Cannot update ${path}: expected a JSON object`);
|
|
9981
10386
|
}
|
|
10387
|
+
const config = parsed;
|
|
10388
|
+
if (config.mcpServers !== void 0 && (typeof config.mcpServers !== "object" || config.mcpServers === null || Array.isArray(config.mcpServers))) {
|
|
10389
|
+
throw new Error(`Cannot update ${path}: mcpServers must be a JSON object`);
|
|
10390
|
+
}
|
|
10391
|
+
return config;
|
|
9982
10392
|
}
|
|
9983
|
-
|
|
9984
|
-
|
|
10393
|
+
function connectHarnessMcp(harnessId, opts) {
|
|
10394
|
+
const exists = (p) => {
|
|
10395
|
+
if (opts.readFile) {
|
|
10396
|
+
try {
|
|
10397
|
+
opts.readFile(p);
|
|
10398
|
+
return true;
|
|
10399
|
+
} catch {
|
|
10400
|
+
return false;
|
|
10401
|
+
}
|
|
10402
|
+
}
|
|
10403
|
+
return existsSync14(p);
|
|
10404
|
+
};
|
|
10405
|
+
const read = (p) => {
|
|
10406
|
+
if (opts.readFile) return opts.readFile(p);
|
|
10407
|
+
return readFileSync11(p, "utf-8");
|
|
10408
|
+
};
|
|
10409
|
+
let targetPath = "";
|
|
10410
|
+
let content = "";
|
|
10411
|
+
let alreadyConfigured = false;
|
|
10412
|
+
let hint = "";
|
|
10413
|
+
const mergeMcpServersJson = (path) => {
|
|
10414
|
+
let existing = {};
|
|
10415
|
+
if (exists(path)) {
|
|
10416
|
+
existing = parseMcpJsonConfig(path, read(path));
|
|
10417
|
+
}
|
|
10418
|
+
if (!existing.mcpServers) {
|
|
10419
|
+
existing.mcpServers = {};
|
|
10420
|
+
}
|
|
10421
|
+
existing.mcpServers.zam = {
|
|
10422
|
+
command: opts.zamPath,
|
|
10423
|
+
args: ["mcp"]
|
|
10424
|
+
};
|
|
10425
|
+
return JSON.stringify(existing, null, 2);
|
|
10426
|
+
};
|
|
10427
|
+
if (harnessId === "claude-code") {
|
|
10428
|
+
targetPath = join16(opts.cwd, ".mcp.json");
|
|
10429
|
+
hint = "Claude Code will prompt you to approve the 'zam' MCP server on next launch.";
|
|
10430
|
+
content = mergeMcpServersJson(targetPath);
|
|
10431
|
+
} else if (harnessId === "claude-desktop") {
|
|
10432
|
+
const platform = opts.platform ?? process.platform;
|
|
10433
|
+
targetPath = platform === "win32" ? join16(
|
|
10434
|
+
opts.home,
|
|
10435
|
+
"AppData",
|
|
10436
|
+
"Roaming",
|
|
10437
|
+
"Claude",
|
|
10438
|
+
"claude_desktop_config.json"
|
|
10439
|
+
) : platform === "darwin" ? join16(
|
|
10440
|
+
opts.home,
|
|
10441
|
+
"Library",
|
|
10442
|
+
"Application Support",
|
|
10443
|
+
"Claude",
|
|
10444
|
+
"claude_desktop_config.json"
|
|
10445
|
+
) : join16(opts.home, ".config", "Claude", "claude_desktop_config.json");
|
|
10446
|
+
hint = "Restart Claude Desktop to load the 'zam' MCP server; MCP Apps panels render inline in the chat.";
|
|
10447
|
+
content = mergeMcpServersJson(targetPath);
|
|
10448
|
+
} else if (harnessId === "antigravity") {
|
|
10449
|
+
targetPath = join16(opts.home, ".gemini", "config", "mcp_config.json");
|
|
10450
|
+
hint = "Shared config read by Antigravity CLI and IDE (2.0+); older IDE builds read ~/.gemini/antigravity/mcp_config.json instead. Refresh Installed MCP Servers; the first tool call may still require approval.";
|
|
10451
|
+
let existing = {};
|
|
10452
|
+
if (exists(targetPath)) {
|
|
10453
|
+
existing = parseMcpJsonConfig(targetPath, read(targetPath));
|
|
10454
|
+
}
|
|
10455
|
+
if (!existing.mcpServers) {
|
|
10456
|
+
existing.mcpServers = {};
|
|
10457
|
+
}
|
|
10458
|
+
existing.mcpServers.zam = {
|
|
10459
|
+
command: opts.zamPath,
|
|
10460
|
+
args: ["mcp"]
|
|
10461
|
+
};
|
|
10462
|
+
content = JSON.stringify(existing, null, 2);
|
|
10463
|
+
} else if (harnessId === "opencode") {
|
|
10464
|
+
targetPath = join16(opts.home, ".config", "opencode", "opencode.json");
|
|
10465
|
+
hint = "OpenCode will load the enabled 'zam' MCP server on next launch.";
|
|
10466
|
+
let existing = {};
|
|
10467
|
+
if (exists(targetPath)) {
|
|
10468
|
+
existing = parseMcpJsonConfig(targetPath, read(targetPath));
|
|
10469
|
+
}
|
|
10470
|
+
const mcp = existing.mcp;
|
|
10471
|
+
if (mcp !== void 0 && (typeof mcp !== "object" || mcp === null || Array.isArray(mcp))) {
|
|
10472
|
+
throw new Error(`Cannot update ${targetPath}: mcp must be a JSON object`);
|
|
10473
|
+
}
|
|
10474
|
+
const servers = mcp ?? {};
|
|
10475
|
+
servers.zam = {
|
|
10476
|
+
type: "local",
|
|
10477
|
+
command: [opts.zamPath, "mcp"],
|
|
10478
|
+
enabled: true
|
|
10479
|
+
};
|
|
10480
|
+
existing.mcp = servers;
|
|
10481
|
+
content = JSON.stringify(existing, null, 2);
|
|
10482
|
+
} else if (harnessId === "codex") {
|
|
10483
|
+
targetPath = join16(opts.home, ".codex", "config.toml");
|
|
10484
|
+
hint = "Codex will prompt for tool execution approvals or respect the TOML approval modes.";
|
|
10485
|
+
let existingStr = "";
|
|
10486
|
+
if (exists(targetPath)) {
|
|
10487
|
+
existingStr = read(targetPath);
|
|
10488
|
+
}
|
|
10489
|
+
if (existingStr.includes("[mcp_servers.zam]")) {
|
|
10490
|
+
alreadyConfigured = true;
|
|
10491
|
+
content = existingStr;
|
|
10492
|
+
} else {
|
|
10493
|
+
const block = `
|
|
10494
|
+
[mcp_servers.zam]
|
|
10495
|
+
command = ${JSON.stringify(opts.zamPath)}
|
|
10496
|
+
args = ["mcp"]
|
|
10497
|
+
default_tools_approval_mode = "approve"
|
|
10498
|
+
|
|
10499
|
+
[mcp_servers.zam.tools.zam_review_action]
|
|
10500
|
+
approval_mode = "prompt"
|
|
10501
|
+
`;
|
|
10502
|
+
content = existingStr ? `${existingStr.trimEnd()}
|
|
10503
|
+
${block}` : block;
|
|
10504
|
+
}
|
|
10505
|
+
} else if (harnessId === "vscode") {
|
|
10506
|
+
const platform = opts.platform ?? process.platform;
|
|
10507
|
+
targetPath = platform === "win32" ? join16(opts.home, "AppData", "Roaming", "Code", "User", "mcp.json") : platform === "darwin" ? join16(
|
|
10508
|
+
opts.home,
|
|
10509
|
+
"Library",
|
|
10510
|
+
"Application Support",
|
|
10511
|
+
"Code",
|
|
10512
|
+
"User",
|
|
10513
|
+
"mcp.json"
|
|
10514
|
+
) : join16(opts.home, ".config", "Code", "User", "mcp.json");
|
|
10515
|
+
hint = "Reload VS Code after setup. ZAM Companion stays separate from the Codex chat and can be moved to any panel or sidebar.";
|
|
10516
|
+
let existing = {};
|
|
10517
|
+
if (exists(targetPath)) {
|
|
10518
|
+
existing = parseMcpJsonConfig(targetPath, read(targetPath));
|
|
10519
|
+
}
|
|
10520
|
+
if (existing.servers !== void 0 && (typeof existing.servers !== "object" || existing.servers === null || Array.isArray(existing.servers))) {
|
|
10521
|
+
throw new Error(
|
|
10522
|
+
`Cannot update ${targetPath}: servers must be a JSON object`
|
|
10523
|
+
);
|
|
10524
|
+
}
|
|
10525
|
+
if (existing.inputs !== void 0 && !Array.isArray(existing.inputs)) {
|
|
10526
|
+
throw new Error(`Cannot update ${targetPath}: inputs must be an array`);
|
|
10527
|
+
}
|
|
10528
|
+
if (!existing.servers) existing.servers = {};
|
|
10529
|
+
const expectedServer = { command: opts.zamPath, args: ["mcp"] };
|
|
10530
|
+
const currentServer = existing.servers.zam;
|
|
10531
|
+
alreadyConfigured = Array.isArray(existing.inputs) && typeof currentServer === "object" && currentServer !== null && !Array.isArray(currentServer) && JSON.stringify(currentServer) === JSON.stringify(expectedServer);
|
|
10532
|
+
existing.servers.zam = expectedServer;
|
|
10533
|
+
if (!existing.inputs) existing.inputs = [];
|
|
10534
|
+
content = JSON.stringify(existing, null, 2);
|
|
10535
|
+
} else if (harnessId === "goose") {
|
|
10536
|
+
targetPath = join16(opts.home, ".config", "goose", "config.yaml");
|
|
10537
|
+
hint = "goose will load the 'zam' extension on next session start. Run 'goose configure' to manage extensions.";
|
|
10538
|
+
const zamExtension = [
|
|
10539
|
+
" zam:",
|
|
10540
|
+
" name: ZAM",
|
|
10541
|
+
` cmd: ${opts.zamPath}`,
|
|
10542
|
+
" args:",
|
|
10543
|
+
" - mcp",
|
|
10544
|
+
" enabled: true",
|
|
10545
|
+
" type: stdio",
|
|
10546
|
+
" timeout: 300",
|
|
10547
|
+
" description: Symbiotic learning agent with spaced repetition"
|
|
10548
|
+
].join("\n");
|
|
10549
|
+
let existingStr = "";
|
|
10550
|
+
if (exists(targetPath)) {
|
|
10551
|
+
existingStr = read(targetPath);
|
|
10552
|
+
}
|
|
10553
|
+
if (/^\s+zam:\s*$/m.test(existingStr) && existingStr.includes("- mcp")) {
|
|
10554
|
+
alreadyConfigured = true;
|
|
10555
|
+
content = existingStr;
|
|
10556
|
+
} else if (/^extensions:[ \t]*$/m.test(existingStr)) {
|
|
10557
|
+
content = existingStr.replace(
|
|
10558
|
+
/^extensions:[ \t]*$/m,
|
|
10559
|
+
(line) => `${line}
|
|
10560
|
+
${zamExtension}`
|
|
10561
|
+
);
|
|
10562
|
+
} else if (existingStr.trim()) {
|
|
10563
|
+
content = `${existingStr.trimEnd()}
|
|
10564
|
+
extensions:
|
|
10565
|
+
${zamExtension}
|
|
10566
|
+
`;
|
|
10567
|
+
} else {
|
|
10568
|
+
content = `extensions:
|
|
10569
|
+
${zamExtension}
|
|
10570
|
+
`;
|
|
10571
|
+
}
|
|
10572
|
+
} else if (harnessId === "copilot") {
|
|
10573
|
+
targetPath = join16(
|
|
10574
|
+
opts.copilotHome ?? join16(opts.home, ".copilot"),
|
|
10575
|
+
"mcp-config.json"
|
|
10576
|
+
);
|
|
10577
|
+
hint = "Restart GitHub Copilot or start a new session to load the 'zam' MCP server and its focused Recall, Graph, and Settings canvases.";
|
|
10578
|
+
let existing = {};
|
|
10579
|
+
if (exists(targetPath)) {
|
|
10580
|
+
existing = parseMcpJsonConfig(targetPath, read(targetPath));
|
|
10581
|
+
}
|
|
10582
|
+
if (!existing.mcpServers) {
|
|
10583
|
+
existing.mcpServers = {};
|
|
10584
|
+
}
|
|
10585
|
+
existing.mcpServers.zam = {
|
|
10586
|
+
type: "local",
|
|
10587
|
+
command: opts.zamPath,
|
|
10588
|
+
args: ["mcp"],
|
|
10589
|
+
tools: ["*"]
|
|
10590
|
+
};
|
|
10591
|
+
content = JSON.stringify(existing, null, 2);
|
|
10592
|
+
}
|
|
10593
|
+
return {
|
|
10594
|
+
path: targetPath,
|
|
10595
|
+
content,
|
|
10596
|
+
alreadyConfigured,
|
|
10597
|
+
hint
|
|
10598
|
+
};
|
|
9985
10599
|
}
|
|
9986
10600
|
|
|
9987
|
-
// src/cli/
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10601
|
+
// src/cli/copilot-extension.ts
|
|
10602
|
+
import {
|
|
10603
|
+
existsSync as existsSync15,
|
|
10604
|
+
lstatSync,
|
|
10605
|
+
mkdirSync as mkdirSync10,
|
|
10606
|
+
readFileSync as readFileSync12,
|
|
10607
|
+
writeFileSync as writeFileSync8
|
|
10608
|
+
} from "fs";
|
|
10609
|
+
import { homedir as homedir11 } from "os";
|
|
10610
|
+
import { dirname as dirname7, join as join17, resolve as resolve4 } from "path";
|
|
10611
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10612
|
+
var EXTENSION_NAME = "zam-mcp-apps";
|
|
10613
|
+
var EXTENSION_FILES = [
|
|
10614
|
+
"extension.mjs",
|
|
10615
|
+
"host.bundle.js",
|
|
10616
|
+
"mcp-client.bundle.mjs",
|
|
10617
|
+
"manifest.json"
|
|
10618
|
+
];
|
|
10619
|
+
var packageRoot = [
|
|
10620
|
+
fileURLToPath3(new URL("../..", import.meta.url)),
|
|
10621
|
+
fileURLToPath3(new URL("../../..", import.meta.url))
|
|
10622
|
+
].find((candidate) => existsSync15(join17(candidate, "package.json"))) ?? fileURLToPath3(new URL("../..", import.meta.url));
|
|
10623
|
+
function defaultCliEntry() {
|
|
10624
|
+
return join17(dirname7(fileURLToPath3(import.meta.url)), "index.js");
|
|
10625
|
+
}
|
|
10626
|
+
function resolveCopilotHome(home = homedir11(), configuredHome = process.env.COPILOT_HOME) {
|
|
10627
|
+
return configuredHome?.trim() ? resolve4(configuredHome) : join17(home, ".copilot");
|
|
10628
|
+
}
|
|
10629
|
+
function resolveCopilotZamLaunch(zamPath, options = {}) {
|
|
10630
|
+
const cliEntry = options.cliEntry ?? defaultCliEntry();
|
|
10631
|
+
if (existsSync15(cliEntry)) {
|
|
10632
|
+
return {
|
|
10633
|
+
command: options.nodePath ?? process.execPath,
|
|
10634
|
+
args: [cliEntry, "mcp"]
|
|
10635
|
+
};
|
|
10636
|
+
}
|
|
10637
|
+
return {
|
|
10638
|
+
command: zamPath,
|
|
10639
|
+
args: ["mcp"]
|
|
10640
|
+
};
|
|
10641
|
+
}
|
|
10642
|
+
function planCopilotExtensionInstall(options) {
|
|
10643
|
+
const sourceDir = options.assetsDir ?? join17(packageRoot, "dist", "copilot-extension");
|
|
10644
|
+
for (const file of EXTENSION_FILES) {
|
|
10645
|
+
if (!existsSync15(join17(sourceDir, file))) {
|
|
10646
|
+
throw new Error(
|
|
10647
|
+
`Copilot MCP Apps asset is missing: ${join17(sourceDir, file)}. Run \`npm run build\` and retry.`
|
|
10648
|
+
);
|
|
10649
|
+
}
|
|
10650
|
+
}
|
|
10651
|
+
const manifest = JSON.parse(
|
|
10652
|
+
readFileSync12(join17(sourceDir, "manifest.json"), "utf8")
|
|
10653
|
+
);
|
|
10654
|
+
if (manifest.name !== EXTENSION_NAME || typeof manifest.version !== "string" || !manifest.version) {
|
|
10655
|
+
throw new Error(
|
|
10656
|
+
`Invalid Copilot MCP Apps manifest: ${join17(sourceDir, "manifest.json")}`
|
|
10657
|
+
);
|
|
10658
|
+
}
|
|
10659
|
+
const copilotHome = resolveCopilotHome(
|
|
10660
|
+
options.home,
|
|
10661
|
+
options.copilotHome ?? process.env.COPILOT_HOME
|
|
10662
|
+
);
|
|
10663
|
+
return {
|
|
10664
|
+
sourceDir,
|
|
10665
|
+
destinationDir: join17(copilotHome, "extensions", EXTENSION_NAME),
|
|
10666
|
+
launch: resolveCopilotZamLaunch(options.zamPath, options),
|
|
10667
|
+
files: EXTENSION_FILES
|
|
10668
|
+
};
|
|
10669
|
+
}
|
|
10670
|
+
function writeIfChanged(path, content) {
|
|
10671
|
+
const next = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
10672
|
+
if (existsSync15(path) && readFileSync12(path).equals(next)) return false;
|
|
10673
|
+
writeFileSync8(path, next);
|
|
10674
|
+
return true;
|
|
10675
|
+
}
|
|
10676
|
+
function installCopilotExtension(options) {
|
|
10677
|
+
const plan = planCopilotExtensionInstall(options);
|
|
10678
|
+
if (options.dryRun) {
|
|
10679
|
+
return { ...plan, action: "planned", changedFiles: [] };
|
|
10680
|
+
}
|
|
10681
|
+
const destinationExisted = existsSync15(plan.destinationDir);
|
|
10682
|
+
if (destinationExisted && lstatSync(plan.destinationDir).isSymbolicLink()) {
|
|
10683
|
+
throw new Error(
|
|
10684
|
+
`Refusing to replace symlinked Copilot extension directory: ${plan.destinationDir}`
|
|
10685
|
+
);
|
|
10686
|
+
}
|
|
10687
|
+
mkdirSync10(plan.destinationDir, { recursive: true });
|
|
10688
|
+
const changedFiles = [];
|
|
10689
|
+
for (const file of plan.files) {
|
|
10690
|
+
const source = join17(plan.sourceDir, file);
|
|
10691
|
+
const destination = join17(plan.destinationDir, file);
|
|
10692
|
+
if (writeIfChanged(destination, readFileSync12(source))) {
|
|
10693
|
+
changedFiles.push(file);
|
|
10694
|
+
}
|
|
10695
|
+
}
|
|
10696
|
+
const launchContent = `${JSON.stringify(
|
|
10697
|
+
{
|
|
10698
|
+
schemaVersion: 1,
|
|
10699
|
+
command: plan.launch.command,
|
|
10700
|
+
args: plan.launch.args
|
|
10701
|
+
},
|
|
10702
|
+
null,
|
|
10703
|
+
2
|
|
10704
|
+
)}
|
|
10705
|
+
`;
|
|
10706
|
+
if (writeIfChanged(join17(plan.destinationDir, "launch.json"), launchContent)) {
|
|
10707
|
+
changedFiles.push("launch.json");
|
|
10708
|
+
}
|
|
10709
|
+
return {
|
|
10710
|
+
...plan,
|
|
10711
|
+
action: !destinationExisted ? "installed" : changedFiles.length > 0 ? "updated" : "unchanged",
|
|
10712
|
+
changedFiles
|
|
10713
|
+
};
|
|
10714
|
+
}
|
|
10715
|
+
|
|
10716
|
+
// src/cli/vscode-extension.ts
|
|
10717
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
10718
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
10719
|
+
import { homedir as homedir12 } from "os";
|
|
10720
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
10721
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
10722
|
+
var packageRoot2 = [
|
|
10723
|
+
fileURLToPath4(new URL("../..", import.meta.url)),
|
|
10724
|
+
fileURLToPath4(new URL("../../..", import.meta.url))
|
|
10725
|
+
].find((candidate) => existsSync16(join18(candidate, "package.json"))) ?? fileURLToPath4(new URL("../..", import.meta.url));
|
|
10726
|
+
function resolveVscodeExecutable(options = {}) {
|
|
10727
|
+
const home = options.home ?? homedir12();
|
|
10728
|
+
const platform = options.platform ?? process.platform;
|
|
10729
|
+
const find = options.find ?? findExecutable;
|
|
10730
|
+
const exists = options.exists ?? existsSync16;
|
|
10731
|
+
const found = find("code");
|
|
10732
|
+
if (found) return found;
|
|
10733
|
+
const candidates = platform === "darwin" ? [
|
|
10734
|
+
"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
|
|
10735
|
+
join18(
|
|
10736
|
+
home,
|
|
10737
|
+
"Applications",
|
|
10738
|
+
"Visual Studio Code.app",
|
|
10739
|
+
"Contents",
|
|
10740
|
+
"Resources",
|
|
10741
|
+
"app",
|
|
10742
|
+
"bin",
|
|
10743
|
+
"code"
|
|
10744
|
+
)
|
|
10745
|
+
] : platform === "win32" ? [
|
|
10746
|
+
join18(
|
|
10747
|
+
home,
|
|
10748
|
+
"AppData",
|
|
10749
|
+
"Local",
|
|
10750
|
+
"Programs",
|
|
10751
|
+
"Microsoft VS Code",
|
|
10752
|
+
"bin",
|
|
10753
|
+
"code.cmd"
|
|
10754
|
+
)
|
|
10755
|
+
] : ["/usr/bin/code", "/usr/local/bin/code", "/snap/bin/code"];
|
|
10756
|
+
return candidates.find((candidate) => exists(candidate)) ?? null;
|
|
10757
|
+
}
|
|
10758
|
+
function packageVersion() {
|
|
10759
|
+
const parsed = JSON.parse(
|
|
10760
|
+
readFileSync13(join18(packageRoot2, "package.json"), "utf8")
|
|
10761
|
+
);
|
|
10762
|
+
if (typeof parsed.version !== "string" || !parsed.version) {
|
|
10763
|
+
throw new Error("Cannot determine the ZAM package version");
|
|
10764
|
+
}
|
|
10765
|
+
return parsed.version;
|
|
10766
|
+
}
|
|
10767
|
+
function planVscodeExtensionInstall(options) {
|
|
10768
|
+
const home = options.home ?? homedir12();
|
|
10769
|
+
const version = options.version ?? packageVersion();
|
|
10770
|
+
const assetsDir = options.assetsDir ?? join18(packageRoot2, "dist", "vscode-extension");
|
|
10771
|
+
const vsixPath = join18(assetsDir, `ZAM_Companion_${version}.vsix`);
|
|
10772
|
+
if (!existsSync16(vsixPath)) {
|
|
10773
|
+
throw new Error(
|
|
10774
|
+
`ZAM Companion VSIX asset is missing: ${vsixPath}. Run \`npm run build\` and retry.`
|
|
10775
|
+
);
|
|
10776
|
+
}
|
|
10777
|
+
const codePath = options.codePath ?? resolveVscodeExecutable({
|
|
10778
|
+
home,
|
|
10779
|
+
platform: options.platform,
|
|
10780
|
+
find: options.find,
|
|
10781
|
+
exists: options.exists
|
|
10782
|
+
});
|
|
10783
|
+
if (!codePath) {
|
|
10784
|
+
throw new Error(
|
|
10785
|
+
"Visual Studio Code was not detected. Install VS Code or add its 'code' command to PATH."
|
|
10786
|
+
);
|
|
10787
|
+
}
|
|
10788
|
+
return {
|
|
10789
|
+
version,
|
|
10790
|
+
vsixPath,
|
|
10791
|
+
codePath,
|
|
10792
|
+
launchConfigPath: join18(home, ".zam", "vscode-launch.json"),
|
|
10793
|
+
launch: { command: options.zamPath, args: ["mcp"] }
|
|
10794
|
+
};
|
|
10795
|
+
}
|
|
10796
|
+
function installVscodeExtension(options) {
|
|
10797
|
+
const plan = planVscodeExtensionInstall(options);
|
|
10798
|
+
if (options.dryRun) return { ...plan, action: "planned" };
|
|
10799
|
+
const launchContent = `${JSON.stringify(
|
|
10800
|
+
{
|
|
10801
|
+
schemaVersion: 1,
|
|
10802
|
+
version: plan.version,
|
|
10803
|
+
command: plan.launch.command,
|
|
10804
|
+
args: plan.launch.args
|
|
10805
|
+
},
|
|
10806
|
+
null,
|
|
10807
|
+
2
|
|
10808
|
+
)}
|
|
10809
|
+
`;
|
|
10810
|
+
const existed = existsSync16(plan.launchConfigPath);
|
|
10811
|
+
const changed = !existed || readFileSync13(plan.launchConfigPath, "utf8") !== launchContent;
|
|
10812
|
+
const run = options.run ?? ((command, args) => {
|
|
10813
|
+
execFileSync3(command, args, { stdio: "pipe" });
|
|
10814
|
+
});
|
|
10815
|
+
run(plan.codePath, ["--install-extension", plan.vsixPath, "--force"]);
|
|
10816
|
+
if (changed) {
|
|
10817
|
+
mkdirSync11(dirname8(plan.launchConfigPath), { recursive: true });
|
|
10818
|
+
writeFileSync9(plan.launchConfigPath, launchContent, "utf8");
|
|
10819
|
+
}
|
|
10820
|
+
return {
|
|
10821
|
+
...plan,
|
|
10822
|
+
action: !existed ? "installed" : changed ? "updated" : "unchanged"
|
|
10823
|
+
};
|
|
10824
|
+
}
|
|
10825
|
+
|
|
10826
|
+
// src/cli/agent-connect.ts
|
|
10827
|
+
var CONNECT_HARNESSES = [
|
|
10828
|
+
"claude-code",
|
|
10829
|
+
"claude-desktop",
|
|
10830
|
+
"antigravity",
|
|
10831
|
+
"codex",
|
|
10832
|
+
"vscode",
|
|
10833
|
+
"opencode",
|
|
10834
|
+
"goose",
|
|
10835
|
+
"copilot"
|
|
10836
|
+
];
|
|
10837
|
+
var USER_SCOPED_CONNECT_HARNESSES = [
|
|
10838
|
+
"claude-desktop",
|
|
10839
|
+
"antigravity",
|
|
10840
|
+
"codex",
|
|
10841
|
+
"vscode",
|
|
10842
|
+
"opencode",
|
|
10843
|
+
"goose",
|
|
10844
|
+
"copilot"
|
|
10845
|
+
];
|
|
10846
|
+
var CONNECT_HARNESS_LABELS = {
|
|
10847
|
+
"claude-code": "Claude Code",
|
|
10848
|
+
"claude-desktop": "Claude Desktop",
|
|
10849
|
+
antigravity: "Antigravity",
|
|
10850
|
+
codex: "Codex",
|
|
10851
|
+
vscode: "VS Code",
|
|
10852
|
+
opencode: "OpenCode",
|
|
10853
|
+
goose: "Goose",
|
|
10854
|
+
copilot: "GitHub Copilot"
|
|
10855
|
+
};
|
|
10856
|
+
function isConnectHarnessId(value) {
|
|
10857
|
+
return CONNECT_HARNESSES.includes(value);
|
|
10858
|
+
}
|
|
10859
|
+
function resolveDeps(deps) {
|
|
10860
|
+
const home = deps.home ?? homedir13();
|
|
10861
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
10862
|
+
const copilotHome = deps.copilotHome ?? process.env.COPILOT_HOME;
|
|
10863
|
+
return {
|
|
10864
|
+
home,
|
|
10865
|
+
cwd,
|
|
10866
|
+
copilotHome,
|
|
10867
|
+
findZam: deps.findZam ?? (() => findExecutable("zam")),
|
|
10868
|
+
detect: deps.detect ?? (() => detectInstalledConnectHarnesses({ home, copilotHome })),
|
|
10869
|
+
connectMcp: deps.connectMcp ?? connectHarnessMcp,
|
|
10870
|
+
writeConfig: deps.writeConfig ?? ((path, content) => {
|
|
10871
|
+
mkdirSync12(dirname9(path), { recursive: true });
|
|
10872
|
+
writeFileSync10(path, content, "utf-8");
|
|
10873
|
+
}),
|
|
10874
|
+
installCopilot: deps.installCopilot ?? installCopilotExtension,
|
|
10875
|
+
installVscode: deps.installVscode ?? installVscodeExtension,
|
|
10876
|
+
resolveAntigravity: deps.resolveAntigravity ?? resolveAntigravityIdeExecutable,
|
|
10877
|
+
refreshSkills: deps.refreshSkills ?? distributeGlobalSkills
|
|
10878
|
+
};
|
|
10879
|
+
}
|
|
10880
|
+
function performAgentConnect(opts = {}, deps = {}) {
|
|
10881
|
+
const d = resolveDeps(deps);
|
|
10882
|
+
const dryRun = Boolean(opts.dryRun);
|
|
10883
|
+
const detected = opts.harness ? [opts.harness] : d.detect();
|
|
10884
|
+
const foundZam = d.findZam();
|
|
10885
|
+
const zamPath = foundZam ?? "zam";
|
|
10886
|
+
const results = [];
|
|
10887
|
+
for (const harness of detected) {
|
|
10888
|
+
const label = CONNECT_HARNESS_LABELS[harness];
|
|
10889
|
+
try {
|
|
10890
|
+
const prepared = d.connectMcp(harness, {
|
|
10891
|
+
zamPath,
|
|
10892
|
+
cwd: d.cwd,
|
|
10893
|
+
home: d.home,
|
|
10894
|
+
copilotHome: d.copilotHome
|
|
10895
|
+
});
|
|
10896
|
+
let extension = null;
|
|
10897
|
+
if (harness === "copilot") {
|
|
10898
|
+
const installed = d.installCopilot({
|
|
10899
|
+
home: d.home,
|
|
10900
|
+
zamPath,
|
|
10901
|
+
dryRun
|
|
10902
|
+
});
|
|
10903
|
+
extension = {
|
|
10904
|
+
kind: "copilot",
|
|
10905
|
+
action: installed.action,
|
|
10906
|
+
location: installed.destinationDir,
|
|
10907
|
+
detail: `${installed.launch.command} ${installed.launch.args.join(" ")}`
|
|
10908
|
+
};
|
|
10909
|
+
} else if (harness === "vscode") {
|
|
10910
|
+
const installed = d.installVscode({ home: d.home, zamPath, dryRun });
|
|
10911
|
+
extension = {
|
|
10912
|
+
kind: "vscode",
|
|
10913
|
+
action: installed.action,
|
|
10914
|
+
location: installed.vsixPath,
|
|
10915
|
+
detail: installed.launchConfigPath
|
|
10916
|
+
};
|
|
10917
|
+
} else if (harness === "antigravity") {
|
|
10918
|
+
const antigravityPath = d.resolveAntigravity();
|
|
10919
|
+
if (antigravityPath) {
|
|
10920
|
+
const installed = d.installVscode({
|
|
10921
|
+
home: d.home,
|
|
10922
|
+
zamPath,
|
|
10923
|
+
codePath: antigravityPath,
|
|
10924
|
+
dryRun
|
|
10925
|
+
});
|
|
10926
|
+
extension = {
|
|
10927
|
+
kind: "vscode",
|
|
10928
|
+
action: installed.action,
|
|
10929
|
+
location: installed.vsixPath,
|
|
10930
|
+
detail: installed.launchConfigPath
|
|
10931
|
+
};
|
|
10932
|
+
}
|
|
10933
|
+
}
|
|
10934
|
+
let wrote = false;
|
|
10935
|
+
if (!prepared.alreadyConfigured && !dryRun) {
|
|
10936
|
+
d.writeConfig(prepared.path, prepared.content);
|
|
10937
|
+
wrote = true;
|
|
10938
|
+
}
|
|
10939
|
+
results.push({
|
|
10940
|
+
harness,
|
|
10941
|
+
label,
|
|
10942
|
+
path: prepared.path,
|
|
10943
|
+
content: prepared.content,
|
|
10944
|
+
alreadyConfigured: prepared.alreadyConfigured,
|
|
10945
|
+
wrote,
|
|
10946
|
+
hint: prepared.hint,
|
|
10947
|
+
extension
|
|
10948
|
+
});
|
|
10949
|
+
} catch (error) {
|
|
10950
|
+
results.push({
|
|
10951
|
+
harness,
|
|
10952
|
+
label,
|
|
10953
|
+
path: "",
|
|
10954
|
+
content: "",
|
|
10955
|
+
alreadyConfigured: false,
|
|
10956
|
+
wrote: false,
|
|
10957
|
+
hint: "",
|
|
10958
|
+
extension: null,
|
|
10959
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10960
|
+
});
|
|
10961
|
+
}
|
|
10962
|
+
}
|
|
10963
|
+
let skills = null;
|
|
10964
|
+
if (!dryRun && detected.length > 0) {
|
|
10965
|
+
const skillResults = d.refreshSkills(d.home);
|
|
10966
|
+
skills = {
|
|
10967
|
+
refreshed: skillResults.filter((result) => result.success).length,
|
|
10968
|
+
total: skillResults.length
|
|
10969
|
+
};
|
|
10970
|
+
}
|
|
10971
|
+
return {
|
|
10972
|
+
success: results.every((result) => !result.error),
|
|
10973
|
+
detected,
|
|
10974
|
+
zamPath,
|
|
10975
|
+
zamOnPath: Boolean(foundZam),
|
|
10976
|
+
results,
|
|
10977
|
+
skills
|
|
10978
|
+
};
|
|
10979
|
+
}
|
|
10980
|
+
function inspectConnectHarnesses(deps = {}) {
|
|
10981
|
+
const d = resolveDeps(deps);
|
|
10982
|
+
const foundZam = d.findZam();
|
|
10983
|
+
const zamPath = foundZam ?? "zam";
|
|
10984
|
+
const installed = new Set(d.detect());
|
|
10985
|
+
const harnesses = USER_SCOPED_CONNECT_HARNESSES.map((harness) => {
|
|
10986
|
+
const status = {
|
|
10987
|
+
harness,
|
|
10988
|
+
label: CONNECT_HARNESS_LABELS[harness],
|
|
10989
|
+
installed: installed.has(harness),
|
|
10990
|
+
configured: false,
|
|
10991
|
+
configPath: ""
|
|
10992
|
+
};
|
|
10993
|
+
try {
|
|
10994
|
+
const probe = d.connectMcp(harness, {
|
|
10995
|
+
zamPath,
|
|
10996
|
+
cwd: d.cwd,
|
|
10997
|
+
home: d.home,
|
|
10998
|
+
copilotHome: d.copilotHome
|
|
10999
|
+
});
|
|
11000
|
+
status.configured = probe.alreadyConfigured;
|
|
11001
|
+
status.configPath = probe.path;
|
|
11002
|
+
} catch (error) {
|
|
11003
|
+
status.note = error instanceof Error ? error.message : String(error);
|
|
11004
|
+
}
|
|
11005
|
+
return status;
|
|
11006
|
+
});
|
|
11007
|
+
return { zamOnPath: Boolean(foundZam), harnesses };
|
|
11008
|
+
}
|
|
11009
|
+
|
|
11010
|
+
// src/cli/curriculum/breadcrumb.ts
|
|
11011
|
+
init_kernel();
|
|
11012
|
+
var LAST_SELECTION_KEY = "curriculum.lastSelection";
|
|
11013
|
+
async function getLastCurriculumSelection(db) {
|
|
11014
|
+
const raw = await getSetting(db, LAST_SELECTION_KEY);
|
|
11015
|
+
if (!raw) return void 0;
|
|
11016
|
+
try {
|
|
11017
|
+
return JSON.parse(raw);
|
|
11018
|
+
} catch {
|
|
11019
|
+
return void 0;
|
|
11020
|
+
}
|
|
11021
|
+
}
|
|
11022
|
+
async function setLastCurriculumSelection(db, breadcrumb) {
|
|
11023
|
+
await setSetting(db, LAST_SELECTION_KEY, JSON.stringify(breadcrumb));
|
|
11024
|
+
}
|
|
11025
|
+
|
|
11026
|
+
// src/cli/curriculum/providers/bildungsplan-bremen/manifest.ts
|
|
11027
|
+
var BILDUNGSPLAN_BREMEN_MANIFEST = {
|
|
11028
|
+
schoolYear: "2025/2026",
|
|
11029
|
+
capturedOn: "2026-07-02",
|
|
11030
|
+
sourceRevision: "Bildungspl\xE4ne Bremen",
|
|
11031
|
+
schoolTypes: [
|
|
11032
|
+
{ id: "oberschule", label: "Oberschule" },
|
|
11033
|
+
{ id: "gymnasium", label: "Gymnasium" }
|
|
11034
|
+
],
|
|
11035
|
+
grades: {
|
|
11036
|
+
oberschule: ["7", "8", "9", "10"],
|
|
11037
|
+
gymnasium: ["7", "8", "9", "10"]
|
|
11038
|
+
},
|
|
11039
|
+
subjects: {
|
|
11040
|
+
oberschule: [
|
|
11041
|
+
{ id: "mathematik", label: "Mathematik" },
|
|
11042
|
+
{ id: "informatik", label: "Informatik" },
|
|
11043
|
+
{ id: "physik", label: "Physik" },
|
|
11044
|
+
{ id: "chemie", label: "Chemie" },
|
|
11045
|
+
{ id: "biologie", label: "Biologie" }
|
|
11046
|
+
],
|
|
11047
|
+
gymnasium: [
|
|
11048
|
+
{ id: "mathematik", label: "Mathematik" },
|
|
10010
11049
|
{ id: "informatik", label: "Informatik" },
|
|
10011
11050
|
{ id: "physik", label: "Physik" },
|
|
10012
11051
|
{ id: "chemie", label: "Chemie" },
|
|
@@ -11870,7 +12909,7 @@ function normalizeForComparison11(str) {
|
|
|
11870
12909
|
// src/cli/curriculum/providers/lehrplanplus-bayern/manifest.ts
|
|
11871
12910
|
var LEHRPLANPLUS_BAYERN_MANIFEST = {
|
|
11872
12911
|
schoolYear: "2026/2027",
|
|
11873
|
-
capturedOn: "2026-07-
|
|
12912
|
+
capturedOn: "2026-07-12",
|
|
11874
12913
|
sourceRevision: "LehrplanPLUS Realschule \u2013 Oktober 2023",
|
|
11875
12914
|
schoolTypes: [
|
|
11876
12915
|
{ id: "grundschule", label: "Grundschule" },
|
|
@@ -11927,12 +12966,242 @@ var LEHRPLANPLUS_BAYERN_MANIFEST = {
|
|
|
11927
12966
|
]
|
|
11928
12967
|
},
|
|
11929
12968
|
tracks: {
|
|
12969
|
+
"realschule|5|sport": [
|
|
12970
|
+
{ id: "basis_sport", label: "Basissport 5" },
|
|
12971
|
+
{ id: "diff_sport", label: "Differenzierter Sport" }
|
|
12972
|
+
],
|
|
11930
12973
|
"realschule|9|mathematik": [
|
|
11931
12974
|
{ id: "wpfg1", label: "Mathematik 9 (I)" },
|
|
11932
12975
|
{ id: "wpfg2-3", label: "Mathematik 9 (II/III)" }
|
|
11933
12976
|
]
|
|
11934
12977
|
},
|
|
11935
12978
|
topics: {
|
|
12979
|
+
"realschule|5|biologie": [
|
|
12980
|
+
{ id: "lb1", label: "Prozessbezogene Kompetenzen" },
|
|
12981
|
+
{
|
|
12982
|
+
id: "lb2",
|
|
12983
|
+
label: "Biologie, die Wissenschaft von den Lebewesen",
|
|
12984
|
+
hours: 14
|
|
12985
|
+
},
|
|
12986
|
+
{
|
|
12987
|
+
id: "lb3",
|
|
12988
|
+
label: "Bau und Funktion des menschlichen K\xF6rpers",
|
|
12989
|
+
hours: 22
|
|
12990
|
+
},
|
|
12991
|
+
{
|
|
12992
|
+
id: "lb4",
|
|
12993
|
+
label: "Tiere und Pflanzen in der Umgebung des Menschen",
|
|
12994
|
+
hours: 20
|
|
12995
|
+
}
|
|
12996
|
+
],
|
|
12997
|
+
"realschule|5|deutsch": [
|
|
12998
|
+
{ id: "lb1", label: "Sprechen und Zuh\xF6ren" },
|
|
12999
|
+
{ id: "lb2", label: "Lesen \u2013 mit Texten und weiteren Medien umgehen" },
|
|
13000
|
+
{ id: "lb3", label: "Schreiben" },
|
|
13001
|
+
{
|
|
13002
|
+
id: "lb4",
|
|
13003
|
+
label: "Sprachgebrauch und Sprache untersuchen und reflektieren"
|
|
13004
|
+
}
|
|
13005
|
+
],
|
|
13006
|
+
"realschule|5|englisch": [
|
|
13007
|
+
{ id: "lb1", label: "Kommunikative Kompetenzen" },
|
|
13008
|
+
{ id: "lb2", label: "Interkulturelle Kompetenzen" },
|
|
13009
|
+
{ id: "lb3", label: "Text- und Medienkompetenzen" },
|
|
13010
|
+
{ id: "lb4", label: "Methodische Kompetenzen" },
|
|
13011
|
+
{ id: "lb5", label: "Themengebiete" }
|
|
13012
|
+
],
|
|
13013
|
+
"realschule|5|ethik": [
|
|
13014
|
+
{ id: "lb1", label: "Meine Wirklichkeit und ich", hours: 16 },
|
|
13015
|
+
{ id: "lb2", label: "Leben in der Familie", hours: 12 },
|
|
13016
|
+
{ id: "lb3", label: "Spielen", hours: 12 },
|
|
13017
|
+
{
|
|
13018
|
+
id: "lb4",
|
|
13019
|
+
label: "Feste und Riten in Religion und Brauchtum",
|
|
13020
|
+
hours: 16
|
|
13021
|
+
}
|
|
13022
|
+
],
|
|
13023
|
+
"realschule|5|evangelische-religionslehre": [
|
|
13024
|
+
{ id: "lb1", label: "Leben in Gemeinschaft" },
|
|
13025
|
+
{ id: "lb2", label: "Die Bibel \u2013 Buch des Lebens" },
|
|
13026
|
+
{
|
|
13027
|
+
id: "lb3",
|
|
13028
|
+
label: "Erfahrungen mit Gott als Begleiter auf dem Lebensweg"
|
|
13029
|
+
},
|
|
13030
|
+
{ id: "lb4", label: "Glaube wird sichtbar und hinterl\xE4sst Spuren" },
|
|
13031
|
+
{
|
|
13032
|
+
id: "lb5",
|
|
13033
|
+
label: "Sch\xF6pfung \u2013 Unsere Welt und unser Leben als Geschenk Gottes?"
|
|
13034
|
+
}
|
|
13035
|
+
],
|
|
13036
|
+
"realschule|5|geographie": [
|
|
13037
|
+
{ id: "lb1", label: "Einf\xFChrung in das Fach", hours: 8 },
|
|
13038
|
+
{ id: "lb2", label: "Planet Erde", hours: 8 },
|
|
13039
|
+
{ id: "lb3", label: "Gestalt und Gliederung der Erde", hours: 10 },
|
|
13040
|
+
{ id: "lb4", label: "Ver\xE4nderung der Erdoberfl\xE4che", hours: 10 },
|
|
13041
|
+
{
|
|
13042
|
+
id: "lb5",
|
|
13043
|
+
label: "Naturr\xE4umliche und politische Strukturen in Deutschland und Bayern",
|
|
13044
|
+
hours: 8
|
|
13045
|
+
},
|
|
13046
|
+
{ id: "lb6", label: "Anwendung im Nahraum", hours: 8 },
|
|
13047
|
+
{ id: "lb7", label: "Aktuelle geographische Fragestellung", hours: 4 }
|
|
13048
|
+
],
|
|
13049
|
+
"realschule|5|it": [
|
|
13050
|
+
{ id: "lb1", label: "Anfangsunterricht" },
|
|
13051
|
+
{ id: "lb2", label: "Aufbauunterricht" },
|
|
13052
|
+
{ id: "lb3", label: "Bilingualer Sachfachunterricht (optional)" }
|
|
13053
|
+
],
|
|
13054
|
+
"realschule|5|iu": [
|
|
13055
|
+
{ id: "lb1", label: "Miteinander leben \u2013 Eigene Aufgaben wahrnehmen" },
|
|
13056
|
+
{
|
|
13057
|
+
id: "lb2",
|
|
13058
|
+
label: "Religi\xF6ses Leben \u2013 Ausdrucksformen des Islams kennen"
|
|
13059
|
+
},
|
|
13060
|
+
{
|
|
13061
|
+
id: "lb3",
|
|
13062
|
+
label: "Glaubenslehre des Islams \u2013 Gottesvorstellungen reflektieren"
|
|
13063
|
+
},
|
|
13064
|
+
{
|
|
13065
|
+
id: "lb4",
|
|
13066
|
+
label: "Propheten \u2013 Gottes Offenbarungen beschreiben"
|
|
13067
|
+
},
|
|
13068
|
+
{
|
|
13069
|
+
id: "lb5",
|
|
13070
|
+
label: "Muhammads Leben und Wirken \u2013 Eigenschaften von Vorbildern reflektieren"
|
|
13071
|
+
},
|
|
13072
|
+
{
|
|
13073
|
+
id: "lb6",
|
|
13074
|
+
label: "Koran und Schrifttradition \u2013 Fachsprache entwickeln"
|
|
13075
|
+
},
|
|
13076
|
+
{
|
|
13077
|
+
id: "lb7",
|
|
13078
|
+
label: "Geschichte und Geographie des Islams \u2013 Historische Kontexte erl\xE4utern"
|
|
13079
|
+
},
|
|
13080
|
+
{
|
|
13081
|
+
id: "lb8",
|
|
13082
|
+
label: "Religionen und Weltanschauungen \u2013 Gemeinsamkeiten und Unterschiede vergleichen"
|
|
13083
|
+
}
|
|
13084
|
+
],
|
|
13085
|
+
"realschule|5|ir": [
|
|
13086
|
+
{
|
|
13087
|
+
id: "lb1",
|
|
13088
|
+
label: "J\xFCdischer Kalender und Jahreszyklus: Grundlagen des j\xFCdischen Kalenders, Feiertage im Jahreskreis, Chanukka und Purim",
|
|
13089
|
+
hours: 12
|
|
13090
|
+
},
|
|
13091
|
+
{
|
|
13092
|
+
id: "lb2",
|
|
13093
|
+
label: "Gebet und Ritus: Schma und Amida, die beiden Hauptgebete",
|
|
13094
|
+
hours: 10
|
|
13095
|
+
},
|
|
13096
|
+
{
|
|
13097
|
+
id: "lb3",
|
|
13098
|
+
label: "Mensch und Welt: Vertrauend einen neuen Anfang wagen",
|
|
13099
|
+
hours: 10
|
|
13100
|
+
},
|
|
13101
|
+
{
|
|
13102
|
+
id: "lb4",
|
|
13103
|
+
label: "J\xFCdische Geschichte und Philosophie: Biblische Zeit und Monotheismus",
|
|
13104
|
+
hours: 12
|
|
13105
|
+
},
|
|
13106
|
+
{
|
|
13107
|
+
id: "lb5",
|
|
13108
|
+
label: "Schriftliche Quellen \u2013 Werte: Tora und Anawa (Bescheidenheit)",
|
|
13109
|
+
hours: 12
|
|
13110
|
+
}
|
|
13111
|
+
],
|
|
13112
|
+
"realschule|5|katholische-religionslehre": [
|
|
13113
|
+
{
|
|
13114
|
+
id: "lb1",
|
|
13115
|
+
label: "Auf Gott vertrauen \u2013 einen neuen Anfang wagen",
|
|
13116
|
+
hours: 10
|
|
13117
|
+
},
|
|
13118
|
+
{
|
|
13119
|
+
id: "lb2",
|
|
13120
|
+
label: "\u201EUmsorge mich mit deiner Liebe\u201C \u2013 beten und meditieren",
|
|
13121
|
+
hours: 10
|
|
13122
|
+
},
|
|
13123
|
+
{
|
|
13124
|
+
id: "lb3",
|
|
13125
|
+
label: "Erfahrungen mit Gott \u2013 Die Heilige Schrift",
|
|
13126
|
+
hours: 12
|
|
13127
|
+
},
|
|
13128
|
+
{
|
|
13129
|
+
id: "lb4",
|
|
13130
|
+
label: '\u201EIn jenen Tagen trat einer auf" \u2013 Jesus im Blickwinkel seiner Zeit und Umwelt',
|
|
13131
|
+
hours: 12
|
|
13132
|
+
},
|
|
13133
|
+
{
|
|
13134
|
+
id: "lb5",
|
|
13135
|
+
label: "Leben in der Pfarrgemeinde \u2013 Eingebundensein in die Kirche",
|
|
13136
|
+
hours: 12
|
|
13137
|
+
}
|
|
13138
|
+
],
|
|
13139
|
+
"realschule|5|kunst": [
|
|
13140
|
+
{
|
|
13141
|
+
id: "lb1",
|
|
13142
|
+
label: "Bildnerische Auseinandersetzung mit Wirklichkeit und Fantasie",
|
|
13143
|
+
hours: 44
|
|
13144
|
+
},
|
|
13145
|
+
{ id: "lb2", label: "Bildende Kunst", hours: 20 },
|
|
13146
|
+
{ id: "lb3", label: "Angewandte Kunst", hours: 20 }
|
|
13147
|
+
],
|
|
13148
|
+
"realschule|5|mathematik": [
|
|
13149
|
+
{ id: "lb1", label: "Nat\xFCrliche Zahlen", hours: 50 },
|
|
13150
|
+
{ id: "lb2", label: "Ganze Zahlen", hours: 20 },
|
|
13151
|
+
{
|
|
13152
|
+
id: "lb3",
|
|
13153
|
+
label: "Geometrische Grundvorstellungen und Grundbegriffe",
|
|
13154
|
+
hours: 30
|
|
13155
|
+
},
|
|
13156
|
+
{ id: "lb4", label: "Gr\xF6\xDFen", hours: 20 },
|
|
13157
|
+
{
|
|
13158
|
+
id: "lb5",
|
|
13159
|
+
label: "Umfang und Fl\xE4cheninhalt ebener Figuren",
|
|
13160
|
+
hours: 15
|
|
13161
|
+
},
|
|
13162
|
+
{ id: "lb6", label: "Auswertung von Daten", hours: 5 }
|
|
13163
|
+
],
|
|
13164
|
+
"realschule|5|musik": [
|
|
13165
|
+
{ id: "lb1", label: "Sprechen \u2013 Singen \u2013 Musizieren", hours: 20 },
|
|
13166
|
+
{ id: "lb2", label: "Musik \u2013 Mensch \u2013 Zeit", hours: 10 },
|
|
13167
|
+
{ id: "lb3", label: "Bewegung \u2013 Tanz \u2013 Szene", hours: 10 },
|
|
13168
|
+
{ id: "lb4", label: "Musik und ihre Grundlagen", hours: 16 }
|
|
13169
|
+
],
|
|
13170
|
+
"realschule|5|or": [
|
|
13171
|
+
{ id: "lb1", label: "Miteinander leben", hours: 10 },
|
|
13172
|
+
{ id: "lb2", label: "Von Gott und zu Gott sprechen", hours: 10 },
|
|
13173
|
+
{ id: "lb3", label: "Die Bibel", hours: 12 },
|
|
13174
|
+
{ id: "lb4", label: "Ursprung der Kirche", hours: 12 },
|
|
13175
|
+
{ id: "lb5", label: "Kirche vor Ort", hours: 12 }
|
|
13176
|
+
],
|
|
13177
|
+
"realschule|5|sport|basis_sport": [
|
|
13178
|
+
{ id: "lb1", label: "Gesundheit und Fitness" },
|
|
13179
|
+
{ id: "lb2", label: "Fairness/Kooperation/Selbstkompetenz" },
|
|
13180
|
+
{ id: "lb3", label: "Freizeit und Umwelt" },
|
|
13181
|
+
{ id: "lb4", label: "Sportliche Handlungsfelder" }
|
|
13182
|
+
],
|
|
13183
|
+
"realschule|5|sport|diff_sport": [
|
|
13184
|
+
{ id: "lb1", label: "Bewegungsk\xFCnste" },
|
|
13185
|
+
{ id: "lb2", label: "Radsport" },
|
|
13186
|
+
{ id: "lb3", label: "Rhythmische Sportgymnastik" },
|
|
13187
|
+
{ id: "lb4", label: "Sportklettern" }
|
|
13188
|
+
],
|
|
13189
|
+
"realschule|5|textiles-gestalten": [
|
|
13190
|
+
{ id: "lb1", label: "Eine textile Fl\xE4che bilden \u2013 Filzen", hours: 18 },
|
|
13191
|
+
{ id: "lb2", label: "Eine textile Fl\xE4che bilden \u2013 H\xE4keln", hours: 18 },
|
|
13192
|
+
{ id: "lb3", label: "Eine textile Fl\xE4che bilden \u2013 Weben", hours: 24 },
|
|
13193
|
+
{ id: "lb4", label: "Eine textile Fl\xE4che bilden \u2013 Kn\xFCpfen", hours: 24 },
|
|
13194
|
+
{
|
|
13195
|
+
id: "lb5",
|
|
13196
|
+
label: "Eine textile Fl\xE4che verarbeiten \u2013 Handn\xE4hen, Maschinenn\xE4hen",
|
|
13197
|
+
hours: 42
|
|
13198
|
+
}
|
|
13199
|
+
],
|
|
13200
|
+
"realschule|5|werken": [
|
|
13201
|
+
{ id: "lb1", label: "Arbeiten mit dem Werkstoff Holz", hours: 28 },
|
|
13202
|
+
{ id: "lb2", label: "Arbeiten mit Papierwerkstoffen", hours: 28 },
|
|
13203
|
+
{ id: "lb3", label: "Arbeiten mit plastischen Massen", hours: 28 }
|
|
13204
|
+
],
|
|
11936
13205
|
"realschule|9|mathematik|wpfg1": [
|
|
11937
13206
|
{ id: "lb1", label: "Reelle Zahlen", hours: 10 },
|
|
11938
13207
|
{ id: "lb2", label: "Zentrische Streckung", hours: 17 },
|
|
@@ -11974,6 +13243,24 @@ var LEHRPLANPLUS_BAYERN_MANIFEST = {
|
|
|
11974
13243
|
]
|
|
11975
13244
|
},
|
|
11976
13245
|
contentUrls: {
|
|
13246
|
+
"realschule|5|biologie": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/biologie/inhalt/fachlehrplaene",
|
|
13247
|
+
"realschule|5|deutsch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/deutsch/inhalt/fachlehrplaene",
|
|
13248
|
+
"realschule|5|englisch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/englisch/inhalt/fachlehrplaene",
|
|
13249
|
+
"realschule|5|ethik": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/ethik/inhalt/fachlehrplaene",
|
|
13250
|
+
"realschule|5|evangelische-religionslehre": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/evangelische-religionslehre/inhalt/fachlehrplaene",
|
|
13251
|
+
"realschule|5|geographie": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/geographie/inhalt/fachlehrplaene",
|
|
13252
|
+
"realschule|5|it": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/it/inhalt/fachlehrplaene",
|
|
13253
|
+
"realschule|5|iu": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/iu/inhalt/fachlehrplaene",
|
|
13254
|
+
"realschule|5|ir": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/ir/inhalt/fachlehrplaene",
|
|
13255
|
+
"realschule|5|katholische-religionslehre": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/katholische-religionslehre/inhalt/fachlehrplaene",
|
|
13256
|
+
"realschule|5|kunst": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/kunst/inhalt/fachlehrplaene",
|
|
13257
|
+
"realschule|5|mathematik": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/mathematik/inhalt/fachlehrplaene",
|
|
13258
|
+
"realschule|5|musik": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/musik/inhalt/fachlehrplaene",
|
|
13259
|
+
"realschule|5|or": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/or/inhalt/fachlehrplaene",
|
|
13260
|
+
"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",
|
|
13261
|
+
"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",
|
|
13262
|
+
"realschule|5|textiles-gestalten": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/textiles-gestalten/inhalt/fachlehrplaene",
|
|
13263
|
+
"realschule|5|werken": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/5/fach/werken/inhalt/fachlehrplaene",
|
|
11977
13264
|
"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",
|
|
11978
13265
|
"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",
|
|
11979
13266
|
"realschule|9|deutsch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/deutsch/inhalt/fachlehrplaene",
|
|
@@ -11985,14 +13272,103 @@ var LEHRPLANPLUS_BAYERN_MANIFEST = {
|
|
|
11985
13272
|
function levelKey(schoolType, grade, subject, track) {
|
|
11986
13273
|
return track ? `${schoolType}|${grade}|${subject}|${track}` : `${schoolType}|${grade}|${subject}`;
|
|
11987
13274
|
}
|
|
11988
|
-
|
|
11989
|
-
id
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
13275
|
+
function parseHtmlSections(html) {
|
|
13276
|
+
const chunks = html.split('<div id="thema_');
|
|
13277
|
+
const sections = [];
|
|
13278
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
13279
|
+
const chunk = chunks[i];
|
|
13280
|
+
const quoteIdx = chunk.indexOf('"');
|
|
13281
|
+
if (quoteIdx === -1) continue;
|
|
13282
|
+
const id = chunk.slice(0, quoteIdx);
|
|
13283
|
+
const classStart = chunk.indexOf('class="');
|
|
13284
|
+
if (classStart === -1) continue;
|
|
13285
|
+
const classEnd = chunk.indexOf('"', classStart + 7);
|
|
13286
|
+
const classContent = chunk.slice(classStart + 7, classEnd);
|
|
13287
|
+
const lvlMatch = classContent.match(/headline_lvl(\d+)/);
|
|
13288
|
+
if (!lvlMatch) continue;
|
|
13289
|
+
const level = parseInt(lvlMatch[1], 10);
|
|
13290
|
+
const contentHtml = chunk.slice(classEnd + 1);
|
|
13291
|
+
const headerMatch = contentHtml.match(
|
|
13292
|
+
/<a[^>]*class="paragraph_toggle"[^>]*>([\s\S]*?)<\/a>/i
|
|
13293
|
+
);
|
|
13294
|
+
const headerText = headerMatch ? cleanHtmlText12(headerMatch[1]).trim() : "";
|
|
13295
|
+
sections.push({
|
|
13296
|
+
id,
|
|
13297
|
+
level,
|
|
13298
|
+
headerText,
|
|
13299
|
+
contentHtml
|
|
13300
|
+
});
|
|
13301
|
+
}
|
|
13302
|
+
return sections;
|
|
13303
|
+
}
|
|
13304
|
+
function resolveTopicLabel(topicId) {
|
|
13305
|
+
for (const key of Object.keys(LEHRPLANPLUS_BAYERN_MANIFEST.topics)) {
|
|
13306
|
+
const list = LEHRPLANPLUS_BAYERN_MANIFEST.topics[key];
|
|
13307
|
+
const match = list.find(
|
|
13308
|
+
(t2) => t2.id === topicId || `${key}#${t2.id}` === topicId
|
|
13309
|
+
);
|
|
13310
|
+
if (match) {
|
|
13311
|
+
return match.label;
|
|
13312
|
+
}
|
|
13313
|
+
}
|
|
13314
|
+
return null;
|
|
13315
|
+
}
|
|
13316
|
+
function findTopicSectionHtml(sections, topicId) {
|
|
13317
|
+
const label = resolveTopicLabel(topicId);
|
|
13318
|
+
if (!label) return null;
|
|
13319
|
+
const normalizedLabel = normalizeForComparison12(label);
|
|
13320
|
+
const lvl1Index = sections.findIndex((s) => {
|
|
13321
|
+
if (s.level !== 1) return false;
|
|
13322
|
+
const normalizedHeader = normalizeForComparison12(s.headerText);
|
|
13323
|
+
return normalizedHeader.includes(normalizedLabel);
|
|
13324
|
+
});
|
|
13325
|
+
if (lvl1Index === -1) return null;
|
|
13326
|
+
const collectedSections = [sections[lvl1Index]];
|
|
13327
|
+
for (let i = lvl1Index + 1; i < sections.length; i++) {
|
|
13328
|
+
if (sections[i].level === 1) {
|
|
13329
|
+
break;
|
|
13330
|
+
}
|
|
13331
|
+
collectedSections.push(sections[i]);
|
|
13332
|
+
}
|
|
13333
|
+
return collectedSections.map((s) => s.contentHtml).join("\n");
|
|
13334
|
+
}
|
|
13335
|
+
function isExcludedCompetenceItem(liAttrs, text) {
|
|
13336
|
+
if (/plus_servicematerialien|plus_ueberg_ziele/i.test(liAttrs)) {
|
|
13337
|
+
return true;
|
|
13338
|
+
}
|
|
13339
|
+
const normalized = text.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").trim();
|
|
13340
|
+
if (/^\+?\s*servicematerialien/.test(normalized)) return true;
|
|
13341
|
+
if (/^\+?\s*uebergreifende\s+ziele/.test(normalized)) return true;
|
|
13342
|
+
return false;
|
|
13343
|
+
}
|
|
13344
|
+
function extractCompetenceItems(topicHtml) {
|
|
13345
|
+
const items = [];
|
|
13346
|
+
const abschBlocks = topicHtml.match(/<div class="thema_absch">[\s\S]*?<\/div>/gi) ?? [];
|
|
13347
|
+
for (const block of abschBlocks) {
|
|
13348
|
+
const liRegex = /<li([^>]*)>([\s\S]*?)<\/li>/gi;
|
|
13349
|
+
let match = liRegex.exec(block);
|
|
13350
|
+
while (match !== null) {
|
|
13351
|
+
const text = cleanHtmlText12(match[2]).trim();
|
|
13352
|
+
if (text.length > 0 && !isExcludedCompetenceItem(match[1], text)) {
|
|
13353
|
+
items.push(text);
|
|
13354
|
+
}
|
|
13355
|
+
match = liRegex.exec(block);
|
|
13356
|
+
}
|
|
13357
|
+
}
|
|
13358
|
+
return items;
|
|
13359
|
+
}
|
|
13360
|
+
function truncateLabel(text, maxLen = 72) {
|
|
13361
|
+
if (text.length <= maxLen) return text;
|
|
13362
|
+
return `${text.slice(0, maxLen - 1).trim()}\u2026`;
|
|
13363
|
+
}
|
|
13364
|
+
var lehrplanplusBayernProvider = {
|
|
13365
|
+
id: "lehrplanplus-bayern",
|
|
13366
|
+
country: "DE",
|
|
13367
|
+
countryLabel: "Deutschland",
|
|
13368
|
+
region: "BY",
|
|
13369
|
+
regionLabel: "Bayern",
|
|
13370
|
+
label: "LehrplanPLUS (Bayern)",
|
|
13371
|
+
listSchoolTypes() {
|
|
11996
13372
|
return LEHRPLANPLUS_BAYERN_MANIFEST.schoolTypes;
|
|
11997
13373
|
},
|
|
11998
13374
|
listGrades(schoolType) {
|
|
@@ -12030,68 +13406,42 @@ var lehrplanplusBayernProvider = {
|
|
|
12030
13406
|
};
|
|
12031
13407
|
},
|
|
12032
13408
|
extractTopics(html, topicIds) {
|
|
12033
|
-
const
|
|
12034
|
-
const sections = [];
|
|
12035
|
-
for (let i = 1; i < chunks.length; i++) {
|
|
12036
|
-
const chunk = chunks[i];
|
|
12037
|
-
const quoteIdx = chunk.indexOf('"');
|
|
12038
|
-
if (quoteIdx === -1) continue;
|
|
12039
|
-
const id = chunk.slice(0, quoteIdx);
|
|
12040
|
-
const classStart = chunk.indexOf('class="');
|
|
12041
|
-
if (classStart === -1) continue;
|
|
12042
|
-
const classEnd = chunk.indexOf('"', classStart + 7);
|
|
12043
|
-
const classContent = chunk.slice(classStart + 7, classEnd);
|
|
12044
|
-
const lvlMatch = classContent.match(/headline_lvl(\d+)/);
|
|
12045
|
-
if (!lvlMatch) continue;
|
|
12046
|
-
const level = parseInt(lvlMatch[1], 10);
|
|
12047
|
-
const contentHtml = chunk.slice(classEnd + 1);
|
|
12048
|
-
const headerMatch = contentHtml.match(
|
|
12049
|
-
/<a[^>]*class="paragraph_toggle"[^>]*>([\s\S]*?)<\/a>/i
|
|
12050
|
-
);
|
|
12051
|
-
const headerText = headerMatch ? cleanHtmlText12(headerMatch[1]).trim() : "";
|
|
12052
|
-
sections.push({
|
|
12053
|
-
id,
|
|
12054
|
-
level,
|
|
12055
|
-
headerText,
|
|
12056
|
-
contentHtml
|
|
12057
|
-
});
|
|
12058
|
-
}
|
|
13409
|
+
const sections = parseHtmlSections(html);
|
|
12059
13410
|
const results = {};
|
|
12060
13411
|
for (const topicId of topicIds) {
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
const match = list.find(
|
|
12065
|
-
(t2) => t2.id === topicId || `${key}#${t2.id}` === topicId
|
|
12066
|
-
);
|
|
12067
|
-
if (match) {
|
|
12068
|
-
label = match.label;
|
|
12069
|
-
break;
|
|
12070
|
-
}
|
|
13412
|
+
const topicHtml = findTopicSectionHtml(sections, topicId);
|
|
13413
|
+
if (topicHtml) {
|
|
13414
|
+
results[topicId] = cleanHtmlText12(topicHtml);
|
|
12071
13415
|
}
|
|
12072
|
-
if (!label) {
|
|
12073
|
-
continue;
|
|
12074
|
-
}
|
|
12075
|
-
const normalizedLabel = normalizeForComparison12(label);
|
|
12076
|
-
const lvl1Index = sections.findIndex((s) => {
|
|
12077
|
-
if (s.level !== 1) return false;
|
|
12078
|
-
const normalizedHeader = normalizeForComparison12(s.headerText);
|
|
12079
|
-
return normalizedHeader.includes(normalizedLabel);
|
|
12080
|
-
});
|
|
12081
|
-
if (lvl1Index === -1) {
|
|
12082
|
-
continue;
|
|
12083
|
-
}
|
|
12084
|
-
const collectedSections = [sections[lvl1Index]];
|
|
12085
|
-
for (let i = lvl1Index + 1; i < sections.length; i++) {
|
|
12086
|
-
if (sections[i].level === 1) {
|
|
12087
|
-
break;
|
|
12088
|
-
}
|
|
12089
|
-
collectedSections.push(sections[i]);
|
|
12090
|
-
}
|
|
12091
|
-
const fullHtml = collectedSections.map((s) => s.contentHtml).join("\n");
|
|
12092
|
-
results[topicId] = cleanHtmlText12(fullHtml);
|
|
12093
13416
|
}
|
|
12094
13417
|
return results;
|
|
13418
|
+
},
|
|
13419
|
+
extractSubTopics(html, topicId) {
|
|
13420
|
+
const sections = parseHtmlSections(html);
|
|
13421
|
+
const topicHtml = findTopicSectionHtml(sections, topicId);
|
|
13422
|
+
if (!topicHtml) return [];
|
|
13423
|
+
const header = sections.find((s) => {
|
|
13424
|
+
if (s.level !== 1) return false;
|
|
13425
|
+
const label = resolveTopicLabel(topicId);
|
|
13426
|
+
if (!label) return false;
|
|
13427
|
+
return normalizeForComparison12(s.headerText).includes(
|
|
13428
|
+
normalizeForComparison12(label)
|
|
13429
|
+
);
|
|
13430
|
+
});
|
|
13431
|
+
const headerText = header?.headerText ?? resolveTopicLabel(topicId) ?? "";
|
|
13432
|
+
const competenceItems = extractCompetenceItems(topicHtml);
|
|
13433
|
+
if (competenceItems.length === 0) return [];
|
|
13434
|
+
return competenceItems.map((item, index) => {
|
|
13435
|
+
const text = `${headerText}
|
|
13436
|
+
|
|
13437
|
+
${item}`;
|
|
13438
|
+
return {
|
|
13439
|
+
id: `ku${index + 1}`,
|
|
13440
|
+
label: truncateLabel(item),
|
|
13441
|
+
textLength: text.length,
|
|
13442
|
+
text
|
|
13443
|
+
};
|
|
13444
|
+
});
|
|
12095
13445
|
}
|
|
12096
13446
|
};
|
|
12097
13447
|
function cleanHtmlText12(html) {
|
|
@@ -12630,12 +13980,133 @@ function getCurriculumProvider(id) {
|
|
|
12630
13980
|
return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
|
|
12631
13981
|
}
|
|
12632
13982
|
|
|
13983
|
+
// src/cli/llm/capability-probe.ts
|
|
13984
|
+
init_kernel();
|
|
13985
|
+
var EMBEDDING_MODEL_HINTS = [
|
|
13986
|
+
"embed",
|
|
13987
|
+
"text-embedding",
|
|
13988
|
+
"bge-",
|
|
13989
|
+
"gte-",
|
|
13990
|
+
"nomic",
|
|
13991
|
+
"mxbai"
|
|
13992
|
+
];
|
|
13993
|
+
var VISION_MODEL_HINTS = [
|
|
13994
|
+
"vision",
|
|
13995
|
+
"-vl",
|
|
13996
|
+
"vl-",
|
|
13997
|
+
"vlm",
|
|
13998
|
+
"llava",
|
|
13999
|
+
"gpt-4o",
|
|
14000
|
+
"gpt-4.1",
|
|
14001
|
+
"gpt-5",
|
|
14002
|
+
"gemini",
|
|
14003
|
+
"pixtral",
|
|
14004
|
+
"minicpm-v",
|
|
14005
|
+
"internvl",
|
|
14006
|
+
"moondream",
|
|
14007
|
+
"llama-3.2",
|
|
14008
|
+
"llama3.2",
|
|
14009
|
+
// Xiaomi MiMo(-VL) is multimodal; the plain "mimo-v*" tag carries no "-vl".
|
|
14010
|
+
"mimo"
|
|
14011
|
+
];
|
|
14012
|
+
function matchesAny(id, hints) {
|
|
14013
|
+
const lower = id.toLowerCase();
|
|
14014
|
+
return hints.some((hint) => lower.includes(hint));
|
|
14015
|
+
}
|
|
14016
|
+
function catalogHasModel(catalog, model) {
|
|
14017
|
+
const lower = model.toLowerCase();
|
|
14018
|
+
return catalog.some((id) => id.toLowerCase() === lower);
|
|
14019
|
+
}
|
|
14020
|
+
function classifyCapabilities(entry, catalog, catalogKnown, dimProbeEmbedding = false) {
|
|
14021
|
+
const detected = emptyCapabilityFlags();
|
|
14022
|
+
if (entry.apiFlavor === "anthropic-messages") {
|
|
14023
|
+
detected.text = true;
|
|
14024
|
+
detected.image = true;
|
|
14025
|
+
return detected;
|
|
14026
|
+
}
|
|
14027
|
+
const looksEmbedding = matchesAny(entry.model, EMBEDDING_MODEL_HINTS);
|
|
14028
|
+
const looksVision = matchesAny(entry.model, VISION_MODEL_HINTS);
|
|
14029
|
+
const inCatalog = catalogHasModel(catalog, entry.model);
|
|
14030
|
+
detected.embedding = looksEmbedding || dimProbeEmbedding;
|
|
14031
|
+
detected.image = looksVision;
|
|
14032
|
+
detected.text = !detected.embedding && (inCatalog || !catalogKnown);
|
|
14033
|
+
return detected;
|
|
14034
|
+
}
|
|
14035
|
+
function resolveApiKey(apiKeyRef) {
|
|
14036
|
+
if (!apiKeyRef) return DEFAULT_LLM_API_KEY;
|
|
14037
|
+
return getProviderApiKey(apiKeyRef) ?? DEFAULT_LLM_API_KEY;
|
|
14038
|
+
}
|
|
14039
|
+
async function probeModelCapabilities(entry, opts = {}) {
|
|
14040
|
+
const apiKey = resolveApiKey(entry.apiKeyRef);
|
|
14041
|
+
if (entry.apiFlavor === "anthropic-messages") {
|
|
14042
|
+
const online2 = await isLlmOnline(entry.url);
|
|
14043
|
+
return {
|
|
14044
|
+
reachable: online2,
|
|
14045
|
+
catalog: [],
|
|
14046
|
+
detected: classifyCapabilities(entry, [], false)
|
|
14047
|
+
};
|
|
14048
|
+
}
|
|
14049
|
+
const online = await isLlmOnline(entry.url);
|
|
14050
|
+
if (!online) {
|
|
14051
|
+
return { reachable: false, catalog: [], detected: emptyCapabilityFlags() };
|
|
14052
|
+
}
|
|
14053
|
+
const catalog = await getAvailableModels(entry.url, apiKey);
|
|
14054
|
+
const catalogKnown = catalog.length > 0;
|
|
14055
|
+
let dimProbeEmbedding = false;
|
|
14056
|
+
const looksEmbedding = matchesAny(entry.model, EMBEDDING_MODEL_HINTS);
|
|
14057
|
+
if (opts.embeddingDimProbe && !catalogKnown && !looksEmbedding) {
|
|
14058
|
+
try {
|
|
14059
|
+
const [vector] = await embedTexts(
|
|
14060
|
+
{ url: entry.url, model: entry.model, apiKey },
|
|
14061
|
+
["capability probe"]
|
|
14062
|
+
);
|
|
14063
|
+
dimProbeEmbedding = Array.isArray(vector) && vector.length > 0;
|
|
14064
|
+
} catch {
|
|
14065
|
+
dimProbeEmbedding = false;
|
|
14066
|
+
}
|
|
14067
|
+
}
|
|
14068
|
+
return {
|
|
14069
|
+
reachable: true,
|
|
14070
|
+
catalog,
|
|
14071
|
+
detected: classifyCapabilities(
|
|
14072
|
+
entry,
|
|
14073
|
+
catalog,
|
|
14074
|
+
catalogKnown,
|
|
14075
|
+
dimProbeEmbedding
|
|
14076
|
+
)
|
|
14077
|
+
};
|
|
14078
|
+
}
|
|
14079
|
+
function reconcileCapabilities(userSelected, detected) {
|
|
14080
|
+
const result = emptyCapabilityFlags();
|
|
14081
|
+
for (const key of Object.keys(result)) {
|
|
14082
|
+
result[key] = userSelected[key] && detected[key];
|
|
14083
|
+
}
|
|
14084
|
+
return result;
|
|
14085
|
+
}
|
|
14086
|
+
function validateModelSave(entry, probe, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
14087
|
+
if (!probe.reachable) {
|
|
14088
|
+
return {
|
|
14089
|
+
ok: false,
|
|
14090
|
+
error: "Endpoint is unreachable \u2014 cannot verify capabilities. Bring it online and retry."
|
|
14091
|
+
};
|
|
14092
|
+
}
|
|
14093
|
+
return {
|
|
14094
|
+
ok: true,
|
|
14095
|
+
entry: {
|
|
14096
|
+
...entry,
|
|
14097
|
+
capabilities: reconcileCapabilities(entry.capabilities, probe.detected),
|
|
14098
|
+
detectedCapabilities: probe.detected,
|
|
14099
|
+
probedAt: now()
|
|
14100
|
+
}
|
|
14101
|
+
};
|
|
14102
|
+
}
|
|
14103
|
+
|
|
12633
14104
|
// src/cli/llm/vision.ts
|
|
12634
14105
|
init_kernel();
|
|
12635
14106
|
import { randomBytes } from "crypto";
|
|
12636
|
-
import { readFileSync as
|
|
14107
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
12637
14108
|
import { tmpdir as tmpdir2 } from "os";
|
|
12638
|
-
import { basename as basename4, join as
|
|
14109
|
+
import { basename as basename4, join as join19 } from "path";
|
|
12639
14110
|
var LANGUAGE_NAMES2 = {
|
|
12640
14111
|
en: "English",
|
|
12641
14112
|
de: "German",
|
|
@@ -12671,13 +14142,13 @@ async function observeUiSnapshotViaLLM(db, input) {
|
|
|
12671
14142
|
const imageUrls = [];
|
|
12672
14143
|
const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input.imagePath);
|
|
12673
14144
|
if (isVideo) {
|
|
12674
|
-
const { mkdirSync:
|
|
14145
|
+
const { mkdirSync: mkdirSync15, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
|
|
12675
14146
|
const { execSync: execSync5 } = await import("child_process");
|
|
12676
|
-
const tempDir =
|
|
14147
|
+
const tempDir = join19(
|
|
12677
14148
|
tmpdir2(),
|
|
12678
14149
|
`zam-frames-${randomBytes(4).toString("hex")}`
|
|
12679
14150
|
);
|
|
12680
|
-
|
|
14151
|
+
mkdirSync15(tempDir, { recursive: true });
|
|
12681
14152
|
try {
|
|
12682
14153
|
execSync5(
|
|
12683
14154
|
`ffmpeg -i "${input.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
|
|
@@ -12706,7 +14177,7 @@ async function observeUiSnapshotViaLLM(db, input) {
|
|
|
12706
14177
|
}
|
|
12707
14178
|
}
|
|
12708
14179
|
for (const file of sampledFiles) {
|
|
12709
|
-
const bytes =
|
|
14180
|
+
const bytes = readFileSync14(join19(tempDir, file));
|
|
12710
14181
|
imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
|
|
12711
14182
|
}
|
|
12712
14183
|
} finally {
|
|
@@ -12716,7 +14187,7 @@ async function observeUiSnapshotViaLLM(db, input) {
|
|
|
12716
14187
|
}
|
|
12717
14188
|
}
|
|
12718
14189
|
} else {
|
|
12719
|
-
const imageBytes =
|
|
14190
|
+
const imageBytes = readFileSync14(input.imagePath);
|
|
12720
14191
|
const ext = input.imagePath.split(".").pop()?.toLowerCase();
|
|
12721
14192
|
const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
12722
14193
|
imageUrls.push(`data:${mime};base64,${imageBytes.toString("base64")}`);
|
|
@@ -13100,6 +14571,12 @@ function bindRoleProviders(roles, role, primary, fallback) {
|
|
|
13100
14571
|
if (fallback) binding.fallback = fallback;
|
|
13101
14572
|
return { ...roles, [role]: binding };
|
|
13102
14573
|
}
|
|
14574
|
+
function unbindRole(roles, role) {
|
|
14575
|
+
if (!(role in roles)) return roles;
|
|
14576
|
+
const next = { ...roles };
|
|
14577
|
+
delete next[role];
|
|
14578
|
+
return next;
|
|
14579
|
+
}
|
|
13103
14580
|
function maskSecret(key) {
|
|
13104
14581
|
return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
|
|
13105
14582
|
}
|
|
@@ -13182,36 +14659,36 @@ async function withProviderScope(machine, action) {
|
|
|
13182
14659
|
|
|
13183
14660
|
// src/cli/provisioning/index.ts
|
|
13184
14661
|
import {
|
|
13185
|
-
existsSync as
|
|
13186
|
-
lstatSync,
|
|
13187
|
-
mkdirSync as
|
|
13188
|
-
readFileSync as
|
|
14662
|
+
existsSync as existsSync17,
|
|
14663
|
+
lstatSync as lstatSync2,
|
|
14664
|
+
mkdirSync as mkdirSync13,
|
|
14665
|
+
readFileSync as readFileSync15,
|
|
13189
14666
|
realpathSync,
|
|
13190
14667
|
rmSync as rmSync2,
|
|
13191
14668
|
symlinkSync,
|
|
13192
|
-
writeFileSync as
|
|
14669
|
+
writeFileSync as writeFileSync11
|
|
13193
14670
|
} from "fs";
|
|
13194
|
-
import { basename as basename5, dirname as
|
|
13195
|
-
import { fileURLToPath as
|
|
13196
|
-
var
|
|
13197
|
-
|
|
13198
|
-
|
|
13199
|
-
].find((candidate) =>
|
|
14671
|
+
import { basename as basename5, dirname as dirname10, join as join20 } from "path";
|
|
14672
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
14673
|
+
var packageRoot3 = [
|
|
14674
|
+
fileURLToPath5(new URL("../..", import.meta.url)),
|
|
14675
|
+
fileURLToPath5(new URL("../../..", import.meta.url))
|
|
14676
|
+
].find((candidate) => existsSync17(join20(candidate, "package.json"))) ?? fileURLToPath5(new URL("../..", import.meta.url));
|
|
13200
14677
|
var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
|
|
13201
14678
|
var SKILL_PAIRS = [
|
|
13202
14679
|
{
|
|
13203
|
-
from:
|
|
13204
|
-
to:
|
|
14680
|
+
from: join20(packageRoot3, ".claude", "skills", "zam", "SKILL.md"),
|
|
14681
|
+
to: join20(".claude", "skills", "zam", "SKILL.md"),
|
|
13205
14682
|
agents: ["claude", "copilot"]
|
|
13206
14683
|
},
|
|
13207
14684
|
{
|
|
13208
|
-
from:
|
|
13209
|
-
to:
|
|
14685
|
+
from: join20(packageRoot3, ".agent", "skills", "zam", "SKILL.md"),
|
|
14686
|
+
to: join20(".agent", "skills", "zam", "SKILL.md"),
|
|
13210
14687
|
agents: ["agent"]
|
|
13211
14688
|
},
|
|
13212
14689
|
{
|
|
13213
|
-
from:
|
|
13214
|
-
to:
|
|
14690
|
+
from: join20(packageRoot3, ".agents", "skills", "zam", "SKILL.md"),
|
|
14691
|
+
to: join20(".agents", "skills", "zam", "SKILL.md"),
|
|
13215
14692
|
agents: ["codex"]
|
|
13216
14693
|
}
|
|
13217
14694
|
];
|
|
@@ -13241,7 +14718,7 @@ function parseSetupAgents(value) {
|
|
|
13241
14718
|
}
|
|
13242
14719
|
function pathExists(path) {
|
|
13243
14720
|
try {
|
|
13244
|
-
|
|
14721
|
+
lstatSync2(path);
|
|
13245
14722
|
return true;
|
|
13246
14723
|
} catch {
|
|
13247
14724
|
return false;
|
|
@@ -13249,7 +14726,7 @@ function pathExists(path) {
|
|
|
13249
14726
|
}
|
|
13250
14727
|
function isSymbolicLink(path) {
|
|
13251
14728
|
try {
|
|
13252
|
-
return
|
|
14729
|
+
return lstatSync2(path).isSymbolicLink();
|
|
13253
14730
|
} catch {
|
|
13254
14731
|
return false;
|
|
13255
14732
|
}
|
|
@@ -13270,16 +14747,16 @@ function linkPointsTo(sourceDir, destinationDir) {
|
|
|
13270
14747
|
}
|
|
13271
14748
|
function isZamSkillCopy(destinationDir) {
|
|
13272
14749
|
try {
|
|
13273
|
-
if (!
|
|
13274
|
-
const skillFile =
|
|
13275
|
-
if (!
|
|
13276
|
-
return /^name:\s*zam\s*$/m.test(
|
|
14750
|
+
if (!lstatSync2(destinationDir).isDirectory()) return false;
|
|
14751
|
+
const skillFile = join20(destinationDir, "SKILL.md");
|
|
14752
|
+
if (!existsSync17(skillFile)) return false;
|
|
14753
|
+
return /^name:\s*zam\s*$/m.test(readFileSync15(skillFile, "utf8"));
|
|
13277
14754
|
} catch {
|
|
13278
14755
|
return false;
|
|
13279
14756
|
}
|
|
13280
14757
|
}
|
|
13281
14758
|
function classifySkillDestination(sourceDir, destinationDir) {
|
|
13282
|
-
if (!
|
|
14759
|
+
if (!existsSync17(sourceDir)) return "source-missing";
|
|
13283
14760
|
if (!isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir)) {
|
|
13284
14761
|
return "source-directory";
|
|
13285
14762
|
}
|
|
@@ -13296,9 +14773,9 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
|
|
|
13296
14773
|
};
|
|
13297
14774
|
for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
|
|
13298
14775
|
if (!pairAgents.some((agent) => agents.has(agent))) continue;
|
|
13299
|
-
const sourceDir =
|
|
13300
|
-
const destinationDir =
|
|
13301
|
-
const label =
|
|
14776
|
+
const sourceDir = dirname10(from);
|
|
14777
|
+
const destinationDir = dirname10(join20(cwd, to));
|
|
14778
|
+
const label = dirname10(to);
|
|
13302
14779
|
const state = classifySkillDestination(sourceDir, destinationDir);
|
|
13303
14780
|
if (state === "source-missing") {
|
|
13304
14781
|
if (!opts.quiet) {
|
|
@@ -13355,7 +14832,7 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
|
|
|
13355
14832
|
if (destinationExists) {
|
|
13356
14833
|
rmSync2(destinationDir, { recursive: true, force: true });
|
|
13357
14834
|
}
|
|
13358
|
-
|
|
14835
|
+
mkdirSync13(dirname10(destinationDir), { recursive: true });
|
|
13359
14836
|
symlinkSync(
|
|
13360
14837
|
sourceDir,
|
|
13361
14838
|
destinationDir,
|
|
@@ -13371,8 +14848,8 @@ function inspectSkillLinks(cwd = process.cwd(), agents = parseSetupAgents()) {
|
|
|
13371
14848
|
const results = [];
|
|
13372
14849
|
for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
|
|
13373
14850
|
if (!pairAgents.some((agent) => agents.has(agent))) continue;
|
|
13374
|
-
const sourceDir =
|
|
13375
|
-
const destinationDir =
|
|
14851
|
+
const sourceDir = dirname10(from);
|
|
14852
|
+
const destinationDir = dirname10(join20(cwd, to));
|
|
13376
14853
|
results.push({
|
|
13377
14854
|
agents: pairAgents,
|
|
13378
14855
|
source: sourceDir,
|
|
@@ -13416,13 +14893,13 @@ async function resolveUser(opts, db, resolveOpts) {
|
|
|
13416
14893
|
}
|
|
13417
14894
|
|
|
13418
14895
|
// src/cli/workspaces/backup.ts
|
|
13419
|
-
import { mkdirSync as
|
|
13420
|
-
import { join as
|
|
14896
|
+
import { mkdirSync as mkdirSync14 } from "fs";
|
|
14897
|
+
import { join as join21 } from "path";
|
|
13421
14898
|
async function backupDatabaseTo(db, targetDir) {
|
|
13422
|
-
const backupDir =
|
|
13423
|
-
|
|
14899
|
+
const backupDir = join21(targetDir, "zam-backups");
|
|
14900
|
+
mkdirSync14(backupDir, { recursive: true });
|
|
13424
14901
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
13425
|
-
const dest =
|
|
14902
|
+
const dest = join21(backupDir, `zam-${stamp}.db`);
|
|
13426
14903
|
await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
13427
14904
|
return dest;
|
|
13428
14905
|
}
|
|
@@ -13553,20 +15030,20 @@ bridgeCommand.command("workspace-info").description("Report the workspace dir, i
|
|
|
13553
15030
|
activeWorkspace,
|
|
13554
15031
|
workspaceDir: activeWorkspace.path,
|
|
13555
15032
|
defaultWorkspaceDir: defaultWorkspaceDir(),
|
|
13556
|
-
dataDir:
|
|
15033
|
+
dataDir: join22(homedir14(), ".zam")
|
|
13557
15034
|
});
|
|
13558
15035
|
});
|
|
13559
15036
|
});
|
|
13560
15037
|
function provisionConfiguredWorkspaces() {
|
|
13561
15038
|
return getConfiguredWorkspaces().flatMap(
|
|
13562
|
-
(workspace) =>
|
|
15039
|
+
(workspace) => existsSync18(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
|
|
13563
15040
|
);
|
|
13564
15041
|
}
|
|
13565
15042
|
function buildWorkspaceLinkHealth(workspaces) {
|
|
13566
15043
|
const agents = parseSetupAgents();
|
|
13567
15044
|
const map = {};
|
|
13568
15045
|
for (const workspace of workspaces) {
|
|
13569
|
-
if (!
|
|
15046
|
+
if (!existsSync18(workspace.path)) continue;
|
|
13570
15047
|
const links = inspectSkillLinks(workspace.path, agents);
|
|
13571
15048
|
map[workspace.id] = {
|
|
13572
15049
|
health: summarizeSkillLinkHealth(links),
|
|
@@ -13596,7 +15073,7 @@ bridgeCommand.command("workspace-list").description("List configured ZAM workspa
|
|
|
13596
15073
|
activeWorkspace,
|
|
13597
15074
|
workspaceDir: activeWorkspace.path,
|
|
13598
15075
|
defaultWorkspaceDir: defaultWorkspaceDir(),
|
|
13599
|
-
dataDir:
|
|
15076
|
+
dataDir: join22(homedir14(), ".zam"),
|
|
13600
15077
|
linkHealth: buildWorkspaceLinkHealth(workspaces)
|
|
13601
15078
|
});
|
|
13602
15079
|
});
|
|
@@ -13611,7 +15088,7 @@ bridgeCommand.command("workspace-repair-links").description(
|
|
|
13611
15088
|
if (!id) jsonError("A non-empty --id is required");
|
|
13612
15089
|
const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
|
|
13613
15090
|
if (!workspace) jsonError(`Workspace "${id}" is not configured`);
|
|
13614
|
-
if (!
|
|
15091
|
+
if (!existsSync18(workspace.path)) {
|
|
13615
15092
|
jsonError(`Workspace path does not exist: ${workspace.path}`);
|
|
13616
15093
|
}
|
|
13617
15094
|
const agents = parseSetupAgents(opts.agents);
|
|
@@ -13642,8 +15119,8 @@ function parseBridgeWorkspaceKind(value) {
|
|
|
13642
15119
|
bridgeCommand.command("workspace-add").description("Register an existing directory as a ZAM workspace (JSON)").requiredOption("--path <dir>", "Existing workspace/repository directory").option("--id <id>", "Workspace id").option("--label <label>", "Human-readable label").option("--kind <kind>", "Workspace kind", "custom").action(async (opts) => {
|
|
13643
15120
|
const raw = String(opts.path ?? "").trim();
|
|
13644
15121
|
if (!raw) jsonError("A non-empty --path is required");
|
|
13645
|
-
const path =
|
|
13646
|
-
if (!
|
|
15122
|
+
const path = resolve5(raw);
|
|
15123
|
+
if (!existsSync18(path)) jsonError(`Workspace path does not exist: ${path}`);
|
|
13647
15124
|
const id = opts.id ? String(opts.id).trim() : void 0;
|
|
13648
15125
|
if (opts.id && !id) jsonError("A non-empty --id is required");
|
|
13649
15126
|
const kind = parseBridgeWorkspaceKind(opts.kind);
|
|
@@ -13687,8 +15164,8 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
|
|
|
13687
15164
|
bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
|
|
13688
15165
|
const raw = String(opts.dir ?? "").trim();
|
|
13689
15166
|
if (!raw) jsonError("A non-empty --dir is required");
|
|
13690
|
-
const dir =
|
|
13691
|
-
if (!
|
|
15167
|
+
const dir = resolve5(raw);
|
|
15168
|
+
if (!existsSync18(dir)) jsonError(`Workspace path does not exist: ${dir}`);
|
|
13692
15169
|
const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
|
|
13693
15170
|
await withDb2(async (db) => {
|
|
13694
15171
|
const workspace = await activateWorkspacePath(db, dir);
|
|
@@ -13751,7 +15228,7 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
|
|
|
13751
15228
|
jsonError(`Workspace is not configured: ${opts.workspace}`);
|
|
13752
15229
|
}
|
|
13753
15230
|
const activeWorkspace = await ensureActiveWorkspace(db);
|
|
13754
|
-
const workspace = opts.dir ?
|
|
15231
|
+
const workspace = opts.dir ? existsSync18(opts.dir) ? opts.dir : homedir14() : existingWorkspaceDirOrHome(configuredWorkspace ?? activeWorkspace);
|
|
13755
15232
|
launchHarness(harness, {
|
|
13756
15233
|
executable,
|
|
13757
15234
|
workspace,
|
|
@@ -14123,7 +15600,7 @@ bridgeCommand.command("discover-skills").description(
|
|
|
14123
15600
|
"20"
|
|
14124
15601
|
).action(async (opts) => {
|
|
14125
15602
|
try {
|
|
14126
|
-
const monitorDir =
|
|
15603
|
+
const monitorDir = join22(homedir14(), ".zam", "monitor");
|
|
14127
15604
|
let files;
|
|
14128
15605
|
try {
|
|
14129
15606
|
files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -14136,7 +15613,7 @@ bridgeCommand.command("discover-skills").description(
|
|
|
14136
15613
|
return;
|
|
14137
15614
|
}
|
|
14138
15615
|
const limit = Number(opts.limit);
|
|
14139
|
-
const sorted = files.map((f) => ({ name: f, path:
|
|
15616
|
+
const sorted = files.map((f) => ({ name: f, path: join22(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
|
|
14140
15617
|
const sessionCommands = /* @__PURE__ */ new Map();
|
|
14141
15618
|
for (const file of sorted) {
|
|
14142
15619
|
const sessionId = file.name.replace(".jsonl", "");
|
|
@@ -14248,7 +15725,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
|
|
|
14248
15725
|
});
|
|
14249
15726
|
function resolveWindowsPowerShell() {
|
|
14250
15727
|
try {
|
|
14251
|
-
|
|
15728
|
+
execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
|
|
14252
15729
|
return "pwsh";
|
|
14253
15730
|
} catch {
|
|
14254
15731
|
return "powershell";
|
|
@@ -14263,7 +15740,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
|
|
|
14263
15740
|
throw new Error(`Invalid process name format: ${processName}`);
|
|
14264
15741
|
}
|
|
14265
15742
|
if (platform === "win32") {
|
|
14266
|
-
const stdout =
|
|
15743
|
+
const stdout = execFileSync4(
|
|
14267
15744
|
resolveWindowsPowerShell(),
|
|
14268
15745
|
[
|
|
14269
15746
|
"-NoProfile",
|
|
@@ -14576,13 +16053,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
|
|
|
14576
16053
|
} else if (platform === "darwin") {
|
|
14577
16054
|
if (hwnd) {
|
|
14578
16055
|
const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
|
|
14579
|
-
|
|
16056
|
+
execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
|
|
14580
16057
|
stdio: "pipe"
|
|
14581
16058
|
});
|
|
14582
16059
|
return { method: "screencapture-window", target: null };
|
|
14583
16060
|
} else if (processName) {
|
|
14584
16061
|
try {
|
|
14585
|
-
const windowId =
|
|
16062
|
+
const windowId = execFileSync4(
|
|
14586
16063
|
"osascript",
|
|
14587
16064
|
[
|
|
14588
16065
|
"-e",
|
|
@@ -14591,17 +16068,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
|
|
|
14591
16068
|
{ encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
|
|
14592
16069
|
).trim();
|
|
14593
16070
|
if (windowId && /^\d+$/.test(windowId)) {
|
|
14594
|
-
|
|
16071
|
+
execFileSync4("screencapture", ["-l", windowId, outputPath], {
|
|
14595
16072
|
stdio: "pipe"
|
|
14596
16073
|
});
|
|
14597
16074
|
return { method: "screencapture-window", target: null };
|
|
14598
16075
|
}
|
|
14599
16076
|
} catch {
|
|
14600
16077
|
}
|
|
14601
|
-
|
|
16078
|
+
execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
|
|
14602
16079
|
return { method: "screencapture-full", target: null };
|
|
14603
16080
|
} else {
|
|
14604
|
-
|
|
16081
|
+
execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
|
|
14605
16082
|
return { method: "screencapture-full", target: null };
|
|
14606
16083
|
}
|
|
14607
16084
|
} else {
|
|
@@ -14638,7 +16115,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
|
|
|
14638
16115
|
return;
|
|
14639
16116
|
}
|
|
14640
16117
|
}
|
|
14641
|
-
const outputPath = opts.image ?? opts.output ??
|
|
16118
|
+
const outputPath = opts.image ?? opts.output ?? join22(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
|
|
14642
16119
|
const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
|
|
14643
16120
|
if (!isProvided) {
|
|
14644
16121
|
const post = decidePostCapture(policy, {
|
|
@@ -14666,7 +16143,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
|
|
|
14666
16143
|
return;
|
|
14667
16144
|
}
|
|
14668
16145
|
}
|
|
14669
|
-
const imageBytes =
|
|
16146
|
+
const imageBytes = readFileSync16(outputPath);
|
|
14670
16147
|
const base64 = imageBytes.toString("base64");
|
|
14671
16148
|
jsonOut({
|
|
14672
16149
|
sessionId: opts.session ?? null,
|
|
@@ -14693,11 +16170,11 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
14693
16170
|
return;
|
|
14694
16171
|
}
|
|
14695
16172
|
const sessionId = opts.session;
|
|
14696
|
-
const statePath =
|
|
16173
|
+
const statePath = join22(tmpdir3(), `zam-recording-${sessionId}.json`);
|
|
14697
16174
|
const defaultExt = platform === "win32" ? ".mkv" : ".mov";
|
|
14698
|
-
const outputPath = opts.output ??
|
|
14699
|
-
const { existsSync:
|
|
14700
|
-
if (
|
|
16175
|
+
const outputPath = opts.output ?? join22(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
|
|
16176
|
+
const { existsSync: existsSync20, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
|
|
16177
|
+
if (existsSync20(statePath)) {
|
|
14701
16178
|
jsonOut({
|
|
14702
16179
|
sessionId,
|
|
14703
16180
|
started: false,
|
|
@@ -14705,7 +16182,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
14705
16182
|
});
|
|
14706
16183
|
return;
|
|
14707
16184
|
}
|
|
14708
|
-
const logPath =
|
|
16185
|
+
const logPath = join22(tmpdir3(), `zam-recording-${sessionId}.log`);
|
|
14709
16186
|
let logFd;
|
|
14710
16187
|
try {
|
|
14711
16188
|
logFd = openSync(logPath, "w");
|
|
@@ -14751,7 +16228,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
14751
16228
|
}
|
|
14752
16229
|
child.unref();
|
|
14753
16230
|
if (child.pid) {
|
|
14754
|
-
|
|
16231
|
+
writeFileSync12(
|
|
14755
16232
|
statePath,
|
|
14756
16233
|
JSON.stringify({
|
|
14757
16234
|
pid: child.pid,
|
|
@@ -14787,9 +16264,9 @@ bridgeCommand.command("stop-recording").description(
|
|
|
14787
16264
|
return;
|
|
14788
16265
|
}
|
|
14789
16266
|
const sessionId = opts.session;
|
|
14790
|
-
const statePath =
|
|
14791
|
-
const { existsSync:
|
|
14792
|
-
if (!
|
|
16267
|
+
const statePath = join22(tmpdir3(), `zam-recording-${sessionId}.json`);
|
|
16268
|
+
const { existsSync: existsSync20, readFileSync: readFileSync18, rmSync: rmSync4 } = await import("fs");
|
|
16269
|
+
if (!existsSync20(statePath)) {
|
|
14793
16270
|
jsonOut({
|
|
14794
16271
|
sessionId,
|
|
14795
16272
|
stopped: false,
|
|
@@ -14797,7 +16274,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
14797
16274
|
});
|
|
14798
16275
|
return;
|
|
14799
16276
|
}
|
|
14800
|
-
const state = JSON.parse(
|
|
16277
|
+
const state = JSON.parse(readFileSync18(statePath, "utf8"));
|
|
14801
16278
|
const { pid, outputPath } = state;
|
|
14802
16279
|
try {
|
|
14803
16280
|
process.kill(pid, "SIGINT");
|
|
@@ -14813,7 +16290,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
14813
16290
|
};
|
|
14814
16291
|
let attempts = 0;
|
|
14815
16292
|
while (isProcessRunning(pid) && attempts < 20) {
|
|
14816
|
-
await new Promise((
|
|
16293
|
+
await new Promise((resolve6) => setTimeout(resolve6, 250));
|
|
14817
16294
|
attempts++;
|
|
14818
16295
|
}
|
|
14819
16296
|
if (isProcessRunning(pid)) {
|
|
@@ -14826,7 +16303,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
14826
16303
|
rmSync4(statePath, { force: true });
|
|
14827
16304
|
} catch {
|
|
14828
16305
|
}
|
|
14829
|
-
if (!
|
|
16306
|
+
if (!existsSync20(outputPath)) {
|
|
14830
16307
|
jsonOut({
|
|
14831
16308
|
sessionId,
|
|
14832
16309
|
stopped: false,
|
|
@@ -15021,12 +16498,15 @@ bridgeCommand.command("provider-config-bind").description("Bind providers to an
|
|
|
15021
16498
|
await withProviderScope(machine, async (db) => {
|
|
15022
16499
|
const providers = await readScopedProviders(db, machine);
|
|
15023
16500
|
const roles = await readScopedRoles(db, machine);
|
|
15024
|
-
|
|
16501
|
+
let nextRoles = bindRoleProviders(
|
|
15025
16502
|
roles,
|
|
15026
16503
|
opts.role,
|
|
15027
16504
|
opts.primary,
|
|
15028
16505
|
opts.fallback
|
|
15029
16506
|
);
|
|
16507
|
+
if (opts.role === "recall") {
|
|
16508
|
+
nextRoles = unbindRole(nextRoles, "text");
|
|
16509
|
+
}
|
|
15030
16510
|
await writeScopedRoles(db, machine, nextRoles);
|
|
15031
16511
|
const binding = nextRoles[opts.role];
|
|
15032
16512
|
const primary = providers[opts.primary];
|
|
@@ -15058,12 +16538,202 @@ bridgeCommand.command("list-models").description("List models exposed by an LLM
|
|
|
15058
16538
|
const models = await getAvailableModels(opts.url, apiKey);
|
|
15059
16539
|
jsonOut({ models });
|
|
15060
16540
|
});
|
|
16541
|
+
function modelRow(entry) {
|
|
16542
|
+
return {
|
|
16543
|
+
id: entry.id,
|
|
16544
|
+
label: entry.label,
|
|
16545
|
+
url: entry.url,
|
|
16546
|
+
model: entry.model,
|
|
16547
|
+
local: entry.local,
|
|
16548
|
+
apiFlavor: entry.apiFlavor,
|
|
16549
|
+
runner: entry.runner,
|
|
16550
|
+
order: entry.order,
|
|
16551
|
+
capabilities: entry.capabilities,
|
|
16552
|
+
detectedCapabilities: entry.detectedCapabilities,
|
|
16553
|
+
probedAt: entry.probedAt,
|
|
16554
|
+
apiKeyRef: entry.apiKeyRef,
|
|
16555
|
+
keyState: entry.apiKeyRef ? getProviderApiKey(entry.apiKeyRef) ? "set" : "missing" : "none"
|
|
16556
|
+
};
|
|
16557
|
+
}
|
|
16558
|
+
function parseCapabilityFlags(json) {
|
|
16559
|
+
const flags = emptyCapabilityFlags();
|
|
16560
|
+
if (!json) return flags;
|
|
16561
|
+
let parsed;
|
|
16562
|
+
try {
|
|
16563
|
+
parsed = JSON.parse(json);
|
|
16564
|
+
} catch {
|
|
16565
|
+
jsonError("Invalid --capabilities JSON");
|
|
16566
|
+
}
|
|
16567
|
+
if (parsed && typeof parsed === "object") {
|
|
16568
|
+
const record = parsed;
|
|
16569
|
+
for (const key of Object.keys(flags)) {
|
|
16570
|
+
if (record[key] === true) flags[key] = true;
|
|
16571
|
+
}
|
|
16572
|
+
}
|
|
16573
|
+
return flags;
|
|
16574
|
+
}
|
|
16575
|
+
function urlLooksLocal(url) {
|
|
16576
|
+
return /localhost|127\.0\.0\.1|\[::1\]|::1/.test(url);
|
|
16577
|
+
}
|
|
16578
|
+
bridgeCommand.command("model-list").description("List the machine-local capability model registry (JSON)").action(() => {
|
|
16579
|
+
ensureMachineAiModelsMigrated();
|
|
16580
|
+
const models = [...getMachineAiModels()].sort((a, b) => a.order - b.order);
|
|
16581
|
+
jsonOut({ models: models.map(modelRow) });
|
|
16582
|
+
});
|
|
16583
|
+
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(
|
|
16584
|
+
"--flavor <flavor>",
|
|
16585
|
+
`Wire protocol: ${VALID_API_FLAVORS.join(" | ")}`
|
|
16586
|
+
).option("--key-ref <ref>", "Credential reference for API key").option("--embedding-dim-probe", "Allow one cheap /v1/embeddings dim probe").action(async (opts) => {
|
|
16587
|
+
if (opts.flavor && !VALID_API_FLAVORS.includes(opts.flavor)) {
|
|
16588
|
+
jsonError(`Invalid --flavor: ${opts.flavor}.`);
|
|
16589
|
+
}
|
|
16590
|
+
const apiFlavor = opts.flavor ?? inferApiFlavor(opts.url);
|
|
16591
|
+
const probe = await probeModelCapabilities(
|
|
16592
|
+
{
|
|
16593
|
+
url: opts.url,
|
|
16594
|
+
model: opts.model,
|
|
16595
|
+
apiFlavor,
|
|
16596
|
+
apiKeyRef: opts.keyRef
|
|
16597
|
+
},
|
|
16598
|
+
{ embeddingDimProbe: opts.embeddingDimProbe === true }
|
|
16599
|
+
);
|
|
16600
|
+
jsonOut({
|
|
16601
|
+
reachable: probe.reachable,
|
|
16602
|
+
catalog: probe.catalog,
|
|
16603
|
+
detected: probe.detected
|
|
16604
|
+
});
|
|
16605
|
+
});
|
|
16606
|
+
bridgeCommand.command("model-upsert").description(
|
|
16607
|
+
"Add or update a registry entry; probes before persisting (JSON)"
|
|
16608
|
+
).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(
|
|
16609
|
+
"--flavor <flavor>",
|
|
16610
|
+
`Wire protocol: ${VALID_API_FLAVORS.join(" | ")}`
|
|
16611
|
+
).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) => {
|
|
16612
|
+
if (opts.flavor && !VALID_API_FLAVORS.includes(opts.flavor)) {
|
|
16613
|
+
jsonError(`Invalid --flavor: ${opts.flavor}.`);
|
|
16614
|
+
}
|
|
16615
|
+
const models = getMachineAiModels();
|
|
16616
|
+
const existingIndex = opts.id ? models.findIndex((m) => m.id === opts.id) : -1;
|
|
16617
|
+
if (opts.id && existingIndex < 0) jsonError(`No such model: ${opts.id}`);
|
|
16618
|
+
const prev = existingIndex >= 0 ? models[existingIndex] : void 0;
|
|
16619
|
+
const url = opts.url ?? prev?.url ?? "";
|
|
16620
|
+
if (!url) jsonError("--url is required");
|
|
16621
|
+
const model = opts.model ?? prev?.model ?? "";
|
|
16622
|
+
if (!model) jsonError("--model is required");
|
|
16623
|
+
const apiFlavor = opts.flavor ?? prev?.apiFlavor ?? inferApiFlavor(url);
|
|
16624
|
+
const local = command.getOptionValueSource("local") === "cli" ? opts.local === true : prev?.local ?? urlLooksLocal(url);
|
|
16625
|
+
const order = opts.order !== void 0 ? Number.parseInt(opts.order, 10) : prev?.order ?? models.length;
|
|
16626
|
+
const candidate = {
|
|
16627
|
+
id: opts.id ?? ulid11(),
|
|
16628
|
+
label: opts.label ?? prev?.label ?? model,
|
|
16629
|
+
url,
|
|
16630
|
+
model,
|
|
16631
|
+
local,
|
|
16632
|
+
apiFlavor,
|
|
16633
|
+
order,
|
|
16634
|
+
capabilities: opts.capabilities ? parseCapabilityFlags(opts.capabilities) : prev?.capabilities ?? emptyCapabilityFlags(),
|
|
16635
|
+
detectedCapabilities: prev?.detectedCapabilities ?? emptyCapabilityFlags()
|
|
16636
|
+
};
|
|
16637
|
+
const runner = opts.runner ?? prev?.runner;
|
|
16638
|
+
if (runner) candidate.runner = runner;
|
|
16639
|
+
const apiKeyRef = opts.keyRef ?? prev?.apiKeyRef;
|
|
16640
|
+
if (apiKeyRef) candidate.apiKeyRef = apiKeyRef;
|
|
16641
|
+
const probe = await probeModelCapabilities(candidate, {
|
|
16642
|
+
embeddingDimProbe: true
|
|
16643
|
+
});
|
|
16644
|
+
const validation = validateModelSave(candidate, probe);
|
|
16645
|
+
if (!validation.ok || !validation.entry) {
|
|
16646
|
+
jsonError(validation.error ?? "Model could not be saved.");
|
|
16647
|
+
}
|
|
16648
|
+
const next = [...models];
|
|
16649
|
+
if (existingIndex >= 0) next[existingIndex] = validation.entry;
|
|
16650
|
+
else next.push(validation.entry);
|
|
16651
|
+
saveMachineAiModels(next);
|
|
16652
|
+
jsonOut({
|
|
16653
|
+
ok: true,
|
|
16654
|
+
model: modelRow(validation.entry),
|
|
16655
|
+
probe: { reachable: probe.reachable, detected: probe.detected }
|
|
16656
|
+
});
|
|
16657
|
+
});
|
|
16658
|
+
bridgeCommand.command("model-reprobe").description("Re-run capability detection for an entry; may widen (JSON)").requiredOption("--id <id>", "Registry entry id").action(async (opts) => {
|
|
16659
|
+
const models = getMachineAiModels();
|
|
16660
|
+
const index = models.findIndex((m) => m.id === opts.id);
|
|
16661
|
+
if (index < 0) jsonError(`No such model: ${opts.id}`);
|
|
16662
|
+
const entry = models[index];
|
|
16663
|
+
const probe = await probeModelCapabilities(entry, {
|
|
16664
|
+
embeddingDimProbe: true
|
|
16665
|
+
});
|
|
16666
|
+
const validation = validateModelSave(entry, probe);
|
|
16667
|
+
if (!validation.ok || !validation.entry) {
|
|
16668
|
+
jsonError(validation.error ?? "Re-probe failed.");
|
|
16669
|
+
}
|
|
16670
|
+
const next = [...models];
|
|
16671
|
+
next[index] = validation.entry;
|
|
16672
|
+
saveMachineAiModels(next);
|
|
16673
|
+
jsonOut({
|
|
16674
|
+
ok: true,
|
|
16675
|
+
model: modelRow(validation.entry),
|
|
16676
|
+
probe: { reachable: probe.reachable, detected: probe.detected }
|
|
16677
|
+
});
|
|
16678
|
+
});
|
|
16679
|
+
bridgeCommand.command("model-remove").description("Remove a registry entry (JSON)").requiredOption("--id <id>", "Registry entry id").action((opts) => {
|
|
16680
|
+
const models = getMachineAiModels();
|
|
16681
|
+
const next = models.filter((m) => m.id !== opts.id);
|
|
16682
|
+
if (next.length === models.length) jsonError(`No such model: ${opts.id}`);
|
|
16683
|
+
next.sort((a, b) => a.order - b.order).forEach((m, i) => {
|
|
16684
|
+
m.order = i;
|
|
16685
|
+
});
|
|
16686
|
+
saveMachineAiModels(next);
|
|
16687
|
+
jsonOut({ ok: true, id: opts.id, models: next.map(modelRow) });
|
|
16688
|
+
});
|
|
16689
|
+
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) => {
|
|
16690
|
+
let ids;
|
|
16691
|
+
try {
|
|
16692
|
+
ids = JSON.parse(opts.ids);
|
|
16693
|
+
} catch {
|
|
16694
|
+
jsonError("Invalid --ids JSON");
|
|
16695
|
+
return;
|
|
16696
|
+
}
|
|
16697
|
+
if (!Array.isArray(ids)) jsonError("--ids must be a JSON array");
|
|
16698
|
+
const models = getMachineAiModels();
|
|
16699
|
+
const rank = new Map(ids.map((id, i) => [id, i]));
|
|
16700
|
+
const next = [...models].sort((a, b) => {
|
|
16701
|
+
const ra = rank.get(a.id) ?? ids.length + a.order;
|
|
16702
|
+
const rb = rank.get(b.id) ?? ids.length + b.order;
|
|
16703
|
+
return ra - rb;
|
|
16704
|
+
});
|
|
16705
|
+
next.forEach((m, i) => {
|
|
16706
|
+
m.order = i;
|
|
16707
|
+
});
|
|
16708
|
+
saveMachineAiModels(next);
|
|
16709
|
+
jsonOut({ ok: true, models: next.map(modelRow) });
|
|
16710
|
+
});
|
|
16711
|
+
bridgeCommand.command("model-set-capabilities").description(
|
|
16712
|
+
"Set user-enabled capabilities within the detected ceiling (JSON)"
|
|
16713
|
+
).requiredOption("--id <id>", "Registry entry id").requiredOption(
|
|
16714
|
+
"--capabilities <json>",
|
|
16715
|
+
"JSON object of desired capabilities"
|
|
16716
|
+
).action((opts) => {
|
|
16717
|
+
const models = getMachineAiModels();
|
|
16718
|
+
const index = models.findIndex((m) => m.id === opts.id);
|
|
16719
|
+
if (index < 0) jsonError(`No such model: ${opts.id}`);
|
|
16720
|
+
const requested = parseCapabilityFlags(opts.capabilities);
|
|
16721
|
+
const detected = models[index].detectedCapabilities;
|
|
16722
|
+
const capabilities = emptyCapabilityFlags();
|
|
16723
|
+
for (const key of Object.keys(capabilities)) {
|
|
16724
|
+
capabilities[key] = requested[key] && detected[key];
|
|
16725
|
+
}
|
|
16726
|
+
const next = [...models];
|
|
16727
|
+
next[index] = { ...models[index], capabilities };
|
|
16728
|
+
saveMachineAiModels(next);
|
|
16729
|
+
jsonOut({ ok: true, model: modelRow(next[index]) });
|
|
16730
|
+
});
|
|
15061
16731
|
bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for an endpoint URL (JSON)").requiredOption("--url <url>", "Endpoint base URL").action((opts) => {
|
|
15062
16732
|
jsonOut({ recommendation: getCloudModelRecommendation(opts.url) });
|
|
15063
16733
|
});
|
|
15064
16734
|
bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
|
|
15065
16735
|
const profile = getSystemProfile();
|
|
15066
|
-
const flmInstalled = hasCommand("flm") ||
|
|
16736
|
+
const flmInstalled = hasCommand("flm") || existsSync18("C:\\Program Files\\flm\\flm.exe");
|
|
15067
16737
|
const ollamaInstalled = isOllamaInstalled();
|
|
15068
16738
|
const runners = [
|
|
15069
16739
|
{ id: "flm", label: "FastFlowLM", installed: flmInstalled },
|
|
@@ -15203,6 +16873,93 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
15203
16873
|
}
|
|
15204
16874
|
});
|
|
15205
16875
|
});
|
|
16876
|
+
function parseDiscussionThread(raw) {
|
|
16877
|
+
if (!raw) return [];
|
|
16878
|
+
let parsed;
|
|
16879
|
+
try {
|
|
16880
|
+
parsed = JSON.parse(raw);
|
|
16881
|
+
} catch {
|
|
16882
|
+
throw new Error("Invalid --thread: not valid JSON");
|
|
16883
|
+
}
|
|
16884
|
+
if (!Array.isArray(parsed)) {
|
|
16885
|
+
throw new Error("Invalid --thread: expected a JSON array of turns");
|
|
16886
|
+
}
|
|
16887
|
+
return parsed.map((turn, index) => {
|
|
16888
|
+
const candidate = turn;
|
|
16889
|
+
if (candidate?.role !== "user" && candidate?.role !== "assistant" || typeof candidate?.content !== "string") {
|
|
16890
|
+
throw new Error(
|
|
16891
|
+
`Invalid --thread: turn ${index} must be {"role": "user"|"assistant", "content": string}`
|
|
16892
|
+
);
|
|
16893
|
+
}
|
|
16894
|
+
return { role: candidate.role, content: candidate.content };
|
|
16895
|
+
});
|
|
16896
|
+
}
|
|
16897
|
+
bridgeCommand.command("discuss-review").description(
|
|
16898
|
+
"Answer one turn of the post-reveal follow-up discussion about a card (JSON)"
|
|
16899
|
+
).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(
|
|
16900
|
+
"--thread <json>",
|
|
16901
|
+
'Prior turns, oldest first, as JSON: [{"role":"user"|"assistant","content":"\u2026"},\u2026]'
|
|
16902
|
+
).option("--context <context>", "Optional token context details").option("--source-link <link>", "Optional source link").option(
|
|
16903
|
+
"--source-content <content>",
|
|
16904
|
+
"Pre-resolved source reference content (skips re-fetch when set)"
|
|
16905
|
+
).action(async (opts) => {
|
|
16906
|
+
await withDb2(async (db) => {
|
|
16907
|
+
const isEnabled = await getSetting(db, "llm.enabled") === "true";
|
|
16908
|
+
if (!isEnabled) {
|
|
16909
|
+
jsonOut({
|
|
16910
|
+
success: false,
|
|
16911
|
+
error: "LLM integration is disabled",
|
|
16912
|
+
reply: ""
|
|
16913
|
+
});
|
|
16914
|
+
return;
|
|
16915
|
+
}
|
|
16916
|
+
let thread;
|
|
16917
|
+
try {
|
|
16918
|
+
thread = parseDiscussionThread(opts.thread);
|
|
16919
|
+
} catch (err) {
|
|
16920
|
+
jsonOut({
|
|
16921
|
+
success: false,
|
|
16922
|
+
error: err.message,
|
|
16923
|
+
reply: ""
|
|
16924
|
+
});
|
|
16925
|
+
return;
|
|
16926
|
+
}
|
|
16927
|
+
let resolvedContextContent = opts.sourceContent ?? null;
|
|
16928
|
+
if (resolvedContextContent == null && opts.sourceLink) {
|
|
16929
|
+
try {
|
|
16930
|
+
const resolved = await resolveReviewContext(opts.sourceLink);
|
|
16931
|
+
resolvedContextContent = resolved?.content ?? null;
|
|
16932
|
+
} catch {
|
|
16933
|
+
}
|
|
16934
|
+
}
|
|
16935
|
+
try {
|
|
16936
|
+
const result = await discussReviewViaLLM(db, {
|
|
16937
|
+
slug: opts.slug,
|
|
16938
|
+
concept: opts.concept,
|
|
16939
|
+
domain: opts.domain,
|
|
16940
|
+
bloomLevel: Number(opts.bloomLevel),
|
|
16941
|
+
context: opts.context,
|
|
16942
|
+
question: opts.question,
|
|
16943
|
+
userAnswer: opts.userAnswer,
|
|
16944
|
+
sourceLinkContent: resolvedContextContent,
|
|
16945
|
+
feedback: opts.feedback ?? null,
|
|
16946
|
+
thread,
|
|
16947
|
+
message: opts.message
|
|
16948
|
+
});
|
|
16949
|
+
jsonOut({
|
|
16950
|
+
success: true,
|
|
16951
|
+
reply: result.text,
|
|
16952
|
+
replyModel: result.model
|
|
16953
|
+
});
|
|
16954
|
+
} catch (err) {
|
|
16955
|
+
jsonOut({
|
|
16956
|
+
success: false,
|
|
16957
|
+
error: err.message,
|
|
16958
|
+
reply: ""
|
|
16959
|
+
});
|
|
16960
|
+
}
|
|
16961
|
+
});
|
|
16962
|
+
});
|
|
15206
16963
|
bridgeCommand.command("desktop-bootstrap").description("Initialize first-run desktop state (JSON)").option("--user <id>", "Preferred user ID when none is configured").action(async (opts) => {
|
|
15207
16964
|
await withDb2(async (db) => {
|
|
15208
16965
|
const userId = await ensureDefaultUser(db, opts.user);
|
|
@@ -15231,6 +16988,54 @@ bridgeCommand.command("get-settings").description("Get active ZAM settings (JSON
|
|
|
15231
16988
|
});
|
|
15232
16989
|
});
|
|
15233
16990
|
});
|
|
16991
|
+
bridgeCommand.command("agent-harness-status").description(
|
|
16992
|
+
"Detect installed agent harnesses and their ZAM MCP configuration state (JSON)"
|
|
16993
|
+
).action(() => {
|
|
16994
|
+
const report = inspectConnectHarnesses();
|
|
16995
|
+
jsonOut({
|
|
16996
|
+
success: true,
|
|
16997
|
+
zamOnPath: report.zamOnPath,
|
|
16998
|
+
harnesses: report.harnesses
|
|
16999
|
+
});
|
|
17000
|
+
});
|
|
17001
|
+
bridgeCommand.command("agent-connect").description(
|
|
17002
|
+
"Run the idempotent agent-connect flow for one or all detected harnesses (JSON)"
|
|
17003
|
+
).option("--harness <id>", "Explicit harness id (default: all detected)").option(
|
|
17004
|
+
"--auto-once",
|
|
17005
|
+
"First-run mode: skip when the auto-connect marker is already set; set the marker after a run that detected at least one harness"
|
|
17006
|
+
).action(async (opts) => {
|
|
17007
|
+
if (opts.harness && !isConnectHarnessId(opts.harness)) {
|
|
17008
|
+
jsonOut({
|
|
17009
|
+
success: false,
|
|
17010
|
+
error: `Unsupported harness: ${opts.harness}`
|
|
17011
|
+
});
|
|
17012
|
+
return;
|
|
17013
|
+
}
|
|
17014
|
+
const harness = opts.harness;
|
|
17015
|
+
const run = () => {
|
|
17016
|
+
const report = performAgentConnect({ harness });
|
|
17017
|
+
return {
|
|
17018
|
+
success: report.success,
|
|
17019
|
+
detected: report.detected,
|
|
17020
|
+
zamOnPath: report.zamOnPath,
|
|
17021
|
+
results: report.results.map(({ content: _content, ...rest }) => rest),
|
|
17022
|
+
skills: report.skills
|
|
17023
|
+
};
|
|
17024
|
+
};
|
|
17025
|
+
if (opts.autoOnce) {
|
|
17026
|
+
if (getAgentConnectAutoDone()) {
|
|
17027
|
+
jsonOut({ success: true, skipped: true });
|
|
17028
|
+
return;
|
|
17029
|
+
}
|
|
17030
|
+
const payload = run();
|
|
17031
|
+
if (payload.detected.length > 0) {
|
|
17032
|
+
setAgentConnectAutoDone(true);
|
|
17033
|
+
}
|
|
17034
|
+
jsonOut(payload);
|
|
17035
|
+
return;
|
|
17036
|
+
}
|
|
17037
|
+
jsonOut(run());
|
|
17038
|
+
});
|
|
15234
17039
|
async function readDatabaseUserSummaries(db) {
|
|
15235
17040
|
return await db.prepare(
|
|
15236
17041
|
`SELECT user_id AS id, COUNT(*) AS cardCount
|
|
@@ -15662,7 +17467,10 @@ bridgeCommand.command("personal-card-delete").description("Hard-delete a token a
|
|
|
15662
17467
|
});
|
|
15663
17468
|
});
|
|
15664
17469
|
});
|
|
15665
|
-
bridgeCommand.command("personal-card-import-curriculum").description("Parse curriculum text using LLM and import cards (JSON)").option("--user <id>", "User ID (default: whoami)").
|
|
17470
|
+
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(
|
|
17471
|
+
"--sourceId <id>",
|
|
17472
|
+
"Read curriculum text from a sources row (avoids large IPC payloads)"
|
|
17473
|
+
).requiredOption(
|
|
15666
17474
|
"--domain <domain>",
|
|
15667
17475
|
"Default category/domain for imported cards"
|
|
15668
17476
|
).option("--source <url>", "Provenance source link or URL").option("--preview", "Return parsed cards without saving them").option(
|
|
@@ -15676,6 +17484,17 @@ bridgeCommand.command("personal-card-import-curriculum").description("Parse curr
|
|
|
15676
17484
|
).action(async (opts) => {
|
|
15677
17485
|
await withDb2(async (db) => {
|
|
15678
17486
|
const userId = await resolveUser(opts, db, { json: true });
|
|
17487
|
+
let curriculumText = opts.text;
|
|
17488
|
+
if (opts.sourceId) {
|
|
17489
|
+
const row = await db.prepare("SELECT content FROM sources WHERE id = ?").get(opts.sourceId);
|
|
17490
|
+
if (!row) {
|
|
17491
|
+
jsonError(`Source not found: ${opts.sourceId}`);
|
|
17492
|
+
}
|
|
17493
|
+
curriculumText = row.content ?? "";
|
|
17494
|
+
}
|
|
17495
|
+
if (!curriculumText?.trim()) {
|
|
17496
|
+
jsonError("Curriculum text is required (--text or --sourceId)");
|
|
17497
|
+
}
|
|
15679
17498
|
const contextNames = parseKnowledgeContextNames2(
|
|
15680
17499
|
opts.knowledgeContext || []
|
|
15681
17500
|
);
|
|
@@ -15683,7 +17502,7 @@ bridgeCommand.command("personal-card-import-curriculum").description("Parse curr
|
|
|
15683
17502
|
const firstContext = contexts[0]?.name;
|
|
15684
17503
|
const cards = await importCurriculumViaLLM(
|
|
15685
17504
|
db,
|
|
15686
|
-
|
|
17505
|
+
curriculumText,
|
|
15687
17506
|
opts.domain,
|
|
15688
17507
|
opts.source || null,
|
|
15689
17508
|
{ knowledgeContext: firstContext }
|
|
@@ -15852,7 +17671,7 @@ bridgeCommand.command("personal-source-import").description(
|
|
|
15852
17671
|
} else {
|
|
15853
17672
|
throw new Error(`Invalid source type: ${opts.type}`);
|
|
15854
17673
|
}
|
|
15855
|
-
const sourceId =
|
|
17674
|
+
const sourceId = ulid11();
|
|
15856
17675
|
await db.prepare(
|
|
15857
17676
|
`INSERT INTO sources (id, type, uri, content)
|
|
15858
17677
|
VALUES (?, ?, ?, ?)
|
|
@@ -16037,10 +17856,86 @@ async function fetchRawHtml(url) {
|
|
|
16037
17856
|
clearTimeout(timeoutId);
|
|
16038
17857
|
}
|
|
16039
17858
|
}
|
|
16040
|
-
|
|
16041
|
-
|
|
16042
|
-
|
|
17859
|
+
var CURRICULUM_TOPIC_URI_PREFIX = "zam-curriculum-topic://";
|
|
17860
|
+
async function extractAndStoreCurriculumTopics(db, provider, topics) {
|
|
17861
|
+
const topicsByUri = /* @__PURE__ */ new Map();
|
|
17862
|
+
for (const topic of topics) {
|
|
17863
|
+
const resolved = provider.resolveTopic(topic);
|
|
17864
|
+
const list = topicsByUri.get(resolved.uri) || [];
|
|
17865
|
+
list.push(topic);
|
|
17866
|
+
topicsByUri.set(resolved.uri, list);
|
|
17867
|
+
}
|
|
17868
|
+
const extracted = [];
|
|
17869
|
+
for (const [uri, uriTopics] of topicsByUri.entries()) {
|
|
17870
|
+
const rawHtml = await fetchRawHtml(uri);
|
|
17871
|
+
let extractedTexts = {};
|
|
17872
|
+
if (provider.extractTopics) {
|
|
17873
|
+
const fullTopicIds = uriTopics.map((t2) => `${t2.sourceRef}#${t2.id}`);
|
|
17874
|
+
extractedTexts = provider.extractTopics(rawHtml, fullTopicIds);
|
|
17875
|
+
} else {
|
|
17876
|
+
const cleanText = cleanHtml(rawHtml);
|
|
17877
|
+
for (const t2 of uriTopics) {
|
|
17878
|
+
extractedTexts[`${t2.sourceRef}#${t2.id}`] = cleanText;
|
|
17879
|
+
}
|
|
17880
|
+
}
|
|
17881
|
+
const pageCleanedText = cleanHtml(rawHtml);
|
|
17882
|
+
const sourceId = ulid11();
|
|
17883
|
+
await db.prepare(
|
|
17884
|
+
`INSERT INTO sources (id, type, uri, content)
|
|
17885
|
+
VALUES (?, 'web', ?, ?)
|
|
17886
|
+
ON CONFLICT(uri) DO UPDATE SET
|
|
17887
|
+
type = excluded.type,
|
|
17888
|
+
content = excluded.content`
|
|
17889
|
+
).run(sourceId, uri, pageCleanedText);
|
|
17890
|
+
const record = await db.prepare("SELECT id FROM sources WHERE uri = ?").get(uri);
|
|
17891
|
+
for (const topic of uriTopics) {
|
|
17892
|
+
const resolved = provider.resolveTopic(topic);
|
|
17893
|
+
const text = extractedTexts[resolved.topicId] || "";
|
|
17894
|
+
const topicUri = `${CURRICULUM_TOPIC_URI_PREFIX}${resolved.topicId}`;
|
|
17895
|
+
const topicSourceId = ulid11();
|
|
17896
|
+
await db.prepare(
|
|
17897
|
+
`INSERT INTO sources (id, type, uri, content)
|
|
17898
|
+
VALUES (?, 'web', ?, ?)
|
|
17899
|
+
ON CONFLICT(uri) DO UPDATE SET
|
|
17900
|
+
content = excluded.content`
|
|
17901
|
+
).run(topicSourceId, topicUri, text);
|
|
17902
|
+
const topicRecord = await db.prepare("SELECT id FROM sources WHERE uri = ?").get(topicUri);
|
|
17903
|
+
extracted.push({
|
|
17904
|
+
topicId: resolved.topicId,
|
|
17905
|
+
uri,
|
|
17906
|
+
sourceId: record.id,
|
|
17907
|
+
topicSourceId: topicRecord.id,
|
|
17908
|
+
textLength: text.length
|
|
17909
|
+
});
|
|
17910
|
+
}
|
|
17911
|
+
}
|
|
17912
|
+
return extracted;
|
|
17913
|
+
}
|
|
17914
|
+
async function assignConfirmedProposalContexts(db, proposals, contexts) {
|
|
17915
|
+
for (const p of proposals) {
|
|
17916
|
+
const baseText = p.question && p.question.trim().length > 0 ? p.question : p.concept;
|
|
17917
|
+
const cleanDomain = slugify(p.domain || "");
|
|
17918
|
+
const cleanBase = slugify(baseText);
|
|
17919
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
17920
|
+
if (baseSlug.length > 60) {
|
|
17921
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
17922
|
+
}
|
|
17923
|
+
if (!baseSlug) {
|
|
17924
|
+
baseSlug = "token";
|
|
17925
|
+
}
|
|
17926
|
+
const token = await getTokenBySlug(db, baseSlug);
|
|
17927
|
+
if (token) {
|
|
17928
|
+
for (const context of contexts) {
|
|
17929
|
+
await assignTokenToContext(db, token.id, context.id);
|
|
17930
|
+
}
|
|
17931
|
+
}
|
|
17932
|
+
}
|
|
17933
|
+
}
|
|
17934
|
+
bridgeCommand.command("curriculum-import-status").description(
|
|
17935
|
+
"Check which selected curriculum topics are already imported for a user (JSON)"
|
|
17936
|
+
).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) => {
|
|
16043
17937
|
await withDb2(async (db) => {
|
|
17938
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
16044
17939
|
const provider = getCurriculumProvider(opts.provider);
|
|
16045
17940
|
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
16046
17941
|
let topics;
|
|
@@ -16050,47 +17945,446 @@ bridgeCommand.command("curriculum-extract-topics").description(
|
|
|
16050
17945
|
jsonError("Invalid --topics JSON");
|
|
16051
17946
|
return;
|
|
16052
17947
|
}
|
|
16053
|
-
const
|
|
17948
|
+
const status = [];
|
|
16054
17949
|
for (const topic of topics) {
|
|
16055
|
-
|
|
16056
|
-
const list = topicsByUri.get(resolved.uri) || [];
|
|
16057
|
-
list.push(topic);
|
|
16058
|
-
topicsByUri.set(resolved.uri, list);
|
|
16059
|
-
}
|
|
16060
|
-
const extracted = [];
|
|
16061
|
-
for (const [uri, uriTopics] of topicsByUri.entries()) {
|
|
16062
|
-
const rawHtml = await fetchRawHtml(uri);
|
|
16063
|
-
let extractedTexts = {};
|
|
16064
|
-
if (provider.extractTopics) {
|
|
16065
|
-
const fullTopicIds = uriTopics.map((t2) => `${t2.sourceRef}#${t2.id}`);
|
|
16066
|
-
extractedTexts = provider.extractTopics(rawHtml, fullTopicIds);
|
|
16067
|
-
} else {
|
|
16068
|
-
const cleanText = cleanHtml(rawHtml);
|
|
16069
|
-
for (const t2 of uriTopics) {
|
|
16070
|
-
extractedTexts[`${t2.sourceRef}#${t2.id}`] = cleanText;
|
|
16071
|
-
}
|
|
16072
|
-
}
|
|
16073
|
-
const pageCleanedText = cleanHtml(rawHtml);
|
|
16074
|
-
const sourceId = ulid10();
|
|
16075
|
-
await db.prepare(
|
|
16076
|
-
`INSERT INTO sources (id, type, uri, content)
|
|
16077
|
-
VALUES (?, 'web', ?, ?)
|
|
16078
|
-
ON CONFLICT(uri) DO UPDATE SET
|
|
16079
|
-
type = excluded.type,
|
|
16080
|
-
content = excluded.content`
|
|
16081
|
-
).run(sourceId, uri, pageCleanedText);
|
|
16082
|
-
const record = await db.prepare("SELECT id FROM sources WHERE uri = ?").get(uri);
|
|
16083
|
-
for (const topic of uriTopics) {
|
|
17950
|
+
try {
|
|
16084
17951
|
const resolved = provider.resolveTopic(topic);
|
|
16085
|
-
const
|
|
16086
|
-
|
|
17952
|
+
const cardCount = await countUserCardsForCurriculumTopic(
|
|
17953
|
+
db,
|
|
17954
|
+
userId,
|
|
17955
|
+
provider.id,
|
|
17956
|
+
resolved.topicId
|
|
17957
|
+
);
|
|
17958
|
+
status.push({
|
|
16087
17959
|
topicId: resolved.topicId,
|
|
16088
|
-
|
|
16089
|
-
|
|
16090
|
-
|
|
17960
|
+
shortId: topic.id,
|
|
17961
|
+
cardCount,
|
|
17962
|
+
alreadyImported: cardCount > 0
|
|
17963
|
+
});
|
|
17964
|
+
} catch (err) {
|
|
17965
|
+
status.push({
|
|
17966
|
+
topicId: topic.id,
|
|
17967
|
+
shortId: topic.id,
|
|
17968
|
+
cardCount: 0,
|
|
17969
|
+
alreadyImported: false,
|
|
17970
|
+
error: err.message || String(err)
|
|
16091
17971
|
});
|
|
16092
17972
|
}
|
|
16093
17973
|
}
|
|
17974
|
+
jsonOut({ success: true, status });
|
|
17975
|
+
});
|
|
17976
|
+
});
|
|
17977
|
+
bridgeCommand.command("curriculum-import-topic").description(
|
|
17978
|
+
"Extract, LLM-preview, and confirm-import one curriculum topic server-side (JSON)"
|
|
17979
|
+
).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(
|
|
17980
|
+
"--knowledge-context <context>",
|
|
17981
|
+
"Assign confirmed tokens to a knowledge context (repeatable)",
|
|
17982
|
+
(val, memo) => {
|
|
17983
|
+
memo.push(val);
|
|
17984
|
+
return memo;
|
|
17985
|
+
},
|
|
17986
|
+
[]
|
|
17987
|
+
).action(async (opts) => {
|
|
17988
|
+
await withDb2(async (db) => {
|
|
17989
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
17990
|
+
const provider = getCurriculumProvider(opts.provider);
|
|
17991
|
+
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
17992
|
+
let topic;
|
|
17993
|
+
try {
|
|
17994
|
+
topic = JSON.parse(opts.topic);
|
|
17995
|
+
} catch {
|
|
17996
|
+
jsonError("Invalid --topic JSON");
|
|
17997
|
+
return;
|
|
17998
|
+
}
|
|
17999
|
+
const extracted = await extractAndStoreCurriculumTopics(db, provider, [
|
|
18000
|
+
topic
|
|
18001
|
+
]);
|
|
18002
|
+
const item = extracted[0];
|
|
18003
|
+
if (!item || item.textLength === 0) {
|
|
18004
|
+
jsonError(`No curriculum text extracted for topic "${topic.id}"`);
|
|
18005
|
+
}
|
|
18006
|
+
const contextNames = parseKnowledgeContextNames2(
|
|
18007
|
+
opts.knowledgeContext || []
|
|
18008
|
+
);
|
|
18009
|
+
const contexts = await resolveKnowledgeContexts2(db, contextNames);
|
|
18010
|
+
const firstContext = contexts[0]?.name;
|
|
18011
|
+
const topicRow = await db.prepare("SELECT content FROM sources WHERE id = ?").get(item.topicSourceId);
|
|
18012
|
+
const curriculumText = topicRow.content ?? "";
|
|
18013
|
+
const cards = await importCurriculumViaLLM(
|
|
18014
|
+
db,
|
|
18015
|
+
curriculumText,
|
|
18016
|
+
opts.domain,
|
|
18017
|
+
item.uri,
|
|
18018
|
+
{ knowledgeContext: firstContext }
|
|
18019
|
+
);
|
|
18020
|
+
if (cards.length === 0) {
|
|
18021
|
+
jsonError(`No cards were generated for ${item.topicId}`);
|
|
18022
|
+
}
|
|
18023
|
+
const proposals = cards.map((card) => ({
|
|
18024
|
+
question: card.question,
|
|
18025
|
+
concept: card.concept,
|
|
18026
|
+
domain: card.domain,
|
|
18027
|
+
title: card.title,
|
|
18028
|
+
bloom_level: card.bloom_level,
|
|
18029
|
+
symbiosis_mode: card.symbiosis_mode || "none",
|
|
18030
|
+
excerpt: card.context || "",
|
|
18031
|
+
page_number: null,
|
|
18032
|
+
provider: provider.id,
|
|
18033
|
+
topic_id: item.topicId
|
|
18034
|
+
}));
|
|
18035
|
+
const result = await confirmSourceImport(
|
|
18036
|
+
db,
|
|
18037
|
+
userId,
|
|
18038
|
+
item.sourceId,
|
|
18039
|
+
proposals
|
|
18040
|
+
);
|
|
18041
|
+
await assignConfirmedProposalContexts(db, proposals, contexts);
|
|
18042
|
+
jsonOut({
|
|
18043
|
+
success: true,
|
|
18044
|
+
topicId: item.topicId,
|
|
18045
|
+
proposalCount: proposals.length,
|
|
18046
|
+
createdCount: result.createdCount,
|
|
18047
|
+
ensuredCount: result.linkedCount
|
|
18048
|
+
});
|
|
18049
|
+
});
|
|
18050
|
+
});
|
|
18051
|
+
function proposalBaseSlug(domain, question, concept) {
|
|
18052
|
+
const baseText = question?.trim() ? question : concept;
|
|
18053
|
+
const cleanDomain = slugify(domain || "");
|
|
18054
|
+
const cleanBase = slugify(baseText);
|
|
18055
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
18056
|
+
if (baseSlug.length > 60) {
|
|
18057
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
18058
|
+
}
|
|
18059
|
+
return baseSlug || "token";
|
|
18060
|
+
}
|
|
18061
|
+
async function executeCurriculumConfirmOperations(db, userId, operations, contexts) {
|
|
18062
|
+
let createdCount = 0;
|
|
18063
|
+
let ensuredCount = 0;
|
|
18064
|
+
let removedCount = 0;
|
|
18065
|
+
await db.transaction(async (tx) => {
|
|
18066
|
+
for (const op of operations) {
|
|
18067
|
+
if (op.create.length > 0) {
|
|
18068
|
+
const result = await applySourceProposals(
|
|
18069
|
+
tx,
|
|
18070
|
+
userId,
|
|
18071
|
+
op.sourceId,
|
|
18072
|
+
op.create
|
|
18073
|
+
);
|
|
18074
|
+
createdCount += result.createdCount;
|
|
18075
|
+
ensuredCount += result.linkedCount;
|
|
18076
|
+
await assignConfirmedProposalContexts(tx, op.create, contexts);
|
|
18077
|
+
}
|
|
18078
|
+
for (const slug of op.removeSlugs) {
|
|
18079
|
+
if (await deleteCurriculumCardForUser(
|
|
18080
|
+
tx,
|
|
18081
|
+
userId,
|
|
18082
|
+
slug,
|
|
18083
|
+
op.provider,
|
|
18084
|
+
op.topicId
|
|
18085
|
+
)) {
|
|
18086
|
+
removedCount++;
|
|
18087
|
+
}
|
|
18088
|
+
}
|
|
18089
|
+
}
|
|
18090
|
+
});
|
|
18091
|
+
return { createdCount, ensuredCount, removedCount };
|
|
18092
|
+
}
|
|
18093
|
+
bridgeCommand.command("curriculum-list-subtopics").description(
|
|
18094
|
+
"List finer units inside a Lernbereich for chunked import (JSON)"
|
|
18095
|
+
).requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topic <json>", "Single topic node JSON").action(async (opts) => {
|
|
18096
|
+
try {
|
|
18097
|
+
const provider = getCurriculumProvider(opts.provider);
|
|
18098
|
+
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
18099
|
+
if (!provider.extractSubTopics) {
|
|
18100
|
+
jsonOut({ success: true, subTopics: [] });
|
|
18101
|
+
return;
|
|
18102
|
+
}
|
|
18103
|
+
let topic;
|
|
18104
|
+
try {
|
|
18105
|
+
topic = JSON.parse(opts.topic);
|
|
18106
|
+
} catch {
|
|
18107
|
+
jsonError("Invalid --topic JSON");
|
|
18108
|
+
return;
|
|
18109
|
+
}
|
|
18110
|
+
const resolved = provider.resolveTopic(topic);
|
|
18111
|
+
const rawHtml = await fetchRawHtml(resolved.uri);
|
|
18112
|
+
const subTopics = provider.extractSubTopics(rawHtml, resolved.topicId).map(({ id, label, textLength }) => ({ id, label, textLength }));
|
|
18113
|
+
jsonOut({ success: true, topicId: resolved.topicId, subTopics });
|
|
18114
|
+
} catch (err) {
|
|
18115
|
+
jsonError(err.message || String(err));
|
|
18116
|
+
}
|
|
18117
|
+
});
|
|
18118
|
+
bridgeCommand.command("curriculum-preview-topic").description(
|
|
18119
|
+
"Preview importable cards for one topic (existing + LLM proposals) (JSON)"
|
|
18120
|
+
).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(
|
|
18121
|
+
"--knowledge-context <context>",
|
|
18122
|
+
"Knowledge context for LLM prompt (repeatable)",
|
|
18123
|
+
(val, memo) => {
|
|
18124
|
+
memo.push(val);
|
|
18125
|
+
return memo;
|
|
18126
|
+
},
|
|
18127
|
+
[]
|
|
18128
|
+
).action(async (opts) => {
|
|
18129
|
+
await withDb2(async (db) => {
|
|
18130
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
18131
|
+
const provider = getCurriculumProvider(opts.provider);
|
|
18132
|
+
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
18133
|
+
let topic;
|
|
18134
|
+
try {
|
|
18135
|
+
topic = JSON.parse(opts.topic);
|
|
18136
|
+
} catch {
|
|
18137
|
+
jsonError("Invalid --topic JSON");
|
|
18138
|
+
return;
|
|
18139
|
+
}
|
|
18140
|
+
const resolved = provider.resolveTopic(topic);
|
|
18141
|
+
const extracted = await extractAndStoreCurriculumTopics(db, provider, [
|
|
18142
|
+
topic
|
|
18143
|
+
]);
|
|
18144
|
+
const item = extracted[0];
|
|
18145
|
+
if (!item || item.textLength === 0) {
|
|
18146
|
+
jsonError(`No curriculum text extracted for topic "${topic.id}"`);
|
|
18147
|
+
}
|
|
18148
|
+
const contextNames = parseKnowledgeContextNames2(
|
|
18149
|
+
opts.knowledgeContext || []
|
|
18150
|
+
);
|
|
18151
|
+
const contexts = await resolveKnowledgeContexts2(db, contextNames);
|
|
18152
|
+
const firstContext = contexts[0]?.name;
|
|
18153
|
+
let subTopicIds;
|
|
18154
|
+
if (opts.subTopics) {
|
|
18155
|
+
try {
|
|
18156
|
+
subTopicIds = JSON.parse(opts.subTopics);
|
|
18157
|
+
} catch {
|
|
18158
|
+
jsonError("Invalid --subTopics JSON");
|
|
18159
|
+
return;
|
|
18160
|
+
}
|
|
18161
|
+
}
|
|
18162
|
+
const rawHtml = await fetchRawHtml(item.uri);
|
|
18163
|
+
const chunks = [];
|
|
18164
|
+
if (provider.extractSubTopics) {
|
|
18165
|
+
const subTopics = provider.extractSubTopics(rawHtml, resolved.topicId);
|
|
18166
|
+
const selected = subTopicIds && subTopicIds.length > 0 ? subTopics.filter((st) => subTopicIds.includes(st.id)) : subTopics;
|
|
18167
|
+
if (selected.length > 0) {
|
|
18168
|
+
for (const st of selected) {
|
|
18169
|
+
chunks.push({ subTopicId: st.id, text: st.text });
|
|
18170
|
+
}
|
|
18171
|
+
}
|
|
18172
|
+
}
|
|
18173
|
+
if (chunks.length === 0) {
|
|
18174
|
+
const topicRow = await db.prepare("SELECT content FROM sources WHERE id = ?").get(item.topicSourceId);
|
|
18175
|
+
chunks.push({
|
|
18176
|
+
subTopicId: null,
|
|
18177
|
+
text: topicRow.content ?? ""
|
|
18178
|
+
});
|
|
18179
|
+
}
|
|
18180
|
+
const existingCards = await listUserCardsForCurriculumTopic(
|
|
18181
|
+
db,
|
|
18182
|
+
userId,
|
|
18183
|
+
provider.id,
|
|
18184
|
+
resolved.topicId
|
|
18185
|
+
);
|
|
18186
|
+
const existingSlugs = new Set(existingCards.map((c) => c.slug));
|
|
18187
|
+
const items = [];
|
|
18188
|
+
for (const card of existingCards) {
|
|
18189
|
+
items.push({
|
|
18190
|
+
id: `existing:${card.slug}`,
|
|
18191
|
+
slug: card.slug,
|
|
18192
|
+
question: card.question ?? "",
|
|
18193
|
+
concept: card.concept,
|
|
18194
|
+
domain: card.domain,
|
|
18195
|
+
bloom_level: card.bloomLevel,
|
|
18196
|
+
symbiosis_mode: card.symbiosisMode ?? "none",
|
|
18197
|
+
excerpt: "",
|
|
18198
|
+
isExisting: true,
|
|
18199
|
+
selected: true,
|
|
18200
|
+
subTopicId: card.topicId?.includes("@") ? card.topicId.split("@").pop() ?? null : null,
|
|
18201
|
+
parentTopicId: resolved.topicId
|
|
18202
|
+
});
|
|
18203
|
+
}
|
|
18204
|
+
let proposalIndex = 0;
|
|
18205
|
+
for (const chunk of chunks) {
|
|
18206
|
+
if (!chunk.text.trim()) continue;
|
|
18207
|
+
const cards = await importCurriculumViaLLM(
|
|
18208
|
+
db,
|
|
18209
|
+
chunk.text,
|
|
18210
|
+
opts.domain,
|
|
18211
|
+
item.uri,
|
|
18212
|
+
{ knowledgeContext: firstContext }
|
|
18213
|
+
);
|
|
18214
|
+
const scopedTopicId = chunk.subTopicId ? `${resolved.topicId}@${chunk.subTopicId}` : resolved.topicId;
|
|
18215
|
+
for (const card of cards) {
|
|
18216
|
+
const slug = proposalBaseSlug(
|
|
18217
|
+
card.domain,
|
|
18218
|
+
card.question,
|
|
18219
|
+
card.concept
|
|
18220
|
+
);
|
|
18221
|
+
if (existingSlugs.has(slug)) {
|
|
18222
|
+
continue;
|
|
18223
|
+
}
|
|
18224
|
+
const proposal = {
|
|
18225
|
+
question: card.question,
|
|
18226
|
+
concept: card.concept,
|
|
18227
|
+
domain: card.domain,
|
|
18228
|
+
bloom_level: card.bloom_level,
|
|
18229
|
+
symbiosis_mode: card.symbiosis_mode || "none",
|
|
18230
|
+
excerpt: card.context || "",
|
|
18231
|
+
page_number: null,
|
|
18232
|
+
provider: provider.id,
|
|
18233
|
+
topic_id: scopedTopicId
|
|
18234
|
+
};
|
|
18235
|
+
items.push({
|
|
18236
|
+
id: `new:${proposalIndex++}`,
|
|
18237
|
+
slug: null,
|
|
18238
|
+
question: proposal.question,
|
|
18239
|
+
concept: proposal.concept,
|
|
18240
|
+
domain: proposal.domain,
|
|
18241
|
+
bloom_level: proposal.bloom_level,
|
|
18242
|
+
symbiosis_mode: proposal.symbiosis_mode,
|
|
18243
|
+
excerpt: proposal.excerpt,
|
|
18244
|
+
isExisting: false,
|
|
18245
|
+
selected: false,
|
|
18246
|
+
subTopicId: chunk.subTopicId,
|
|
18247
|
+
parentTopicId: resolved.topicId,
|
|
18248
|
+
proposal
|
|
18249
|
+
});
|
|
18250
|
+
}
|
|
18251
|
+
}
|
|
18252
|
+
if (items.length === 0) {
|
|
18253
|
+
jsonError(`No importable cards for topic "${topic.id}"`);
|
|
18254
|
+
}
|
|
18255
|
+
jsonOut({
|
|
18256
|
+
success: true,
|
|
18257
|
+
topicId: resolved.topicId,
|
|
18258
|
+
sourceId: item.sourceId,
|
|
18259
|
+
items
|
|
18260
|
+
});
|
|
18261
|
+
});
|
|
18262
|
+
});
|
|
18263
|
+
bridgeCommand.command("curriculum-confirm-topic").description(
|
|
18264
|
+
"Create selected new cards and remove deselected existing cards (JSON)"
|
|
18265
|
+
).requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topicId <id>", "Resolved curriculum topic id").requiredOption("--sourceId <id>", "Parent page source database ID").requiredOption(
|
|
18266
|
+
"--create <json>",
|
|
18267
|
+
"JSON array of SourceProposalInput objects to import"
|
|
18268
|
+
).requiredOption(
|
|
18269
|
+
"--removeSlugs <json>",
|
|
18270
|
+
"JSON array of token slugs whose user cards should be deleted"
|
|
18271
|
+
).option("--user <id>", "User ID (default: whoami)").option(
|
|
18272
|
+
"--knowledge-context <context>",
|
|
18273
|
+
"Assign confirmed tokens to a knowledge context (repeatable)",
|
|
18274
|
+
(val, memo) => {
|
|
18275
|
+
memo.push(val);
|
|
18276
|
+
return memo;
|
|
18277
|
+
},
|
|
18278
|
+
[]
|
|
18279
|
+
).action(async (opts) => {
|
|
18280
|
+
await withDb2(async (db) => {
|
|
18281
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
18282
|
+
const provider = getCurriculumProvider(opts.provider);
|
|
18283
|
+
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
18284
|
+
let create;
|
|
18285
|
+
let removeSlugs;
|
|
18286
|
+
try {
|
|
18287
|
+
create = JSON.parse(opts.create);
|
|
18288
|
+
removeSlugs = JSON.parse(opts.removeSlugs);
|
|
18289
|
+
} catch {
|
|
18290
|
+
jsonError("Invalid --create or --removeSlugs JSON");
|
|
18291
|
+
return;
|
|
18292
|
+
}
|
|
18293
|
+
const contextNames = parseKnowledgeContextNames2(
|
|
18294
|
+
opts.knowledgeContext || []
|
|
18295
|
+
);
|
|
18296
|
+
const contexts = await resolveKnowledgeContexts2(db, contextNames);
|
|
18297
|
+
try {
|
|
18298
|
+
const result = await executeCurriculumConfirmOperations(
|
|
18299
|
+
db,
|
|
18300
|
+
userId,
|
|
18301
|
+
[
|
|
18302
|
+
{
|
|
18303
|
+
sourceId: opts.sourceId,
|
|
18304
|
+
provider: provider.id,
|
|
18305
|
+
topicId: opts.topicId,
|
|
18306
|
+
create,
|
|
18307
|
+
removeSlugs
|
|
18308
|
+
}
|
|
18309
|
+
],
|
|
18310
|
+
contexts
|
|
18311
|
+
);
|
|
18312
|
+
jsonOut({ success: true, ...result });
|
|
18313
|
+
} catch (err) {
|
|
18314
|
+
jsonError(err.message || String(err));
|
|
18315
|
+
}
|
|
18316
|
+
});
|
|
18317
|
+
});
|
|
18318
|
+
bridgeCommand.command("curriculum-confirm-batch").description("Atomically confirm multiple curriculum topic selections (JSON)").requiredOption(
|
|
18319
|
+
"--operations <json>",
|
|
18320
|
+
"JSON array of {sourceId, provider, topicId, create, removeSlugs}"
|
|
18321
|
+
).option("--user <id>", "User ID (default: whoami)").option(
|
|
18322
|
+
"--knowledge-context <context>",
|
|
18323
|
+
"Assign confirmed tokens to a knowledge context (repeatable)",
|
|
18324
|
+
(val, memo) => {
|
|
18325
|
+
memo.push(val);
|
|
18326
|
+
return memo;
|
|
18327
|
+
},
|
|
18328
|
+
[]
|
|
18329
|
+
).action(async (opts) => {
|
|
18330
|
+
await withDb2(async (db) => {
|
|
18331
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
18332
|
+
let operations;
|
|
18333
|
+
try {
|
|
18334
|
+
operations = JSON.parse(opts.operations);
|
|
18335
|
+
} catch {
|
|
18336
|
+
jsonError("Invalid --operations JSON");
|
|
18337
|
+
return;
|
|
18338
|
+
}
|
|
18339
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
18340
|
+
jsonError("--operations must be a non-empty JSON array");
|
|
18341
|
+
}
|
|
18342
|
+
for (const op of operations) {
|
|
18343
|
+
if (!op?.sourceId || !op?.provider || !op?.topicId) {
|
|
18344
|
+
jsonError("Each operation requires sourceId, provider, and topicId");
|
|
18345
|
+
}
|
|
18346
|
+
if (!getCurriculumProvider(op.provider)) {
|
|
18347
|
+
jsonError(`Unknown curriculum provider: ${op.provider}`);
|
|
18348
|
+
}
|
|
18349
|
+
if (!Array.isArray(op.create) || !Array.isArray(op.removeSlugs)) {
|
|
18350
|
+
jsonError("Each operation requires create and removeSlugs arrays");
|
|
18351
|
+
}
|
|
18352
|
+
}
|
|
18353
|
+
const contextNames = parseKnowledgeContextNames2(
|
|
18354
|
+
opts.knowledgeContext || []
|
|
18355
|
+
);
|
|
18356
|
+
const contexts = await resolveKnowledgeContexts2(db, contextNames);
|
|
18357
|
+
try {
|
|
18358
|
+
const result = await executeCurriculumConfirmOperations(
|
|
18359
|
+
db,
|
|
18360
|
+
userId,
|
|
18361
|
+
operations,
|
|
18362
|
+
contexts
|
|
18363
|
+
);
|
|
18364
|
+
jsonOut({ success: true, ...result });
|
|
18365
|
+
} catch (err) {
|
|
18366
|
+
jsonError(err.message || String(err));
|
|
18367
|
+
}
|
|
18368
|
+
});
|
|
18369
|
+
});
|
|
18370
|
+
bridgeCommand.command("curriculum-extract-topics").description(
|
|
18371
|
+
"Fetch and extract specific texts for selected curriculum topics (JSON)"
|
|
18372
|
+
).requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topics <json>", "JSON array of selected topic nodes").action(async (opts) => {
|
|
18373
|
+
await withDb2(async (db) => {
|
|
18374
|
+
const provider = getCurriculumProvider(opts.provider);
|
|
18375
|
+
if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
|
|
18376
|
+
let topics;
|
|
18377
|
+
try {
|
|
18378
|
+
topics = JSON.parse(opts.topics);
|
|
18379
|
+
} catch {
|
|
18380
|
+
jsonError("Invalid --topics JSON");
|
|
18381
|
+
return;
|
|
18382
|
+
}
|
|
18383
|
+
const extracted = await extractAndStoreCurriculumTopics(
|
|
18384
|
+
db,
|
|
18385
|
+
provider,
|
|
18386
|
+
topics
|
|
18387
|
+
);
|
|
16094
18388
|
jsonOut({ success: true, extracted });
|
|
16095
18389
|
});
|
|
16096
18390
|
});
|
|
@@ -16324,6 +18618,9 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
16324
18618
|
output: process.stdout,
|
|
16325
18619
|
terminal: false
|
|
16326
18620
|
});
|
|
18621
|
+
process.on("unhandledRejection", (reason) => {
|
|
18622
|
+
logDiag(`unhandledRejection: ${String(reason)}`);
|
|
18623
|
+
});
|
|
16327
18624
|
let pending = Promise.resolve();
|
|
16328
18625
|
rl.on("line", (line) => {
|
|
16329
18626
|
if (!line.trim()) return;
|
|
@@ -16331,17 +18628,26 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
16331
18628
|
const response = await processRequest(line);
|
|
16332
18629
|
process.stdout.write(`${response}
|
|
16333
18630
|
`);
|
|
18631
|
+
}).catch((err) => {
|
|
18632
|
+
logDiag(`serve request failed: ${err.message || err}`);
|
|
18633
|
+
try {
|
|
18634
|
+
process.stdout.write(
|
|
18635
|
+
`${JSON.stringify({ id: null, error: err.message || String(err) })}
|
|
18636
|
+
`
|
|
18637
|
+
);
|
|
18638
|
+
} catch {
|
|
18639
|
+
}
|
|
16334
18640
|
});
|
|
16335
18641
|
});
|
|
16336
18642
|
});
|
|
16337
18643
|
|
|
16338
18644
|
// src/cli/commands/mcp.ts
|
|
16339
|
-
var __dirname =
|
|
16340
|
-
var pkgPath =
|
|
16341
|
-
if (!
|
|
16342
|
-
pkgPath =
|
|
18645
|
+
var __dirname = dirname11(fileURLToPath6(import.meta.url));
|
|
18646
|
+
var pkgPath = join23(__dirname, "..", "..", "package.json");
|
|
18647
|
+
if (!existsSync19(pkgPath)) {
|
|
18648
|
+
pkgPath = join23(__dirname, "..", "..", "..", "package.json");
|
|
16343
18649
|
}
|
|
16344
|
-
var pkg = JSON.parse(
|
|
18650
|
+
var pkg = JSON.parse(readFileSync17(pkgPath, "utf-8"));
|
|
16345
18651
|
var STUDIO_RESOURCE_URI = "ui://zam/studio";
|
|
16346
18652
|
var RECALL_RESOURCE_URI = "ui://zam/recall";
|
|
16347
18653
|
var GRAPH_RESOURCE_URI = "ui://zam/graph";
|
|
@@ -16366,13 +18672,13 @@ var STUDIO_BRIDGE_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16366
18672
|
function loadPanelHtml(fileName, placeholderTitle) {
|
|
16367
18673
|
const candidates = [
|
|
16368
18674
|
// dist/cli/commands/mcp.js → dist/ui/
|
|
16369
|
-
|
|
18675
|
+
join23(__dirname, "..", "..", "ui", fileName),
|
|
16370
18676
|
// src/cli/commands/mcp.ts via tsx → <repo>/dist/ui/
|
|
16371
|
-
|
|
18677
|
+
join23(__dirname, "..", "..", "..", "dist", "ui", fileName)
|
|
16372
18678
|
];
|
|
16373
18679
|
for (const candidate of candidates) {
|
|
16374
|
-
if (
|
|
16375
|
-
return
|
|
18680
|
+
if (existsSync19(candidate)) {
|
|
18681
|
+
return readFileSync17(candidate, "utf-8");
|
|
16376
18682
|
}
|
|
16377
18683
|
}
|
|
16378
18684
|
return `<!doctype html>
|