zam-core 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1230 -919
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -3630,6 +3630,11 @@ function generatePrompt(input8) {
|
|
|
3630
3630
|
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
3631
3631
|
import { dirname as dirname3, join as join7, resolve } from "path";
|
|
3632
3632
|
var DEFAULT_REVIEW_CONTEXT_MAX_CHARS = 6e3;
|
|
3633
|
+
var REVIEW_CONTEXT_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
3634
|
+
var reviewContextCache = /* @__PURE__ */ new Map();
|
|
3635
|
+
function reviewContextCacheKey(sourceLink, maxChars) {
|
|
3636
|
+
return `${sourceLink}\0${maxChars}`;
|
|
3637
|
+
}
|
|
3633
3638
|
function htmlToText(html) {
|
|
3634
3639
|
let content = html;
|
|
3635
3640
|
const bodyMatch = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(html);
|
|
@@ -3772,6 +3777,11 @@ async function resolveReviewContext(sourceLink, opts = {}) {
|
|
|
3772
3777
|
const cleaned = sourceLink?.trim();
|
|
3773
3778
|
if (!cleaned) return null;
|
|
3774
3779
|
const maxChars = opts.maxChars ?? DEFAULT_REVIEW_CONTEXT_MAX_CHARS;
|
|
3780
|
+
const cacheKey = reviewContextCacheKey(cleaned, maxChars);
|
|
3781
|
+
const cached = reviewContextCache.get(cacheKey);
|
|
3782
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
3783
|
+
return cached.context;
|
|
3784
|
+
}
|
|
3775
3785
|
const resolved = await resolveReference(cleaned);
|
|
3776
3786
|
let content = resolved.content;
|
|
3777
3787
|
let truncated = false;
|
|
@@ -3779,7 +3789,7 @@ async function resolveReviewContext(sourceLink, opts = {}) {
|
|
|
3779
3789
|
content = content.slice(0, maxChars);
|
|
3780
3790
|
truncated = true;
|
|
3781
3791
|
}
|
|
3782
|
-
|
|
3792
|
+
const context = {
|
|
3783
3793
|
sourceLink: cleaned,
|
|
3784
3794
|
sourceType: resolved.sourceType,
|
|
3785
3795
|
content,
|
|
@@ -3787,6 +3797,11 @@ async function resolveReviewContext(sourceLink, opts = {}) {
|
|
|
3787
3797
|
url: resolved.url,
|
|
3788
3798
|
truncated
|
|
3789
3799
|
};
|
|
3800
|
+
reviewContextCache.set(cacheKey, {
|
|
3801
|
+
context,
|
|
3802
|
+
expiresAt: Date.now() + REVIEW_CONTEXT_CACHE_TTL_MS
|
|
3803
|
+
});
|
|
3804
|
+
return context;
|
|
3790
3805
|
}
|
|
3791
3806
|
function normalizePath(p) {
|
|
3792
3807
|
const base = p.split("#")[0].trim();
|
|
@@ -5368,12 +5383,28 @@ import {
|
|
|
5368
5383
|
} from "fs";
|
|
5369
5384
|
import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
|
|
5370
5385
|
import { basename as basename5, join as join17, resolve as resolve5 } from "path";
|
|
5371
|
-
import { Command as
|
|
5386
|
+
import { Command as Command5 } from "commander";
|
|
5372
5387
|
|
|
5373
5388
|
// src/cli/llm/client.ts
|
|
5374
5389
|
import { spawn as spawn2 } from "child_process";
|
|
5375
5390
|
import { existsSync as existsSync14 } from "fs";
|
|
5376
5391
|
var DEFAULT_LLM_URL = "http://localhost:8000/v1";
|
|
5392
|
+
var DEFAULT_LLM_MAX_TOKENS = 1e4;
|
|
5393
|
+
var RECALL_QUESTION_MAX_OUTPUT_TOKENS = 400;
|
|
5394
|
+
var RECALL_EVALUATION_MAX_OUTPUT_TOKENS = 1200;
|
|
5395
|
+
var RECALL_ENDPOINT_CACHE_MS = 6e4;
|
|
5396
|
+
var cachedRecallEndpoint = null;
|
|
5397
|
+
function recallEndpointSignature(cfg) {
|
|
5398
|
+
return [
|
|
5399
|
+
cfg.enabled ? "on" : "off",
|
|
5400
|
+
cfg.providerName ?? "",
|
|
5401
|
+
cfg.url,
|
|
5402
|
+
cfg.model,
|
|
5403
|
+
cfg.apiFlavor,
|
|
5404
|
+
cfg.fallback?.url ?? "",
|
|
5405
|
+
cfg.fallback?.model ?? ""
|
|
5406
|
+
].join("|");
|
|
5407
|
+
}
|
|
5377
5408
|
var DEFAULT_LLM_MODEL = "qwen3.5:4b";
|
|
5378
5409
|
var DEFAULT_LLM_API_KEY = "sk-none";
|
|
5379
5410
|
async function getLlmConfig(db) {
|
|
@@ -5484,6 +5515,7 @@ function materializeProvider(rec, base, role, meta) {
|
|
|
5484
5515
|
label: rec.label,
|
|
5485
5516
|
source: meta.source,
|
|
5486
5517
|
local: rec.local ?? isLocalEndpoint(url),
|
|
5518
|
+
...rec.runner ? { runner: rec.runner } : {},
|
|
5487
5519
|
...role === "vision" ? { maxFrames: base.maxFrames } : {}
|
|
5488
5520
|
};
|
|
5489
5521
|
}
|
|
@@ -5566,10 +5598,7 @@ async function readChatContent(res, label) {
|
|
|
5566
5598
|
}
|
|
5567
5599
|
async function generateQuestionViaLLM(db, input8) {
|
|
5568
5600
|
const cfg = await getProviderForRole(db, "recall");
|
|
5569
|
-
|
|
5570
|
-
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5571
|
-
}
|
|
5572
|
-
assertChatCompletions(cfg);
|
|
5601
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
5573
5602
|
const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
|
|
5574
5603
|
const verb = BLOOM_VERBS2[bloom];
|
|
5575
5604
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
@@ -5589,31 +5618,36 @@ ${input8.sourceLinkContent ? `Source Reference:
|
|
|
5589
5618
|
${input8.sourceLinkContent}` : ""}
|
|
5590
5619
|
|
|
5591
5620
|
Active-Recall Question:`;
|
|
5592
|
-
const res = await fetchWithInteractiveTimeout(
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5621
|
+
const res = await fetchWithInteractiveTimeout(
|
|
5622
|
+
`${endpoint.url}/chat/completions`,
|
|
5623
|
+
{
|
|
5624
|
+
method: "POST",
|
|
5625
|
+
headers: {
|
|
5626
|
+
"Content-Type": "application/json",
|
|
5627
|
+
Authorization: `Bearer ${endpoint.apiKey}`
|
|
5628
|
+
},
|
|
5629
|
+
body: JSON.stringify({
|
|
5630
|
+
model: endpoint.model,
|
|
5631
|
+
messages: [
|
|
5632
|
+
{ role: "system", content: systemPrompt },
|
|
5633
|
+
{ role: "user", content: userPrompt }
|
|
5634
|
+
],
|
|
5635
|
+
temperature: 0.1,
|
|
5636
|
+
max_tokens: RECALL_QUESTION_MAX_OUTPUT_TOKENS
|
|
5637
|
+
}),
|
|
5638
|
+
locale: cfg.locale
|
|
5639
|
+
}
|
|
5640
|
+
);
|
|
5641
|
+
const text = await readChatContent(res, "LLM request");
|
|
5642
|
+
return {
|
|
5643
|
+
text,
|
|
5644
|
+
model: endpoint.model,
|
|
5645
|
+
providerName: endpoint.providerName
|
|
5646
|
+
};
|
|
5610
5647
|
}
|
|
5611
5648
|
async function evaluateAnswerViaLLM(db, input8) {
|
|
5612
5649
|
const cfg = await getProviderForRole(db, "recall");
|
|
5613
|
-
|
|
5614
|
-
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5615
|
-
}
|
|
5616
|
-
assertChatCompletions(cfg);
|
|
5650
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
5617
5651
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
5618
5652
|
const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
|
|
5619
5653
|
const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
|
|
@@ -5643,51 +5677,59 @@ ${input8.sourceLinkContent ? `Source Code Reference:
|
|
|
5643
5677
|
${input8.sourceLinkContent}` : ""}
|
|
5644
5678
|
|
|
5645
5679
|
Evaluation:`;
|
|
5646
|
-
const res = await fetchWithInteractiveTimeout(
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5680
|
+
const res = await fetchWithInteractiveTimeout(
|
|
5681
|
+
`${endpoint.url}/chat/completions`,
|
|
5682
|
+
{
|
|
5683
|
+
method: "POST",
|
|
5684
|
+
headers: {
|
|
5685
|
+
"Content-Type": "application/json",
|
|
5686
|
+
Authorization: `Bearer ${endpoint.apiKey}`
|
|
5687
|
+
},
|
|
5688
|
+
body: JSON.stringify({
|
|
5689
|
+
model: endpoint.model,
|
|
5690
|
+
messages: [
|
|
5691
|
+
{ role: "system", content: systemPrompt },
|
|
5692
|
+
{ role: "user", content: userPrompt }
|
|
5693
|
+
],
|
|
5694
|
+
temperature: 0.2,
|
|
5695
|
+
max_tokens: RECALL_EVALUATION_MAX_OUTPUT_TOKENS
|
|
5696
|
+
}),
|
|
5697
|
+
locale: cfg.locale
|
|
5698
|
+
}
|
|
5699
|
+
);
|
|
5700
|
+
const text = await readChatContent(res, "LLM evaluation");
|
|
5701
|
+
return {
|
|
5702
|
+
text,
|
|
5703
|
+
model: endpoint.model,
|
|
5704
|
+
providerName: endpoint.providerName
|
|
5705
|
+
};
|
|
5664
5706
|
}
|
|
5665
5707
|
async function translateQuestionViaLLM(db, question) {
|
|
5666
5708
|
const cfg = await getProviderForRole(db, "recall");
|
|
5667
|
-
|
|
5668
|
-
throw new Error("LLM integration is disabled in settings");
|
|
5669
|
-
}
|
|
5670
|
-
assertChatCompletions(cfg);
|
|
5709
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
5671
5710
|
const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
5672
5711
|
const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
|
|
5673
5712
|
Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
|
|
5674
|
-
const res = await fetchWithInteractiveTimeout(
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5713
|
+
const res = await fetchWithInteractiveTimeout(
|
|
5714
|
+
`${endpoint.url}/chat/completions`,
|
|
5715
|
+
{
|
|
5716
|
+
method: "POST",
|
|
5717
|
+
headers: {
|
|
5718
|
+
"Content-Type": "application/json",
|
|
5719
|
+
Authorization: `Bearer ${endpoint.apiKey}`
|
|
5720
|
+
},
|
|
5721
|
+
body: JSON.stringify({
|
|
5722
|
+
model: endpoint.model,
|
|
5723
|
+
messages: [
|
|
5724
|
+
{ role: "system", content: systemPrompt },
|
|
5725
|
+
{ role: "user", content: question }
|
|
5726
|
+
],
|
|
5727
|
+
temperature: 0.1,
|
|
5728
|
+
max_tokens: DEFAULT_LLM_MAX_TOKENS
|
|
5729
|
+
}),
|
|
5730
|
+
locale: cfg.locale
|
|
5731
|
+
}
|
|
5732
|
+
);
|
|
5691
5733
|
return readChatContent(res, "Translation");
|
|
5692
5734
|
}
|
|
5693
5735
|
async function isLlmOnline(url) {
|
|
@@ -5760,6 +5802,107 @@ async function checkProviderChain(primary) {
|
|
|
5760
5802
|
}
|
|
5761
5803
|
return { primary: first };
|
|
5762
5804
|
}
|
|
5805
|
+
async function resolveUsableRecallEndpoint(db) {
|
|
5806
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5807
|
+
if (!cfg.enabled) {
|
|
5808
|
+
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5809
|
+
}
|
|
5810
|
+
assertChatCompletions(cfg);
|
|
5811
|
+
const signature = recallEndpointSignature(cfg);
|
|
5812
|
+
if (cachedRecallEndpoint && cachedRecallEndpoint.signature === signature && cachedRecallEndpoint.expiresAt > Date.now()) {
|
|
5813
|
+
return cachedRecallEndpoint.endpoint;
|
|
5814
|
+
}
|
|
5815
|
+
const chain = await checkProviderChain(cfg);
|
|
5816
|
+
const selected = chain.firstUsable;
|
|
5817
|
+
if (!selected || !isEndpointUsable(selected)) {
|
|
5818
|
+
throw new Error("No recall LLM endpoint is online");
|
|
5819
|
+
}
|
|
5820
|
+
cachedRecallEndpoint = {
|
|
5821
|
+
endpoint: selected.endpoint,
|
|
5822
|
+
signature,
|
|
5823
|
+
expiresAt: Date.now() + RECALL_ENDPOINT_CACHE_MS
|
|
5824
|
+
};
|
|
5825
|
+
return selected.endpoint;
|
|
5826
|
+
}
|
|
5827
|
+
async function prepareRecallChain(db, opts) {
|
|
5828
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5829
|
+
const fail = (reason, partial = {}) => ({
|
|
5830
|
+
usable: false,
|
|
5831
|
+
reason,
|
|
5832
|
+
online: false,
|
|
5833
|
+
model: cfg.model,
|
|
5834
|
+
availableModels: [],
|
|
5835
|
+
providerName: cfg.providerName,
|
|
5836
|
+
label: cfg.label,
|
|
5837
|
+
local: cfg.local ?? isLocalEndpoint(cfg.url),
|
|
5838
|
+
activeTier: "primary",
|
|
5839
|
+
...partial
|
|
5840
|
+
});
|
|
5841
|
+
if (!cfg.enabled) return fail("disabled");
|
|
5842
|
+
if (cfg.apiFlavor !== "chat-completions") return fail("unsupported-provider");
|
|
5843
|
+
const chain = providerChain(cfg);
|
|
5844
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
5845
|
+
let lastReason = "offline";
|
|
5846
|
+
let lastOnline = false;
|
|
5847
|
+
let lastModel = cfg.model;
|
|
5848
|
+
let lastAvailable = [];
|
|
5849
|
+
for (let index = 0; index < chain.length; index++) {
|
|
5850
|
+
const endpoint = chain[index];
|
|
5851
|
+
let online = await isLlmOnline(endpoint.url);
|
|
5852
|
+
if (!online && (endpoint.local ?? isLocalEndpoint(endpoint.url))) {
|
|
5853
|
+
if (opts.interactive) {
|
|
5854
|
+
online = await startLocalRunner(
|
|
5855
|
+
endpoint.url,
|
|
5856
|
+
endpoint.model,
|
|
5857
|
+
cfg.locale,
|
|
5858
|
+
endpoint.runner
|
|
5859
|
+
);
|
|
5860
|
+
} else {
|
|
5861
|
+
spawnLocalRunner(endpoint.url, endpoint.model, endpoint.runner);
|
|
5862
|
+
while (Date.now() < deadline) {
|
|
5863
|
+
await new Promise((resolve9) => setTimeout(resolve9, 1e3));
|
|
5864
|
+
if (await isLlmOnline(endpoint.url)) {
|
|
5865
|
+
online = true;
|
|
5866
|
+
break;
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
lastOnline = online;
|
|
5872
|
+
lastModel = endpoint.model;
|
|
5873
|
+
if (!online) {
|
|
5874
|
+
lastReason = "offline";
|
|
5875
|
+
continue;
|
|
5876
|
+
}
|
|
5877
|
+
const availableModels = await getAvailableModels(
|
|
5878
|
+
endpoint.url,
|
|
5879
|
+
endpoint.apiKey
|
|
5880
|
+
);
|
|
5881
|
+
lastAvailable = availableModels;
|
|
5882
|
+
const modelKnown = availableModels.length === 0 || availableModels.some(
|
|
5883
|
+
(candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
|
|
5884
|
+
);
|
|
5885
|
+
if (!modelKnown) {
|
|
5886
|
+
lastReason = "model-not-found";
|
|
5887
|
+
continue;
|
|
5888
|
+
}
|
|
5889
|
+
return {
|
|
5890
|
+
usable: true,
|
|
5891
|
+
online: true,
|
|
5892
|
+
model: endpoint.model,
|
|
5893
|
+
availableModels,
|
|
5894
|
+
providerName: endpoint.providerName,
|
|
5895
|
+
label: endpoint.label,
|
|
5896
|
+
local: endpoint.local ?? isLocalEndpoint(endpoint.url),
|
|
5897
|
+
activeTier: index === 0 ? "primary" : "fallback"
|
|
5898
|
+
};
|
|
5899
|
+
}
|
|
5900
|
+
return fail(lastReason, {
|
|
5901
|
+
online: lastOnline,
|
|
5902
|
+
model: lastModel,
|
|
5903
|
+
availableModels: lastAvailable
|
|
5904
|
+
});
|
|
5905
|
+
}
|
|
5763
5906
|
async function isVisionProviderModelExplicit(db, active) {
|
|
5764
5907
|
if (await getSetting(db, "llm.vision.model")) return true;
|
|
5765
5908
|
const providers = await readJsonSetting(db, "llm.providers");
|
|
@@ -5861,13 +6004,8 @@ async function getProviderRoleStatus(db, role) {
|
|
|
5861
6004
|
fallback: summarizeFallback(cfg.fallback)
|
|
5862
6005
|
};
|
|
5863
6006
|
}
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
const chain = await checkProviderChain(cfg);
|
|
5867
|
-
selected = chain.firstUsable ?? chain.primary;
|
|
5868
|
-
} else {
|
|
5869
|
-
selected = await checkProviderEndpoint(cfg);
|
|
5870
|
-
}
|
|
6007
|
+
const chain = await checkProviderChain(cfg);
|
|
6008
|
+
const selected = chain.firstUsable ?? chain.primary;
|
|
5871
6009
|
const active = selected.endpoint;
|
|
5872
6010
|
const usable = selected.online && selected.modelAvailable;
|
|
5873
6011
|
const reason = usable ? void 0 : selected.online ? "model-not-found" : "offline";
|
|
@@ -5889,7 +6027,35 @@ async function getProviderRoleStatus(db, role) {
|
|
|
5889
6027
|
fallback: summarizeFallback(cfg.fallback)
|
|
5890
6028
|
};
|
|
5891
6029
|
}
|
|
5892
|
-
function
|
|
6030
|
+
function runnerKindFromHint(hint) {
|
|
6031
|
+
if (!hint) return void 0;
|
|
6032
|
+
const normalized = hint.trim().toLowerCase();
|
|
6033
|
+
if (normalized === "flm" || normalized === "fastflowlm") {
|
|
6034
|
+
return "fastflowlm";
|
|
6035
|
+
}
|
|
6036
|
+
if (normalized === "ollama") return "ollama";
|
|
6037
|
+
if (normalized === "foundry-local" || normalized === "foundry" || normalized === "generic") {
|
|
6038
|
+
return "generic";
|
|
6039
|
+
}
|
|
6040
|
+
return void 0;
|
|
6041
|
+
}
|
|
6042
|
+
function defaultPortForRunner(runner, url) {
|
|
6043
|
+
try {
|
|
6044
|
+
const urlObj = new URL(url);
|
|
6045
|
+
const explicit = urlObj.port;
|
|
6046
|
+
if (explicit) return explicit;
|
|
6047
|
+
if (runner === "ollama") return "11434";
|
|
6048
|
+
if (urlObj.protocol === "https:") return "443";
|
|
6049
|
+
return "80";
|
|
6050
|
+
} catch {
|
|
6051
|
+
return runner === "ollama" ? "11434" : "8000";
|
|
6052
|
+
}
|
|
6053
|
+
}
|
|
6054
|
+
function detectRunner(url, model, hint) {
|
|
6055
|
+
const fromHint = runnerKindFromHint(hint);
|
|
6056
|
+
if (fromHint) {
|
|
6057
|
+
return { runner: fromHint, port: defaultPortForRunner(fromHint, url) };
|
|
6058
|
+
}
|
|
5893
6059
|
let runner = "unknown";
|
|
5894
6060
|
let port = "8000";
|
|
5895
6061
|
try {
|
|
@@ -5905,8 +6071,8 @@ function detectRunner(url, model) {
|
|
|
5905
6071
|
}
|
|
5906
6072
|
return { runner, port };
|
|
5907
6073
|
}
|
|
5908
|
-
function spawnLocalRunner(url, model) {
|
|
5909
|
-
const { runner, port } = detectRunner(url, model);
|
|
6074
|
+
function spawnLocalRunner(url, model, hint) {
|
|
6075
|
+
const { runner, port } = detectRunner(url, model, hint);
|
|
5910
6076
|
try {
|
|
5911
6077
|
if (runner === "fastflowlm") {
|
|
5912
6078
|
const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
|
|
@@ -5927,8 +6093,8 @@ function spawnLocalRunner(url, model) {
|
|
|
5927
6093
|
} catch {
|
|
5928
6094
|
}
|
|
5929
6095
|
}
|
|
5930
|
-
async function startLocalRunner(url, model, locale) {
|
|
5931
|
-
const { runner, port } = detectRunner(url, model);
|
|
6096
|
+
async function startLocalRunner(url, model, locale, hint) {
|
|
6097
|
+
const { runner, port } = detectRunner(url, model, hint);
|
|
5932
6098
|
if (runner === "fastflowlm") {
|
|
5933
6099
|
const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
|
|
5934
6100
|
if (!hasCommand("flm") && flmExe === "flm") {
|
|
@@ -6012,105 +6178,39 @@ async function startLocalRunner(url, model, locale) {
|
|
|
6012
6178
|
}
|
|
6013
6179
|
}
|
|
6014
6180
|
async function ensureLocalLlmRunning(db) {
|
|
6015
|
-
const
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
}
|
|
6019
|
-
if (
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
return { usable: false, reason: "unsupported-provider" };
|
|
6024
|
-
}
|
|
6025
|
-
const { url, model, apiKey, locale } = cfg;
|
|
6026
|
-
const isLocal = isLocalEndpoint(url);
|
|
6027
|
-
console.log(`Checking if local LLM server is online at ${url}...`);
|
|
6028
|
-
let online = await isLlmOnline(url);
|
|
6029
|
-
if (!online && isLocal) {
|
|
6030
|
-
console.log(`\x1B[33m\u26A0 Local LLM server is offline on ${url}.\x1B[0m`);
|
|
6031
|
-
online = await startLocalRunner(url, model, locale);
|
|
6032
|
-
}
|
|
6033
|
-
if (!online) {
|
|
6034
|
-
console.warn(
|
|
6035
|
-
`\x1B[33m\u26A0 LLM server is not reachable at ${url}. Continuing without AI coaching.\x1B[0m
|
|
6036
|
-
`
|
|
6181
|
+
const result = await prepareRecallChain(db, {
|
|
6182
|
+
timeoutMs: 25e3,
|
|
6183
|
+
interactive: true
|
|
6184
|
+
});
|
|
6185
|
+
if (result.usable) {
|
|
6186
|
+
const location = result.local ? "local" : "cloud";
|
|
6187
|
+
console.log(
|
|
6188
|
+
`\x1B[32m\u2713 Recall LLM ready (${location}: ${result.model}).\x1B[0m`
|
|
6037
6189
|
);
|
|
6038
|
-
return { usable:
|
|
6190
|
+
return { usable: true };
|
|
6039
6191
|
}
|
|
6040
|
-
|
|
6041
|
-
const available = await getAvailableModels(url, apiKey);
|
|
6042
|
-
const modelKnown = available.length === 0 || available.some((m) => m.toLowerCase() === model.toLowerCase());
|
|
6043
|
-
if (!modelKnown) {
|
|
6192
|
+
if (result.reason === "unsupported-provider") {
|
|
6044
6193
|
console.warn(
|
|
6045
|
-
`\x1B[31m\u2717
|
|
6194
|
+
`\x1B[31m\u2717 Recall provider is not supported for active recall.\x1B[0m`
|
|
6046
6195
|
);
|
|
6047
|
-
|
|
6196
|
+
} else if (result.reason === "model-not-found") {
|
|
6048
6197
|
console.warn(
|
|
6049
|
-
|
|
6198
|
+
`\x1B[31m\u2717 Configured model "${result.model}" is not available on the server.\x1B[0m`
|
|
6050
6199
|
);
|
|
6200
|
+
console.warn(` Available models: ${result.availableModels.join(", ")}`);
|
|
6201
|
+
} else if (result.reason === "offline") {
|
|
6051
6202
|
console.warn(
|
|
6052
|
-
|
|
6203
|
+
`\x1B[33m\u26A0 No recall LLM endpoint is reachable. Continuing without AI coaching.\x1B[0m
|
|
6204
|
+
`
|
|
6053
6205
|
);
|
|
6054
|
-
return { usable: false, reason: "model-not-found" };
|
|
6055
6206
|
}
|
|
6056
|
-
return { usable:
|
|
6207
|
+
return { usable: false, reason: result.reason };
|
|
6057
6208
|
}
|
|
6058
6209
|
async function ensureLlmReadyHeadless(db, opts = {}) {
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
return {
|
|
6064
|
-
usable: false,
|
|
6065
|
-
reason: "disabled",
|
|
6066
|
-
online: false,
|
|
6067
|
-
model,
|
|
6068
|
-
availableModels: []
|
|
6069
|
-
};
|
|
6070
|
-
}
|
|
6071
|
-
if (cfg.apiFlavor !== "chat-completions") {
|
|
6072
|
-
return {
|
|
6073
|
-
usable: false,
|
|
6074
|
-
reason: "unsupported-provider",
|
|
6075
|
-
online: false,
|
|
6076
|
-
model,
|
|
6077
|
-
availableModels: []
|
|
6078
|
-
};
|
|
6079
|
-
}
|
|
6080
|
-
const isLocal = isLocalEndpoint(url);
|
|
6081
|
-
let online = await isLlmOnline(url);
|
|
6082
|
-
if (!online && isLocal) {
|
|
6083
|
-
spawnLocalRunner(url, model);
|
|
6084
|
-
const deadline = Date.now() + timeoutMs;
|
|
6085
|
-
while (Date.now() < deadline) {
|
|
6086
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
6087
|
-
if (await isLlmOnline(url)) {
|
|
6088
|
-
online = true;
|
|
6089
|
-
break;
|
|
6090
|
-
}
|
|
6091
|
-
}
|
|
6092
|
-
}
|
|
6093
|
-
if (!online) {
|
|
6094
|
-
return {
|
|
6095
|
-
usable: false,
|
|
6096
|
-
reason: "offline",
|
|
6097
|
-
online: false,
|
|
6098
|
-
model,
|
|
6099
|
-
availableModels: []
|
|
6100
|
-
};
|
|
6101
|
-
}
|
|
6102
|
-
const availableModels = await getAvailableModels(url, apiKey);
|
|
6103
|
-
const modelKnown = availableModels.length === 0 || availableModels.some((m) => m.toLowerCase() === model.toLowerCase());
|
|
6104
|
-
if (!modelKnown) {
|
|
6105
|
-
return {
|
|
6106
|
-
usable: false,
|
|
6107
|
-
reason: "model-not-found",
|
|
6108
|
-
online: true,
|
|
6109
|
-
model,
|
|
6110
|
-
availableModels
|
|
6111
|
-
};
|
|
6112
|
-
}
|
|
6113
|
-
return { usable: true, online: true, model, availableModels };
|
|
6210
|
+
return prepareRecallChain(db, {
|
|
6211
|
+
timeoutMs: opts.timeoutMs ?? 25e3,
|
|
6212
|
+
interactive: false
|
|
6213
|
+
});
|
|
6114
6214
|
}
|
|
6115
6215
|
async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
6116
6216
|
const {
|
|
@@ -6200,15 +6300,22 @@ async function ensureHighQualityQuestion(db, token) {
|
|
|
6200
6300
|
bloomLevel: token.bloomLevel,
|
|
6201
6301
|
sourceLinkContent
|
|
6202
6302
|
});
|
|
6203
|
-
if (generated
|
|
6204
|
-
await updateToken(db, token.slug, { question: generated });
|
|
6205
|
-
return
|
|
6303
|
+
if (generated.text.trim().length > 0) {
|
|
6304
|
+
await updateToken(db, token.slug, { question: generated.text });
|
|
6305
|
+
return {
|
|
6306
|
+
question: generated.text,
|
|
6307
|
+
source: "llm",
|
|
6308
|
+
model: generated.model
|
|
6309
|
+
};
|
|
6206
6310
|
}
|
|
6207
6311
|
} catch {
|
|
6208
6312
|
}
|
|
6209
6313
|
}
|
|
6210
6314
|
if (token.question && token.question.trim().length > 0) {
|
|
6211
|
-
return
|
|
6315
|
+
return {
|
|
6316
|
+
question: token.question.trim(),
|
|
6317
|
+
source: "original"
|
|
6318
|
+
};
|
|
6212
6319
|
}
|
|
6213
6320
|
return null;
|
|
6214
6321
|
}
|
|
@@ -6416,7 +6523,7 @@ async function requestChatCompletionsVisionDraft(args) {
|
|
|
6416
6523
|
}
|
|
6417
6524
|
],
|
|
6418
6525
|
temperature: 0,
|
|
6419
|
-
max_tokens: args.input.maxTokens ??
|
|
6526
|
+
max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
|
|
6420
6527
|
...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
|
|
6421
6528
|
}),
|
|
6422
6529
|
locale: args.locale,
|
|
@@ -6472,7 +6579,7 @@ async function requestAnthropicVisionDraft(args) {
|
|
|
6472
6579
|
},
|
|
6473
6580
|
body: JSON.stringify({
|
|
6474
6581
|
model: args.model,
|
|
6475
|
-
max_tokens: args.input.maxTokens ??
|
|
6582
|
+
max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
|
|
6476
6583
|
system: VISION_SYSTEM_PROMPT,
|
|
6477
6584
|
messages: [
|
|
6478
6585
|
{
|
|
@@ -6627,271 +6734,642 @@ function isRecord2(value) {
|
|
|
6627
6734
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6628
6735
|
}
|
|
6629
6736
|
|
|
6630
|
-
// src/cli/commands/
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6737
|
+
// src/cli/commands/provider.ts
|
|
6738
|
+
import { password } from "@inquirer/prompts";
|
|
6739
|
+
import { Command as Command2 } from "commander";
|
|
6740
|
+
|
|
6741
|
+
// src/cli/commands/shared/db.ts
|
|
6742
|
+
function defaultErrorHandler(message) {
|
|
6743
|
+
console.error("Error:", message);
|
|
6744
|
+
process.exit(1);
|
|
6637
6745
|
}
|
|
6638
|
-
async function
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
}
|
|
6646
|
-
|
|
6746
|
+
async function withDb(fn, onError = defaultErrorHandler) {
|
|
6747
|
+
let db;
|
|
6748
|
+
try {
|
|
6749
|
+
db = await openDatabase();
|
|
6750
|
+
await fn(db);
|
|
6751
|
+
} catch (err) {
|
|
6752
|
+
onError(err.message);
|
|
6753
|
+
} finally {
|
|
6754
|
+
await db?.close();
|
|
6647
6755
|
}
|
|
6648
|
-
|
|
6756
|
+
}
|
|
6757
|
+
function jsonOut(data) {
|
|
6758
|
+
console.log(JSON.stringify(data, null, 2));
|
|
6649
6759
|
}
|
|
6650
6760
|
|
|
6651
|
-
// src/cli/commands/
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
fileURLToPath2(new URL("../..", import.meta.url)),
|
|
6668
|
-
fileURLToPath2(new URL("../../..", import.meta.url))
|
|
6669
|
-
].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
|
|
6670
|
-
var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
|
|
6671
|
-
var SKILL_PAIRS = [
|
|
6672
|
-
{
|
|
6673
|
-
from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
|
|
6674
|
-
to: join15(".claude", "skills", "zam", "SKILL.md"),
|
|
6675
|
-
agents: ["claude", "copilot"]
|
|
6676
|
-
},
|
|
6677
|
-
{
|
|
6678
|
-
from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
|
|
6679
|
-
to: join15(".agent", "skills", "zam", "SKILL.md"),
|
|
6680
|
-
agents: ["agent"]
|
|
6681
|
-
},
|
|
6682
|
-
{
|
|
6683
|
-
from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
|
|
6684
|
-
to: join15(".agents", "skills", "zam", "SKILL.md"),
|
|
6685
|
-
agents: ["codex"]
|
|
6686
|
-
}
|
|
6687
|
-
];
|
|
6688
|
-
function parseSetupAgents(value) {
|
|
6689
|
-
if (!value || value.trim().toLowerCase() === "all") {
|
|
6690
|
-
return new Set(ALL_SETUP_AGENTS);
|
|
6691
|
-
}
|
|
6692
|
-
const aliases = {
|
|
6693
|
-
claude: ["claude"],
|
|
6694
|
-
copilot: ["copilot"],
|
|
6695
|
-
codex: ["codex"],
|
|
6696
|
-
agent: ["agent"],
|
|
6697
|
-
opencode: ["agent"]
|
|
6698
|
-
};
|
|
6699
|
-
const selected = /* @__PURE__ */ new Set();
|
|
6700
|
-
for (const raw of value.split(",")) {
|
|
6701
|
-
const key = raw.trim().toLowerCase();
|
|
6702
|
-
const mapped = aliases[key];
|
|
6703
|
-
if (!mapped) {
|
|
6704
|
-
throw new Error(
|
|
6705
|
-
`Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
|
|
6706
|
-
);
|
|
6707
|
-
}
|
|
6708
|
-
for (const item of mapped) selected.add(item);
|
|
6709
|
-
}
|
|
6710
|
-
return selected;
|
|
6761
|
+
// src/cli/commands/provider.ts
|
|
6762
|
+
var VALID_API_FLAVORS = [
|
|
6763
|
+
"chat-completions",
|
|
6764
|
+
"anthropic-messages"
|
|
6765
|
+
];
|
|
6766
|
+
var VALID_ROLES = ["vision", "recall", "text"];
|
|
6767
|
+
function upsertProviderRecord(providers, name, patch) {
|
|
6768
|
+
const merged = { ...providers[name] ?? {} };
|
|
6769
|
+
if (patch.url !== void 0) merged.url = patch.url;
|
|
6770
|
+
if (patch.model !== void 0) merged.model = patch.model;
|
|
6771
|
+
if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
|
|
6772
|
+
if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
|
|
6773
|
+
if (patch.label !== void 0) merged.label = patch.label;
|
|
6774
|
+
if (patch.local !== void 0) merged.local = patch.local;
|
|
6775
|
+
if (patch.runner !== void 0) merged.runner = patch.runner;
|
|
6776
|
+
return { ...providers, [name]: merged };
|
|
6711
6777
|
}
|
|
6712
|
-
function
|
|
6778
|
+
function removeProviderRecord(providers, name) {
|
|
6779
|
+
if (!(name in providers)) return { providers, removed: false };
|
|
6780
|
+
const next = { ...providers };
|
|
6781
|
+
delete next[name];
|
|
6782
|
+
return { providers: next, removed: true };
|
|
6783
|
+
}
|
|
6784
|
+
function rolesReferencing(roles, name) {
|
|
6785
|
+
return VALID_ROLES.filter((role) => {
|
|
6786
|
+
const binding = roles[role];
|
|
6787
|
+
return binding?.primary === name || binding?.fallback === name;
|
|
6788
|
+
});
|
|
6789
|
+
}
|
|
6790
|
+
function bindRoleProviders(roles, role, primary, fallback) {
|
|
6791
|
+
const binding = { primary };
|
|
6792
|
+
if (fallback) binding.fallback = fallback;
|
|
6793
|
+
return { ...roles, [role]: binding };
|
|
6794
|
+
}
|
|
6795
|
+
function maskSecret(key) {
|
|
6796
|
+
return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
|
|
6797
|
+
}
|
|
6798
|
+
function buildProviderListing(providers, hasKey) {
|
|
6799
|
+
return Object.entries(providers).map(([name, rec]) => {
|
|
6800
|
+
const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
|
|
6801
|
+
let keyState;
|
|
6802
|
+
if (!rec.apiKeyRef) keyState = "none";
|
|
6803
|
+
else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
|
|
6804
|
+
const row = {
|
|
6805
|
+
name,
|
|
6806
|
+
url: rec.url,
|
|
6807
|
+
model: rec.model,
|
|
6808
|
+
apiFlavor,
|
|
6809
|
+
apiKeyRef: rec.apiKeyRef,
|
|
6810
|
+
keyState
|
|
6811
|
+
};
|
|
6812
|
+
if (rec.label !== void 0) row.label = rec.label;
|
|
6813
|
+
if (rec.local !== void 0) row.local = rec.local;
|
|
6814
|
+
if (rec.runner !== void 0) row.runner = rec.runner;
|
|
6815
|
+
return row;
|
|
6816
|
+
});
|
|
6817
|
+
}
|
|
6818
|
+
function findOrphanKeyRefs(storedRefs, providers) {
|
|
6819
|
+
const used = new Set(
|
|
6820
|
+
Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
|
|
6821
|
+
);
|
|
6822
|
+
return storedRefs.filter((ref) => !used.has(ref));
|
|
6823
|
+
}
|
|
6824
|
+
async function readJson(db, key, fallback) {
|
|
6825
|
+
const raw = await getSetting(db, key);
|
|
6826
|
+
if (!raw) return fallback;
|
|
6713
6827
|
try {
|
|
6714
|
-
|
|
6715
|
-
return true;
|
|
6828
|
+
return JSON.parse(raw);
|
|
6716
6829
|
} catch {
|
|
6717
|
-
return
|
|
6830
|
+
return fallback;
|
|
6718
6831
|
}
|
|
6719
6832
|
}
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6833
|
+
var readProviders = (db) => readJson(db, "llm.providers", {});
|
|
6834
|
+
var readRoles = (db) => readJson(db, "llm.roles", {});
|
|
6835
|
+
async function readScopedProviders(db, machine) {
|
|
6836
|
+
if (machine) return getMachineAiConfig().providers ?? {};
|
|
6837
|
+
if (!db)
|
|
6838
|
+
throw new Error("Database is required for shared provider settings.");
|
|
6839
|
+
return readProviders(db);
|
|
6723
6840
|
}
|
|
6724
|
-
function
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6841
|
+
async function readScopedRoles(db, machine) {
|
|
6842
|
+
if (machine) return getMachineAiConfig().roles ?? {};
|
|
6843
|
+
if (!db)
|
|
6844
|
+
throw new Error("Database is required for shared provider settings.");
|
|
6845
|
+
return readRoles(db);
|
|
6846
|
+
}
|
|
6847
|
+
async function writeScopedProviders(db, machine, p) {
|
|
6848
|
+
if (machine) {
|
|
6849
|
+
const config = getMachineAiConfig();
|
|
6850
|
+
saveMachineAiConfig({ ...config, providers: p });
|
|
6851
|
+
return;
|
|
6729
6852
|
}
|
|
6853
|
+
if (!db)
|
|
6854
|
+
throw new Error("Database is required for shared provider settings.");
|
|
6855
|
+
await setSetting(db, "llm.providers", JSON.stringify(p));
|
|
6730
6856
|
}
|
|
6731
|
-
function
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
return
|
|
6857
|
+
async function writeScopedRoles(db, machine, r) {
|
|
6858
|
+
if (machine) {
|
|
6859
|
+
const config = getMachineAiConfig();
|
|
6860
|
+
saveMachineAiConfig({ ...config, roles: r });
|
|
6861
|
+
return;
|
|
6736
6862
|
}
|
|
6863
|
+
if (!db)
|
|
6864
|
+
throw new Error("Database is required for shared provider settings.");
|
|
6865
|
+
await setSetting(db, "llm.roles", JSON.stringify(r));
|
|
6737
6866
|
}
|
|
6738
|
-
function
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
if (entries.length !== 1 || entries[0] !== "SKILL.md") return false;
|
|
6743
|
-
return /^name:\s*zam\s*$/m.test(
|
|
6744
|
-
readFileSync10(join15(destinationDir, "SKILL.md"), "utf8")
|
|
6745
|
-
);
|
|
6746
|
-
} catch {
|
|
6747
|
-
return false;
|
|
6867
|
+
async function withProviderScope(machine, action) {
|
|
6868
|
+
if (machine) {
|
|
6869
|
+
await action(void 0);
|
|
6870
|
+
return;
|
|
6748
6871
|
}
|
|
6872
|
+
await withDb(action);
|
|
6749
6873
|
}
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
const
|
|
6758
|
-
const
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6874
|
+
var providerCommand = new Command2("provider").description(
|
|
6875
|
+
"Configure role-based AI providers (url/model/flavor/key, per role)"
|
|
6876
|
+
);
|
|
6877
|
+
providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
|
|
6878
|
+
const machine = Boolean(opts.machine);
|
|
6879
|
+
await withProviderScope(machine, async (db) => {
|
|
6880
|
+
const providers = await readScopedProviders(db, machine);
|
|
6881
|
+
const roles = await readScopedRoles(db, machine);
|
|
6882
|
+
const rows = buildProviderListing(
|
|
6883
|
+
providers,
|
|
6884
|
+
(ref) => getProviderApiKey(ref) !== null
|
|
6885
|
+
);
|
|
6886
|
+
const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
|
|
6887
|
+
if (opts.json) {
|
|
6888
|
+
console.log(
|
|
6889
|
+
JSON.stringify(
|
|
6890
|
+
{
|
|
6891
|
+
scope: machine ? "machine" : "shared",
|
|
6892
|
+
providers: rows,
|
|
6893
|
+
roles,
|
|
6894
|
+
orphans
|
|
6895
|
+
},
|
|
6896
|
+
null,
|
|
6897
|
+
2
|
|
6898
|
+
)
|
|
6899
|
+
);
|
|
6900
|
+
return;
|
|
6901
|
+
}
|
|
6902
|
+
console.log(
|
|
6903
|
+
`Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
|
|
6904
|
+
`
|
|
6905
|
+
);
|
|
6906
|
+
if (rows.length === 0) {
|
|
6907
|
+
console.log(
|
|
6908
|
+
" (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
|
|
6909
|
+
);
|
|
6910
|
+
} else {
|
|
6911
|
+
for (const row of rows) {
|
|
6912
|
+
const key = row.keyState === "set" ? "\x1B[32mkey \u2713\x1B[0m" : row.keyState === "missing" ? `\x1B[31mkey \u2717 (set: zam provider set-key ${row.apiKeyRef})\x1B[0m` : "\x1B[90mno key\x1B[0m";
|
|
6913
|
+
console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
|
|
6914
|
+
console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
|
|
6915
|
+
console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
|
|
6916
|
+
console.log(` flavor: ${row.apiFlavor}`);
|
|
6917
|
+
if (row.label) console.log(` label: ${row.label}`);
|
|
6918
|
+
if (row.local !== void 0) {
|
|
6919
|
+
console.log(` local: ${row.local ? "yes" : "no"}`);
|
|
6920
|
+
}
|
|
6921
|
+
if (row.runner) console.log(` runner: ${row.runner}`);
|
|
6922
|
+
if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
|
|
6762
6923
|
}
|
|
6763
|
-
results.push({
|
|
6764
|
-
source: sourceDir,
|
|
6765
|
-
destination: destinationDir,
|
|
6766
|
-
action: "skipped",
|
|
6767
|
-
reason: "source-not-found"
|
|
6768
|
-
});
|
|
6769
|
-
continue;
|
|
6770
6924
|
}
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6925
|
+
console.log("\nRoles (llm.roles):\n");
|
|
6926
|
+
for (const role of VALID_ROLES) {
|
|
6927
|
+
const binding = roles[role];
|
|
6928
|
+
if (!binding?.primary) {
|
|
6929
|
+
console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
|
|
6930
|
+
} else {
|
|
6931
|
+
const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
|
|
6932
|
+
console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
|
|
6933
|
+
}
|
|
6780
6934
|
}
|
|
6781
|
-
if (
|
|
6782
|
-
log(
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
action: "skipped",
|
|
6787
|
-
reason: "already-linked"
|
|
6788
|
-
});
|
|
6789
|
-
continue;
|
|
6935
|
+
if (orphans.length > 0) {
|
|
6936
|
+
console.log(
|
|
6937
|
+
`
|
|
6938
|
+
\x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
|
|
6939
|
+
);
|
|
6790
6940
|
}
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6941
|
+
});
|
|
6942
|
+
});
|
|
6943
|
+
providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--label <label>", "Human-readable provider label").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option("--local", "Mark this provider as a local/on-device endpoint").option(
|
|
6944
|
+
"--runner <runner>",
|
|
6945
|
+
"Local runner hint (flm, foundry-local, ollama, ...)"
|
|
6946
|
+
).option(
|
|
6947
|
+
"--flavor <flavor>",
|
|
6948
|
+
`Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
|
|
6949
|
+
).option(
|
|
6950
|
+
"--key-ref <ref>",
|
|
6951
|
+
"Credential reference for the API key (default: <name> when --key is given)"
|
|
6952
|
+
).option(
|
|
6953
|
+
"--key <value>",
|
|
6954
|
+
"Store this API key now (prefer `set-key` to keep it out of shell history)"
|
|
6955
|
+
).option("--machine", "Store this provider in ~/.zam/config.json").action(async (name, opts) => {
|
|
6956
|
+
let apiFlavor;
|
|
6957
|
+
if (opts.flavor) {
|
|
6958
|
+
if (!VALID_API_FLAVORS.includes(opts.flavor)) {
|
|
6959
|
+
console.error(
|
|
6960
|
+
`Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
|
|
6961
|
+
);
|
|
6962
|
+
process.exit(1);
|
|
6806
6963
|
}
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6964
|
+
apiFlavor = opts.flavor;
|
|
6965
|
+
}
|
|
6966
|
+
const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
|
|
6967
|
+
const machine = Boolean(opts.machine);
|
|
6968
|
+
await withProviderScope(machine, async (db) => {
|
|
6969
|
+
const providers = await readScopedProviders(db, machine);
|
|
6970
|
+
const next = upsertProviderRecord(providers, name, {
|
|
6971
|
+
label: opts.label,
|
|
6972
|
+
url: opts.url,
|
|
6973
|
+
model: opts.model,
|
|
6974
|
+
apiFlavor,
|
|
6975
|
+
apiKeyRef,
|
|
6976
|
+
local: opts.local ? true : void 0,
|
|
6977
|
+
runner: opts.runner
|
|
6978
|
+
});
|
|
6979
|
+
await writeScopedProviders(db, machine, next);
|
|
6980
|
+
if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
|
|
6981
|
+
const rec = next[name];
|
|
6982
|
+
console.log(
|
|
6983
|
+
`Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
|
|
6984
|
+
);
|
|
6985
|
+
console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
|
|
6986
|
+
console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
|
|
6987
|
+
if (rec.label) console.log(` label: ${rec.label}`);
|
|
6988
|
+
if (rec.local !== void 0) {
|
|
6989
|
+
console.log(` local: ${rec.local ? "yes" : "no"}`);
|
|
6990
|
+
}
|
|
6991
|
+
if (rec.runner) console.log(` runner: ${rec.runner}`);
|
|
6992
|
+
console.log(
|
|
6993
|
+
` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
|
|
6994
|
+
);
|
|
6995
|
+
console.log(` key-ref: ${rec.apiKeyRef ?? "(none \u2014 uses default key)"}`);
|
|
6996
|
+
if (opts.key) {
|
|
6997
|
+
console.log(` key: stored (${maskSecret(opts.key)})`);
|
|
6998
|
+
} else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
|
|
6999
|
+
console.log(
|
|
7000
|
+
`
|
|
7001
|
+
\u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
|
|
6819
7002
|
);
|
|
6820
|
-
log(` ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
|
|
6821
7003
|
}
|
|
6822
|
-
|
|
7004
|
+
if (!rec.url) {
|
|
7005
|
+
console.log(
|
|
7006
|
+
"\n \u26A0 No --url set; this provider inherits the base llm.url."
|
|
7007
|
+
);
|
|
7008
|
+
}
|
|
7009
|
+
console.log(
|
|
7010
|
+
`
|
|
7011
|
+
Bind it to a role: zam provider use recall --primary ${name}`
|
|
7012
|
+
);
|
|
7013
|
+
});
|
|
7014
|
+
});
|
|
7015
|
+
providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").option("--machine", "Remove from ~/.zam/config.json instead of the DB").action(async (name, opts) => {
|
|
7016
|
+
const machine = Boolean(opts.machine);
|
|
7017
|
+
await withProviderScope(machine, async (db) => {
|
|
7018
|
+
const providers = await readScopedProviders(db, machine);
|
|
7019
|
+
const { providers: next, removed } = removeProviderRecord(
|
|
7020
|
+
providers,
|
|
7021
|
+
name
|
|
7022
|
+
);
|
|
7023
|
+
if (!removed) {
|
|
7024
|
+
console.log(`No such provider: ${name}`);
|
|
7025
|
+
return;
|
|
7026
|
+
}
|
|
7027
|
+
await writeScopedProviders(db, machine, next);
|
|
7028
|
+
console.log(
|
|
7029
|
+
`Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
|
|
7030
|
+
);
|
|
7031
|
+
const referencing = rolesReferencing(
|
|
7032
|
+
await readScopedRoles(db, machine),
|
|
7033
|
+
name
|
|
7034
|
+
);
|
|
7035
|
+
if (referencing.length > 0) {
|
|
7036
|
+
console.log(
|
|
7037
|
+
` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
|
|
7038
|
+
);
|
|
7039
|
+
}
|
|
7040
|
+
});
|
|
7041
|
+
});
|
|
7042
|
+
providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").option("--machine", "Bind the role in ~/.zam/config.json").action(async (role, opts) => {
|
|
7043
|
+
if (!VALID_ROLES.includes(role)) {
|
|
7044
|
+
console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
|
|
7045
|
+
process.exit(1);
|
|
6823
7046
|
}
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
switch (target.kind) {
|
|
6828
|
-
case "local":
|
|
6829
|
-
return `ZAM database at ${target.location} (local SQLite)`;
|
|
6830
|
-
case "turso-remote":
|
|
6831
|
-
return `ZAM database via Turso remote at ${target.location}`;
|
|
6832
|
-
case "turso-native":
|
|
6833
|
-
return `ZAM database via Turso native driver at ${target.location}`;
|
|
6834
|
-
case "turso-replica":
|
|
6835
|
-
return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
|
|
7047
|
+
if (!opts.primary) {
|
|
7048
|
+
console.error("--primary is required.");
|
|
7049
|
+
process.exit(1);
|
|
6836
7050
|
}
|
|
6837
|
-
|
|
6838
|
-
async
|
|
6839
|
-
|
|
7051
|
+
const machine = Boolean(opts.machine);
|
|
7052
|
+
await withProviderScope(machine, async (db) => {
|
|
7053
|
+
const providers = await readScopedProviders(db, machine);
|
|
7054
|
+
await writeScopedRoles(
|
|
7055
|
+
db,
|
|
7056
|
+
machine,
|
|
7057
|
+
bindRoleProviders(
|
|
7058
|
+
await readScopedRoles(db, machine),
|
|
7059
|
+
role,
|
|
7060
|
+
opts.primary,
|
|
7061
|
+
opts.fallback
|
|
7062
|
+
)
|
|
7063
|
+
);
|
|
7064
|
+
const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
|
|
7065
|
+
console.log(
|
|
7066
|
+
`Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
|
|
7067
|
+
);
|
|
7068
|
+
for (const [label, ref] of [
|
|
7069
|
+
["primary", opts.primary],
|
|
7070
|
+
["fallback", opts.fallback]
|
|
7071
|
+
]) {
|
|
7072
|
+
if (ref && !(ref in providers)) {
|
|
7073
|
+
console.log(
|
|
7074
|
+
` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
|
|
7075
|
+
);
|
|
7076
|
+
}
|
|
7077
|
+
}
|
|
7078
|
+
});
|
|
7079
|
+
});
|
|
7080
|
+
providerCommand.command("set-key <ref>").description(
|
|
7081
|
+
"Store an API key for a provider reference (in credentials.json)"
|
|
7082
|
+
).option(
|
|
7083
|
+
"--key <value>",
|
|
7084
|
+
"The API key (omit to enter it interactively, hidden)"
|
|
7085
|
+
).action(async (ref, opts) => {
|
|
6840
7086
|
try {
|
|
6841
|
-
const
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
} catch (err) {
|
|
6846
|
-
const msg = err.message;
|
|
6847
|
-
if (!msg.includes("already")) {
|
|
6848
|
-
console.warn(` warn database init: ${msg}`);
|
|
6849
|
-
} else {
|
|
6850
|
-
console.log(` skip database already initialized`);
|
|
7087
|
+
const key = opts.key ?? await password({ message: `API key for "${ref}":` });
|
|
7088
|
+
if (!key || key.trim().length === 0) {
|
|
7089
|
+
console.error("No key provided.");
|
|
7090
|
+
process.exit(1);
|
|
6851
7091
|
}
|
|
6852
|
-
|
|
6853
|
-
}
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
${blockBody.trim()}
|
|
6859
|
-
${ZAM_BLOCK_END}`;
|
|
6860
|
-
if (!existsSync15(dest)) {
|
|
6861
|
-
if (!dryRun) {
|
|
6862
|
-
mkdirSync8(dirname5(dest), { recursive: true });
|
|
6863
|
-
writeFileSync7(dest, `${block}
|
|
6864
|
-
`, "utf8");
|
|
7092
|
+
setProviderApiKey(ref, key.trim());
|
|
7093
|
+
console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
|
|
7094
|
+
} catch (err) {
|
|
7095
|
+
if (err.name === "ExitPromptError") {
|
|
7096
|
+
console.log("\nCancelled.");
|
|
7097
|
+
process.exit(0);
|
|
6865
7098
|
}
|
|
6866
|
-
|
|
7099
|
+
console.error("Error:", err.message);
|
|
7100
|
+
process.exit(1);
|
|
6867
7101
|
}
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
7102
|
+
});
|
|
7103
|
+
providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
|
|
7104
|
+
clearProviderApiKey(ref);
|
|
7105
|
+
console.log(`Cleared API key for "${ref}".`);
|
|
7106
|
+
});
|
|
6873
7107
|
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
7108
|
+
// src/cli/commands/resolve-user.ts
|
|
7109
|
+
async function ensureDefaultUser(db, preferredUserId) {
|
|
7110
|
+
const stored = await getSetting(db, "user.id");
|
|
7111
|
+
if (stored) return stored;
|
|
7112
|
+
const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
|
|
7113
|
+
await setSetting(db, "user.id", userId);
|
|
7114
|
+
return userId;
|
|
6878
7115
|
}
|
|
6879
|
-
function
|
|
6880
|
-
if (
|
|
6881
|
-
|
|
7116
|
+
async function resolveUser(opts, db, resolveOpts) {
|
|
7117
|
+
if (opts.user) return opts.user;
|
|
7118
|
+
const stored = await getSetting(db, "user.id");
|
|
7119
|
+
if (stored) return stored;
|
|
7120
|
+
const message = "No user specified. Set a default with: zam whoami --set <id>";
|
|
7121
|
+
if (resolveOpts?.json) {
|
|
7122
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
6882
7123
|
} else {
|
|
6883
|
-
console.
|
|
7124
|
+
console.error(message);
|
|
6884
7125
|
}
|
|
7126
|
+
process.exit(1);
|
|
6885
7127
|
}
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
7128
|
+
|
|
7129
|
+
// src/cli/commands/setup.ts
|
|
7130
|
+
import {
|
|
7131
|
+
existsSync as existsSync15,
|
|
7132
|
+
lstatSync,
|
|
7133
|
+
mkdirSync as mkdirSync8,
|
|
7134
|
+
readdirSync as readdirSync2,
|
|
7135
|
+
readFileSync as readFileSync10,
|
|
7136
|
+
realpathSync,
|
|
7137
|
+
rmSync as rmSync2,
|
|
7138
|
+
symlinkSync,
|
|
7139
|
+
writeFileSync as writeFileSync7
|
|
7140
|
+
} from "fs";
|
|
7141
|
+
import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
|
|
7142
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7143
|
+
import { Command as Command3 } from "commander";
|
|
7144
|
+
var packageRoot = [
|
|
7145
|
+
fileURLToPath2(new URL("../..", import.meta.url)),
|
|
7146
|
+
fileURLToPath2(new URL("../../..", import.meta.url))
|
|
7147
|
+
].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
|
|
7148
|
+
var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
|
|
7149
|
+
var SKILL_PAIRS = [
|
|
7150
|
+
{
|
|
7151
|
+
from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
|
|
7152
|
+
to: join15(".claude", "skills", "zam", "SKILL.md"),
|
|
7153
|
+
agents: ["claude", "copilot"]
|
|
7154
|
+
},
|
|
7155
|
+
{
|
|
7156
|
+
from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
|
|
7157
|
+
to: join15(".agent", "skills", "zam", "SKILL.md"),
|
|
7158
|
+
agents: ["agent"]
|
|
7159
|
+
},
|
|
7160
|
+
{
|
|
7161
|
+
from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
|
|
7162
|
+
to: join15(".agents", "skills", "zam", "SKILL.md"),
|
|
7163
|
+
agents: ["codex"]
|
|
7164
|
+
}
|
|
7165
|
+
];
|
|
7166
|
+
function parseSetupAgents(value) {
|
|
7167
|
+
if (!value || value.trim().toLowerCase() === "all") {
|
|
7168
|
+
return new Set(ALL_SETUP_AGENTS);
|
|
7169
|
+
}
|
|
7170
|
+
const aliases = {
|
|
7171
|
+
claude: ["claude"],
|
|
7172
|
+
copilot: ["copilot"],
|
|
7173
|
+
codex: ["codex"],
|
|
7174
|
+
agent: ["agent"],
|
|
7175
|
+
opencode: ["agent"]
|
|
7176
|
+
};
|
|
7177
|
+
const selected = /* @__PURE__ */ new Set();
|
|
7178
|
+
for (const raw of value.split(",")) {
|
|
7179
|
+
const key = raw.trim().toLowerCase();
|
|
7180
|
+
const mapped = aliases[key];
|
|
7181
|
+
if (!mapped) {
|
|
7182
|
+
throw new Error(
|
|
7183
|
+
`Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
|
|
7184
|
+
);
|
|
7185
|
+
}
|
|
7186
|
+
for (const item of mapped) selected.add(item);
|
|
7187
|
+
}
|
|
7188
|
+
return selected;
|
|
7189
|
+
}
|
|
7190
|
+
function pathExists(path) {
|
|
7191
|
+
try {
|
|
7192
|
+
lstatSync(path);
|
|
7193
|
+
return true;
|
|
7194
|
+
} catch {
|
|
7195
|
+
return false;
|
|
7196
|
+
}
|
|
7197
|
+
}
|
|
7198
|
+
function comparableRealPath(path) {
|
|
7199
|
+
const real = realpathSync(path);
|
|
7200
|
+
return process.platform === "win32" ? real.toLowerCase() : real;
|
|
7201
|
+
}
|
|
7202
|
+
function pathsResolveToSameDirectory(sourceDir, destinationDir) {
|
|
7203
|
+
try {
|
|
7204
|
+
return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
|
|
7205
|
+
} catch {
|
|
7206
|
+
return false;
|
|
7207
|
+
}
|
|
7208
|
+
}
|
|
7209
|
+
function linkPointsTo(sourceDir, destinationDir) {
|
|
7210
|
+
try {
|
|
7211
|
+
return lstatSync(destinationDir).isSymbolicLink() && pathsResolveToSameDirectory(sourceDir, destinationDir);
|
|
7212
|
+
} catch {
|
|
7213
|
+
return false;
|
|
7214
|
+
}
|
|
7215
|
+
}
|
|
7216
|
+
function isReplaceableCopiedSkill(destinationDir) {
|
|
7217
|
+
try {
|
|
7218
|
+
if (!lstatSync(destinationDir).isDirectory()) return false;
|
|
7219
|
+
const entries = readdirSync2(destinationDir);
|
|
7220
|
+
if (entries.length !== 1 || entries[0] !== "SKILL.md") return false;
|
|
7221
|
+
return /^name:\s*zam\s*$/m.test(
|
|
7222
|
+
readFileSync10(join15(destinationDir, "SKILL.md"), "utf8")
|
|
7223
|
+
);
|
|
7224
|
+
} catch {
|
|
7225
|
+
return false;
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
|
|
7229
|
+
const results = [];
|
|
7230
|
+
const log = (message) => {
|
|
7231
|
+
if (!opts.quiet) console.log(message);
|
|
7232
|
+
};
|
|
7233
|
+
for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
|
|
7234
|
+
if (!pairAgents.some((agent) => agents.has(agent))) continue;
|
|
7235
|
+
const sourceDir = dirname5(from);
|
|
7236
|
+
const destinationDir = dirname5(join15(cwd, to));
|
|
7237
|
+
if (!existsSync15(sourceDir)) {
|
|
7238
|
+
if (!opts.quiet) {
|
|
7239
|
+
console.warn(` warn source not found, skipping: ${sourceDir}`);
|
|
7240
|
+
}
|
|
7241
|
+
results.push({
|
|
7242
|
+
source: sourceDir,
|
|
7243
|
+
destination: destinationDir,
|
|
7244
|
+
action: "skipped",
|
|
7245
|
+
reason: "source-not-found"
|
|
7246
|
+
});
|
|
7247
|
+
continue;
|
|
7248
|
+
}
|
|
7249
|
+
if (pathsResolveToSameDirectory(sourceDir, destinationDir) && !lstatSync(destinationDir).isSymbolicLink()) {
|
|
7250
|
+
log(` skip ${dirname5(to)} (package source)`);
|
|
7251
|
+
results.push({
|
|
7252
|
+
source: sourceDir,
|
|
7253
|
+
destination: destinationDir,
|
|
7254
|
+
action: "skipped",
|
|
7255
|
+
reason: "source-directory"
|
|
7256
|
+
});
|
|
7257
|
+
continue;
|
|
7258
|
+
}
|
|
7259
|
+
if (linkPointsTo(sourceDir, destinationDir)) {
|
|
7260
|
+
log(` skip ${dirname5(to)} (already linked)`);
|
|
7261
|
+
results.push({
|
|
7262
|
+
source: sourceDir,
|
|
7263
|
+
destination: destinationDir,
|
|
7264
|
+
action: "skipped",
|
|
7265
|
+
reason: "already-linked"
|
|
7266
|
+
});
|
|
7267
|
+
continue;
|
|
7268
|
+
}
|
|
7269
|
+
const destinationExists = pathExists(destinationDir);
|
|
7270
|
+
const replaceExisting = destinationExists && (Boolean(opts.force) || isReplaceableCopiedSkill(destinationDir));
|
|
7271
|
+
if (destinationExists && !replaceExisting) {
|
|
7272
|
+
if (!opts.quiet) {
|
|
7273
|
+
console.warn(
|
|
7274
|
+
` warn ${dirname5(to)} already exists and is not managed by ZAM; use --force to replace it`
|
|
7275
|
+
);
|
|
7276
|
+
}
|
|
7277
|
+
results.push({
|
|
7278
|
+
source: sourceDir,
|
|
7279
|
+
destination: destinationDir,
|
|
7280
|
+
action: "skipped",
|
|
7281
|
+
reason: "unmanaged-destination"
|
|
7282
|
+
});
|
|
7283
|
+
continue;
|
|
7284
|
+
}
|
|
7285
|
+
const action = destinationExists ? "relinked" : "linked";
|
|
7286
|
+
if (opts.dryRun) {
|
|
7287
|
+
log(` would ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
|
|
7288
|
+
} else {
|
|
7289
|
+
if (destinationExists) {
|
|
7290
|
+
rmSync2(destinationDir, { recursive: true, force: true });
|
|
7291
|
+
}
|
|
7292
|
+
mkdirSync8(dirname5(destinationDir), { recursive: true });
|
|
7293
|
+
symlinkSync(
|
|
7294
|
+
sourceDir,
|
|
7295
|
+
destinationDir,
|
|
7296
|
+
process.platform === "win32" ? "junction" : "dir"
|
|
7297
|
+
);
|
|
7298
|
+
log(` ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
|
|
7299
|
+
}
|
|
7300
|
+
results.push({ source: sourceDir, destination: destinationDir, action });
|
|
7301
|
+
}
|
|
7302
|
+
return results;
|
|
7303
|
+
}
|
|
7304
|
+
function formatDatabaseInitTarget(target) {
|
|
7305
|
+
switch (target.kind) {
|
|
7306
|
+
case "local":
|
|
7307
|
+
return `ZAM database at ${target.location} (local SQLite)`;
|
|
7308
|
+
case "turso-remote":
|
|
7309
|
+
return `ZAM database via Turso remote at ${target.location}`;
|
|
7310
|
+
case "turso-native":
|
|
7311
|
+
return `ZAM database via Turso native driver at ${target.location}`;
|
|
7312
|
+
case "turso-replica":
|
|
7313
|
+
return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
|
|
7314
|
+
}
|
|
7315
|
+
}
|
|
7316
|
+
async function initDatabase(skipInit) {
|
|
7317
|
+
if (skipInit) return;
|
|
7318
|
+
try {
|
|
7319
|
+
const target = getDatabaseTargetInfo();
|
|
7320
|
+
const db = await openDatabaseWithSync({ initialize: true });
|
|
7321
|
+
await db.close();
|
|
7322
|
+
console.log(` init ${formatDatabaseInitTarget(target)}`);
|
|
7323
|
+
} catch (err) {
|
|
7324
|
+
const msg = err.message;
|
|
7325
|
+
if (!msg.includes("already")) {
|
|
7326
|
+
console.warn(` warn database init: ${msg}`);
|
|
7327
|
+
} else {
|
|
7328
|
+
console.log(` skip database already initialized`);
|
|
7329
|
+
}
|
|
7330
|
+
}
|
|
7331
|
+
}
|
|
7332
|
+
var ZAM_BLOCK_START = "<!-- ZAM:START -->";
|
|
7333
|
+
var ZAM_BLOCK_END = "<!-- ZAM:END -->";
|
|
7334
|
+
function upsertMarkedBlock(dest, blockBody, dryRun) {
|
|
7335
|
+
const block = `${ZAM_BLOCK_START}
|
|
7336
|
+
${blockBody.trim()}
|
|
7337
|
+
${ZAM_BLOCK_END}`;
|
|
7338
|
+
if (!existsSync15(dest)) {
|
|
7339
|
+
if (!dryRun) {
|
|
7340
|
+
mkdirSync8(dirname5(dest), { recursive: true });
|
|
7341
|
+
writeFileSync7(dest, `${block}
|
|
7342
|
+
`, "utf8");
|
|
7343
|
+
}
|
|
7344
|
+
return "write";
|
|
7345
|
+
}
|
|
7346
|
+
const existing = readFileSync10(dest, "utf8");
|
|
7347
|
+
if (existing.includes(block)) return "skip";
|
|
7348
|
+
const start = existing.indexOf(ZAM_BLOCK_START);
|
|
7349
|
+
const end = existing.indexOf(ZAM_BLOCK_END);
|
|
7350
|
+
const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
|
|
7351
|
+
|
|
7352
|
+
${block}
|
|
7353
|
+
`;
|
|
7354
|
+
if (!dryRun) writeFileSync7(dest, next, "utf8");
|
|
7355
|
+
return start >= 0 && end > start ? "update" : "write";
|
|
7356
|
+
}
|
|
7357
|
+
function logInstructionAction(action, label, dryRun) {
|
|
7358
|
+
if (action === "skip") {
|
|
7359
|
+
console.log(` skip ${label} (ZAM block already present)`);
|
|
7360
|
+
} else {
|
|
7361
|
+
console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
|
|
7365
|
+
if (skipClaudeMd) return;
|
|
7366
|
+
const dest = join15(cwd, "CLAUDE.md");
|
|
7367
|
+
if (existsSync15(dest)) {
|
|
7368
|
+
if (!opts.updateExisting) {
|
|
7369
|
+
console.log(` skip CLAUDE.md (already present)`);
|
|
7370
|
+
return;
|
|
7371
|
+
}
|
|
7372
|
+
const action = upsertMarkedBlock(
|
|
6895
7373
|
dest,
|
|
6896
7374
|
`## ZAM learning sessions
|
|
6897
7375
|
|
|
@@ -7008,7 +7486,7 @@ ZAM is available in this repository through \`.claude/skills/zam/\`. Use the \`z
|
|
|
7008
7486
|
Boolean(opts.dryRun)
|
|
7009
7487
|
);
|
|
7010
7488
|
}
|
|
7011
|
-
var setupCommand = new
|
|
7489
|
+
var setupCommand = new Command3("setup").description(
|
|
7012
7490
|
"Link ZAM skill directories into this workspace and initialize the database"
|
|
7013
7491
|
).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
|
|
7014
7492
|
"--agents <list>",
|
|
@@ -7061,33 +7539,13 @@ var setupCommand = new Command2("setup").description(
|
|
|
7061
7539
|
}
|
|
7062
7540
|
);
|
|
7063
7541
|
|
|
7064
|
-
// src/cli/commands/shared/db.ts
|
|
7065
|
-
function defaultErrorHandler(message) {
|
|
7066
|
-
console.error("Error:", message);
|
|
7067
|
-
process.exit(1);
|
|
7068
|
-
}
|
|
7069
|
-
async function withDb(fn, onError = defaultErrorHandler) {
|
|
7070
|
-
let db;
|
|
7071
|
-
try {
|
|
7072
|
-
db = await openDatabase();
|
|
7073
|
-
await fn(db);
|
|
7074
|
-
} catch (err) {
|
|
7075
|
-
onError(err.message);
|
|
7076
|
-
} finally {
|
|
7077
|
-
await db?.close();
|
|
7078
|
-
}
|
|
7079
|
-
}
|
|
7080
|
-
function jsonOut(data) {
|
|
7081
|
-
console.log(JSON.stringify(data, null, 2));
|
|
7082
|
-
}
|
|
7083
|
-
|
|
7084
7542
|
// src/cli/commands/workspace.ts
|
|
7085
7543
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
7086
7544
|
import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
7087
7545
|
import { homedir as homedir9 } from "os";
|
|
7088
7546
|
import { join as join16, resolve as resolve4 } from "path";
|
|
7089
7547
|
import { confirm, input } from "@inquirer/prompts";
|
|
7090
|
-
import { Command as
|
|
7548
|
+
import { Command as Command4 } from "commander";
|
|
7091
7549
|
function runGit(cwd, args) {
|
|
7092
7550
|
try {
|
|
7093
7551
|
return execFileSync3("git", args, {
|
|
@@ -7105,7 +7563,7 @@ function ghRepoCreateArgs(repoName, repoVisibility) {
|
|
|
7105
7563
|
function gitRemoteArgs(githubUrl, hasOrigin) {
|
|
7106
7564
|
return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
|
|
7107
7565
|
}
|
|
7108
|
-
var workspaceCommand = new
|
|
7566
|
+
var workspaceCommand = new Command4("workspace").description(
|
|
7109
7567
|
"Manage your ZAM learning workspace"
|
|
7110
7568
|
);
|
|
7111
7569
|
var WORKSPACE_KINDS = [
|
|
@@ -7461,6 +7919,12 @@ function parseNonNegativeIntegerOption(name, value) {
|
|
|
7461
7919
|
}
|
|
7462
7920
|
return parsed;
|
|
7463
7921
|
}
|
|
7922
|
+
function parseProviderScope(scope) {
|
|
7923
|
+
const value = scope ?? "machine";
|
|
7924
|
+
if (value === "machine") return true;
|
|
7925
|
+
if (value === "shared") return false;
|
|
7926
|
+
jsonError(`Invalid --scope: ${value}. Use machine or shared.`);
|
|
7927
|
+
}
|
|
7464
7928
|
async function withDb2(fn) {
|
|
7465
7929
|
await withDb(fn, jsonError);
|
|
7466
7930
|
}
|
|
@@ -7498,7 +7962,7 @@ function parseTokenUpdates(opts) {
|
|
|
7498
7962
|
}
|
|
7499
7963
|
return updates;
|
|
7500
7964
|
}
|
|
7501
|
-
var bridgeCommand = new
|
|
7965
|
+
var bridgeCommand = new Command5("bridge").description(
|
|
7502
7966
|
"Machine-readable JSON protocol for AI integration"
|
|
7503
7967
|
);
|
|
7504
7968
|
bridgeCommand.command("check-due").description("Check due cards for a user (JSON)").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").action(async (opts) => {
|
|
@@ -7768,6 +8232,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
7768
8232
|
const item = queue.items[0];
|
|
7769
8233
|
const isLlmEnabled = await getSetting(db, "llm.enabled") === "true";
|
|
7770
8234
|
let resolvedQuestion = item.question;
|
|
8235
|
+
let questionSource = "original";
|
|
8236
|
+
let questionModel;
|
|
7771
8237
|
if (isLlmEnabled) {
|
|
7772
8238
|
try {
|
|
7773
8239
|
const healed = await ensureHighQualityQuestion(db, {
|
|
@@ -7780,7 +8246,9 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
7780
8246
|
question: item.question
|
|
7781
8247
|
});
|
|
7782
8248
|
if (healed) {
|
|
7783
|
-
resolvedQuestion = healed;
|
|
8249
|
+
resolvedQuestion = healed.question;
|
|
8250
|
+
questionSource = healed.source;
|
|
8251
|
+
questionModel = healed.model;
|
|
7784
8252
|
}
|
|
7785
8253
|
} catch {
|
|
7786
8254
|
}
|
|
@@ -7809,6 +8277,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
7809
8277
|
hasReview: true,
|
|
7810
8278
|
card: item,
|
|
7811
8279
|
prompt,
|
|
8280
|
+
questionSource,
|
|
8281
|
+
questionModel: questionModel ?? null,
|
|
7812
8282
|
resolvedContext,
|
|
7813
8283
|
queueSize: fullQueue.items.length
|
|
7814
8284
|
});
|
|
@@ -8908,6 +9378,188 @@ bridgeCommand.command("provider-status").description("Show secret-safe provider
|
|
|
8908
9378
|
jsonOut2({ roles: { recall, vision, text } });
|
|
8909
9379
|
});
|
|
8910
9380
|
});
|
|
9381
|
+
bridgeCommand.command("provider-config-list").description("List provider records and role bindings (JSON)").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts) => {
|
|
9382
|
+
const machine = parseProviderScope(opts.scope);
|
|
9383
|
+
await withProviderScope(machine, async (db) => {
|
|
9384
|
+
const providers = await readScopedProviders(db, machine);
|
|
9385
|
+
const roles = await readScopedRoles(db, machine);
|
|
9386
|
+
const rows = buildProviderListing(
|
|
9387
|
+
providers,
|
|
9388
|
+
(ref) => getProviderApiKey(ref) !== null
|
|
9389
|
+
);
|
|
9390
|
+
jsonOut2({
|
|
9391
|
+
scope: machine ? "machine" : "shared",
|
|
9392
|
+
providers: rows,
|
|
9393
|
+
roles,
|
|
9394
|
+
orphans: findOrphanKeyRefs(listProviderApiKeyRefs(), providers)
|
|
9395
|
+
});
|
|
9396
|
+
});
|
|
9397
|
+
});
|
|
9398
|
+
bridgeCommand.command("provider-config-upsert").description("Add or update a provider record (JSON)").requiredOption("--name <name>", "Provider name").option("--label <label>", "Human-readable label").option("--url <url>", "Endpoint base URL").option("--model <model>", "Model id").option(
|
|
9399
|
+
"--flavor <flavor>",
|
|
9400
|
+
`Wire protocol: ${VALID_API_FLAVORS.join(" | ")}`
|
|
9401
|
+
).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("--scope <scope>", "machine (default) or shared", "machine").action(async (opts, command) => {
|
|
9402
|
+
const machine = parseProviderScope(opts.scope);
|
|
9403
|
+
let apiFlavor;
|
|
9404
|
+
if (opts.flavor) {
|
|
9405
|
+
if (!VALID_API_FLAVORS.includes(opts.flavor)) {
|
|
9406
|
+
jsonError(
|
|
9407
|
+
`Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
|
|
9408
|
+
);
|
|
9409
|
+
}
|
|
9410
|
+
apiFlavor = opts.flavor;
|
|
9411
|
+
}
|
|
9412
|
+
let local;
|
|
9413
|
+
if (command.getOptionValueSource("local") === "cli") {
|
|
9414
|
+
local = opts.local === true;
|
|
9415
|
+
}
|
|
9416
|
+
const patch = {};
|
|
9417
|
+
if (opts.label !== void 0) patch.label = opts.label;
|
|
9418
|
+
if (opts.url !== void 0) patch.url = opts.url;
|
|
9419
|
+
if (opts.model !== void 0) patch.model = opts.model;
|
|
9420
|
+
if (apiFlavor !== void 0) patch.apiFlavor = apiFlavor;
|
|
9421
|
+
if (opts.keyRef !== void 0) patch.apiKeyRef = opts.keyRef;
|
|
9422
|
+
if (local !== void 0) patch.local = local;
|
|
9423
|
+
if (opts.runner !== void 0) patch.runner = opts.runner;
|
|
9424
|
+
await withProviderScope(machine, async (db) => {
|
|
9425
|
+
const providers = await readScopedProviders(db, machine);
|
|
9426
|
+
const next = upsertProviderRecord(providers, opts.name, patch);
|
|
9427
|
+
await writeScopedProviders(db, machine, next);
|
|
9428
|
+
const rows = buildProviderListing(
|
|
9429
|
+
next,
|
|
9430
|
+
(ref) => getProviderApiKey(ref) !== null
|
|
9431
|
+
);
|
|
9432
|
+
const row = rows.find((entry) => entry.name === opts.name);
|
|
9433
|
+
jsonOut2({
|
|
9434
|
+
ok: true,
|
|
9435
|
+
scope: machine ? "machine" : "shared",
|
|
9436
|
+
name: opts.name,
|
|
9437
|
+
provider: row,
|
|
9438
|
+
cloudModelHint: opts.url ? getCloudModelRecommendation(opts.url) : null
|
|
9439
|
+
});
|
|
9440
|
+
});
|
|
9441
|
+
});
|
|
9442
|
+
bridgeCommand.command("provider-config-remove").description("Remove a provider record (JSON)").requiredOption("--name <name>", "Provider name").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts) => {
|
|
9443
|
+
const machine = parseProviderScope(opts.scope);
|
|
9444
|
+
await withProviderScope(machine, async (db) => {
|
|
9445
|
+
const providers = await readScopedProviders(db, machine);
|
|
9446
|
+
const { providers: next, removed } = removeProviderRecord(
|
|
9447
|
+
providers,
|
|
9448
|
+
opts.name
|
|
9449
|
+
);
|
|
9450
|
+
if (!removed) {
|
|
9451
|
+
jsonError(`No such provider: ${opts.name}`);
|
|
9452
|
+
}
|
|
9453
|
+
await writeScopedProviders(db, machine, next);
|
|
9454
|
+
jsonOut2({
|
|
9455
|
+
ok: true,
|
|
9456
|
+
scope: machine ? "machine" : "shared",
|
|
9457
|
+
name: opts.name,
|
|
9458
|
+
removed: true,
|
|
9459
|
+
referencingRoles: rolesReferencing(
|
|
9460
|
+
await readScopedRoles(db, machine),
|
|
9461
|
+
opts.name
|
|
9462
|
+
)
|
|
9463
|
+
});
|
|
9464
|
+
});
|
|
9465
|
+
});
|
|
9466
|
+
bridgeCommand.command("provider-config-bind").description("Bind providers to an LLM role (JSON)").requiredOption("--role <role>", `Role: ${VALID_ROLES.join(" | ")}`).requiredOption("--primary <name>", "Primary provider name").option("--fallback <name>", "Fallback provider name").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts) => {
|
|
9467
|
+
if (!VALID_ROLES.includes(opts.role)) {
|
|
9468
|
+
jsonError(`Invalid --role: ${opts.role}. Use ${VALID_ROLES.join(", ")}.`);
|
|
9469
|
+
}
|
|
9470
|
+
const machine = parseProviderScope(opts.scope);
|
|
9471
|
+
await withProviderScope(machine, async (db) => {
|
|
9472
|
+
const providers = await readScopedProviders(db, machine);
|
|
9473
|
+
const roles = await readScopedRoles(db, machine);
|
|
9474
|
+
const nextRoles = bindRoleProviders(
|
|
9475
|
+
roles,
|
|
9476
|
+
opts.role,
|
|
9477
|
+
opts.primary,
|
|
9478
|
+
opts.fallback
|
|
9479
|
+
);
|
|
9480
|
+
await writeScopedRoles(db, machine, nextRoles);
|
|
9481
|
+
const binding = nextRoles[opts.role];
|
|
9482
|
+
const primary = providers[opts.primary];
|
|
9483
|
+
jsonOut2({
|
|
9484
|
+
ok: true,
|
|
9485
|
+
scope: machine ? "machine" : "shared",
|
|
9486
|
+
role: opts.role,
|
|
9487
|
+
binding,
|
|
9488
|
+
warnings: [
|
|
9489
|
+
...primary && primary.apiFlavor === "anthropic-messages" && (opts.role === "recall" || opts.role === "text") ? ["unsupported-provider-for-role"] : [],
|
|
9490
|
+
...opts.primary && !(opts.primary in providers) ? ["primary-provider-undefined"] : [],
|
|
9491
|
+
...opts.fallback && !(opts.fallback in providers) ? ["fallback-provider-undefined"] : []
|
|
9492
|
+
]
|
|
9493
|
+
});
|
|
9494
|
+
});
|
|
9495
|
+
});
|
|
9496
|
+
bridgeCommand.command("provider-set-key").description("Store an API key for a provider reference (JSON)").requiredOption("--ref <ref>", "Credential reference name").requiredOption("--key <value>", "API key value (write-only)").action((opts) => {
|
|
9497
|
+
const key = opts.key.trim();
|
|
9498
|
+
if (!key) jsonError("No key provided.");
|
|
9499
|
+
setProviderApiKey(opts.ref, key);
|
|
9500
|
+
jsonOut2({ ok: true, ref: opts.ref, masked: maskSecret(key) });
|
|
9501
|
+
});
|
|
9502
|
+
bridgeCommand.command("provider-clear-key").description("Remove a stored provider API key (JSON)").requiredOption("--ref <ref>", "Credential reference name").action((opts) => {
|
|
9503
|
+
clearProviderApiKey(opts.ref);
|
|
9504
|
+
jsonOut2({ ok: true, ref: opts.ref });
|
|
9505
|
+
});
|
|
9506
|
+
bridgeCommand.command("list-models").description("List models exposed by an LLM endpoint (JSON)").requiredOption("--url <url>", "Endpoint base URL").option("--key-ref <ref>", "Resolve API key from credentials by reference").action(async (opts) => {
|
|
9507
|
+
const apiKey = opts.keyRef ? getProviderApiKey(opts.keyRef) ?? void 0 : void 0;
|
|
9508
|
+
const models = await getAvailableModels(opts.url, apiKey);
|
|
9509
|
+
jsonOut2({ models });
|
|
9510
|
+
});
|
|
9511
|
+
bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for an endpoint URL (JSON)").requiredOption("--url <url>", "Endpoint base URL").action((opts) => {
|
|
9512
|
+
jsonOut2({ recommendation: getCloudModelRecommendation(opts.url) });
|
|
9513
|
+
});
|
|
9514
|
+
bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
|
|
9515
|
+
const profile = getSystemProfile();
|
|
9516
|
+
const flmInstalled = hasCommand("flm") || existsSync17("C:\\Program Files\\flm\\flm.exe");
|
|
9517
|
+
const ollamaInstalled = hasCommand("ollama");
|
|
9518
|
+
const runners = [
|
|
9519
|
+
{ id: "flm", label: "FastFlowLM", installed: flmInstalled },
|
|
9520
|
+
{ id: "ollama", label: "Ollama", installed: ollamaInstalled },
|
|
9521
|
+
{
|
|
9522
|
+
id: "foundry-local",
|
|
9523
|
+
label: "Foundry Local",
|
|
9524
|
+
installed: false
|
|
9525
|
+
}
|
|
9526
|
+
];
|
|
9527
|
+
let recommended = "ollama";
|
|
9528
|
+
if (profile.recommendedRunner === "fastflowlm" && flmInstalled) {
|
|
9529
|
+
recommended = "flm";
|
|
9530
|
+
} else if (profile.recommendedRunner === "ollama" && ollamaInstalled) {
|
|
9531
|
+
recommended = "ollama";
|
|
9532
|
+
} else if (flmInstalled) {
|
|
9533
|
+
recommended = "flm";
|
|
9534
|
+
} else if (ollamaInstalled) {
|
|
9535
|
+
recommended = "ollama";
|
|
9536
|
+
} else if (profile.recommendedRunner === "fastflowlm") {
|
|
9537
|
+
recommended = "flm";
|
|
9538
|
+
}
|
|
9539
|
+
const defaultUrl = recommended === "ollama" ? "http://localhost:11434/v1" : DEFAULT_LLM_URL;
|
|
9540
|
+
jsonOut2({
|
|
9541
|
+
runners,
|
|
9542
|
+
recommended,
|
|
9543
|
+
defaultUrl,
|
|
9544
|
+
defaultModel: profile.recommendedModel || DEFAULT_LLM_MODEL
|
|
9545
|
+
});
|
|
9546
|
+
});
|
|
9547
|
+
var UI_WRITABLE_SETTINGS = /* @__PURE__ */ new Set([
|
|
9548
|
+
"llm.enabled",
|
|
9549
|
+
"llm.vision.enabled",
|
|
9550
|
+
"system.locale"
|
|
9551
|
+
]);
|
|
9552
|
+
bridgeCommand.command("setting-set").description("Set a single allowlisted ZAM setting value (JSON)").requiredOption("--key <key>", "Setting key").requiredOption("--value <value>", "Setting value").action(async (opts) => {
|
|
9553
|
+
if (!UI_WRITABLE_SETTINGS.has(opts.key)) {
|
|
9554
|
+
jsonError(
|
|
9555
|
+
`Setting "${opts.key}" is not writable via setting-set. Allowed: ${[...UI_WRITABLE_SETTINGS].join(", ")}.`
|
|
9556
|
+
);
|
|
9557
|
+
}
|
|
9558
|
+
await withDb2(async (db) => {
|
|
9559
|
+
await setSetting(db, opts.key, opts.value);
|
|
9560
|
+
jsonOut2({ ok: true, key: opts.key, value: opts.value });
|
|
9561
|
+
});
|
|
9562
|
+
});
|
|
8911
9563
|
bridgeCommand.command("check-vision").description(
|
|
8912
9564
|
"Check if UI observer vision analysis is enabled and ready (JSON)"
|
|
8913
9565
|
).action(async () => {
|
|
@@ -8954,7 +9606,10 @@ bridgeCommand.command("translate-question").description("Translate a question dy
|
|
|
8954
9606
|
});
|
|
8955
9607
|
bridgeCommand.command("evaluate-answer").description(
|
|
8956
9608
|
"Evaluate the learner's active-recall answer using the local LLM (JSON)"
|
|
8957
|
-
).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").option("--context <context>", "Optional token context details").option("--source-link <link>", "Optional source link").
|
|
9609
|
+
).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").option("--context <context>", "Optional token context details").option("--source-link <link>", "Optional source link").option(
|
|
9610
|
+
"--source-content <content>",
|
|
9611
|
+
"Pre-resolved source reference content (skips re-fetch when set)"
|
|
9612
|
+
).action(async (opts) => {
|
|
8958
9613
|
await withDb2(async (db) => {
|
|
8959
9614
|
const isEnabled = await getSetting(db, "llm.enabled") === "true";
|
|
8960
9615
|
if (!isEnabled) {
|
|
@@ -8965,8 +9620,8 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
8965
9620
|
});
|
|
8966
9621
|
return;
|
|
8967
9622
|
}
|
|
8968
|
-
let resolvedContextContent = null;
|
|
8969
|
-
if (opts.sourceLink) {
|
|
9623
|
+
let resolvedContextContent = opts.sourceContent ?? null;
|
|
9624
|
+
if (resolvedContextContent == null && opts.sourceLink) {
|
|
8970
9625
|
try {
|
|
8971
9626
|
const resolved = await resolveReviewContext(opts.sourceLink);
|
|
8972
9627
|
resolvedContextContent = resolved?.content ?? null;
|
|
@@ -8974,7 +9629,7 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
8974
9629
|
}
|
|
8975
9630
|
}
|
|
8976
9631
|
try {
|
|
8977
|
-
const
|
|
9632
|
+
const result = await evaluateAnswerViaLLM(db, {
|
|
8978
9633
|
slug: opts.slug,
|
|
8979
9634
|
concept: opts.concept,
|
|
8980
9635
|
domain: opts.domain,
|
|
@@ -8984,7 +9639,11 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
8984
9639
|
userAnswer: opts.userAnswer,
|
|
8985
9640
|
sourceLinkContent: resolvedContextContent
|
|
8986
9641
|
});
|
|
8987
|
-
jsonOut2({
|
|
9642
|
+
jsonOut2({
|
|
9643
|
+
success: true,
|
|
9644
|
+
evaluation: result.text,
|
|
9645
|
+
evaluationModel: result.model
|
|
9646
|
+
});
|
|
8988
9647
|
} catch (err) {
|
|
8989
9648
|
jsonOut2({
|
|
8990
9649
|
success: false,
|
|
@@ -9223,8 +9882,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
9223
9882
|
});
|
|
9224
9883
|
|
|
9225
9884
|
// src/cli/commands/card.ts
|
|
9226
|
-
import { Command as
|
|
9227
|
-
var cardCommand = new
|
|
9885
|
+
import { Command as Command6 } from "commander";
|
|
9886
|
+
var cardCommand = new Command6("card").description(
|
|
9228
9887
|
"Manage spaced-repetition cards"
|
|
9229
9888
|
);
|
|
9230
9889
|
cardCommand.command("due").description("Show due tokens for a user").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").option("--json", "Output as JSON").option("--summary", "Show only counts per domain (no slugs or concepts)").action(async (opts) => {
|
|
@@ -9422,9 +10081,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
|
|
|
9422
10081
|
});
|
|
9423
10082
|
|
|
9424
10083
|
// src/cli/commands/connector.ts
|
|
9425
|
-
import { input as input2, password } from "@inquirer/prompts";
|
|
9426
|
-
import { Command as
|
|
9427
|
-
var connectorCommand = new
|
|
10084
|
+
import { input as input2, password as password2 } from "@inquirer/prompts";
|
|
10085
|
+
import { Command as Command7 } from "commander";
|
|
10086
|
+
var connectorCommand = new Command7("connector").description(
|
|
9428
10087
|
"Manage external service connectors"
|
|
9429
10088
|
);
|
|
9430
10089
|
connectorCommand.command("setup").description("Configure a connector").argument("<type>", "Connector type (ado, turso)").option("--url <url>", "Turso database URL (non-interactive)").option("--token <token>", "Turso auth token (non-interactive)").option(
|
|
@@ -9449,7 +10108,7 @@ connectorCommand.command("setup").description("Configure a connector").argument(
|
|
|
9449
10108
|
const project = await input2({
|
|
9450
10109
|
message: "Project name:"
|
|
9451
10110
|
});
|
|
9452
|
-
const pat = await
|
|
10111
|
+
const pat = await password2({
|
|
9453
10112
|
message: "Personal Access Token:"
|
|
9454
10113
|
});
|
|
9455
10114
|
if (!orgUrl || !project || !pat) {
|
|
@@ -9540,7 +10199,7 @@ async function setupTurso(urlArg, tokenArg, mode) {
|
|
|
9540
10199
|
const url = urlArg ?? await input2({
|
|
9541
10200
|
message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
|
|
9542
10201
|
});
|
|
9543
|
-
const token = tokenArg ?? await
|
|
10202
|
+
const token = tokenArg ?? await password2({
|
|
9544
10203
|
message: "Auth token:"
|
|
9545
10204
|
});
|
|
9546
10205
|
if (!url || !token) {
|
|
@@ -9569,7 +10228,7 @@ async function setupTurso(urlArg, tokenArg, mode) {
|
|
|
9569
10228
|
import { execSync as execSync5 } from "child_process";
|
|
9570
10229
|
import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync9 } from "fs";
|
|
9571
10230
|
import { join as join18 } from "path";
|
|
9572
|
-
import { Command as
|
|
10231
|
+
import { Command as Command8 } from "commander";
|
|
9573
10232
|
function installHook2() {
|
|
9574
10233
|
const gitDir = join18(process.cwd(), ".git");
|
|
9575
10234
|
if (!existsSync18(gitDir)) {
|
|
@@ -9599,7 +10258,7 @@ zam git-sync --commit HEAD --quiet
|
|
|
9599
10258
|
process.exit(1);
|
|
9600
10259
|
}
|
|
9601
10260
|
}
|
|
9602
|
-
var gitSyncCommand = new
|
|
10261
|
+
var gitSyncCommand = new Command8("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
|
|
9603
10262
|
if (opts.install) {
|
|
9604
10263
|
installHook2();
|
|
9605
10264
|
return;
|
|
@@ -9691,7 +10350,7 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
|
|
|
9691
10350
|
import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
|
|
9692
10351
|
import { resolve as resolve6 } from "path";
|
|
9693
10352
|
import { input as input3 } from "@inquirer/prompts";
|
|
9694
|
-
import { Command as
|
|
10353
|
+
import { Command as Command9 } from "commander";
|
|
9695
10354
|
async function resolveGoalsDir() {
|
|
9696
10355
|
let goalsDir;
|
|
9697
10356
|
let db;
|
|
@@ -9704,7 +10363,7 @@ async function resolveGoalsDir() {
|
|
|
9704
10363
|
}
|
|
9705
10364
|
return goalsDir ? resolve6(goalsDir) : resolve6("goals");
|
|
9706
10365
|
}
|
|
9707
|
-
var goalCommand = new
|
|
10366
|
+
var goalCommand = new Command9("goal").description(
|
|
9708
10367
|
"Manage learning goals (markdown files)"
|
|
9709
10368
|
);
|
|
9710
10369
|
goalCommand.command("list").description("List all goals").option(
|
|
@@ -9877,7 +10536,7 @@ import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as
|
|
|
9877
10536
|
import { homedir as homedir11 } from "os";
|
|
9878
10537
|
import { join as join19, resolve as resolve7 } from "path";
|
|
9879
10538
|
import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
|
|
9880
|
-
import { Command as
|
|
10539
|
+
import { Command as Command10 } from "commander";
|
|
9881
10540
|
var HOME2 = homedir11();
|
|
9882
10541
|
function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
|
|
9883
10542
|
console.log(`${color}${char.repeat(len)}\x1B[0m`);
|
|
@@ -9914,7 +10573,7 @@ Here, I declare the core concepts and principles I want to master.
|
|
|
9914
10573
|
);
|
|
9915
10574
|
}
|
|
9916
10575
|
}
|
|
9917
|
-
var initCommand = new
|
|
10576
|
+
var initCommand = new Command10("init").description("Launch the guided interactive onboarding wizard").action(async () => {
|
|
9918
10577
|
printLine();
|
|
9919
10578
|
console.log(
|
|
9920
10579
|
"\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
|
|
@@ -10090,7 +10749,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
|
|
|
10090
10749
|
|
|
10091
10750
|
// src/cli/commands/learn.ts
|
|
10092
10751
|
import { input as input6 } from "@inquirer/prompts";
|
|
10093
|
-
import { Command as
|
|
10752
|
+
import { Command as Command11 } from "commander";
|
|
10094
10753
|
|
|
10095
10754
|
// src/cli/learn-format.ts
|
|
10096
10755
|
var BLOOM_VERBS3 = {
|
|
@@ -10428,7 +11087,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
|
|
|
10428
11087
|
function isExitPrompt(err) {
|
|
10429
11088
|
return err instanceof Error && err.name === "ExitPromptError";
|
|
10430
11089
|
}
|
|
10431
|
-
var learnCommand = new
|
|
11090
|
+
var learnCommand = new Command11("learn").description(
|
|
10432
11091
|
"Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
|
|
10433
11092
|
).option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into the revealed answer").action(async (opts) => {
|
|
10434
11093
|
let db;
|
|
@@ -10484,7 +11143,7 @@ ${t(locale, "welcome", { count: queue.items.length })}`);
|
|
|
10484
11143
|
question: item.question
|
|
10485
11144
|
});
|
|
10486
11145
|
if (healed) {
|
|
10487
|
-
resolvedQuestion = healed;
|
|
11146
|
+
resolvedQuestion = healed.question;
|
|
10488
11147
|
}
|
|
10489
11148
|
} catch {
|
|
10490
11149
|
}
|
|
@@ -10549,7 +11208,8 @@ ${"\u2500".repeat(50)}`);
|
|
|
10549
11208
|
`
|
|
10550
11209
|
${t(locale, "feedback_title", { line: "\u2500".repeat(34) })}`
|
|
10551
11210
|
);
|
|
10552
|
-
|
|
11211
|
+
console.log(` \x1B[2m[${evaluation.model}]\x1B[0m`);
|
|
11212
|
+
for (const line of evaluation.text.split("\n")) {
|
|
10553
11213
|
console.log(` ${line}`);
|
|
10554
11214
|
}
|
|
10555
11215
|
} catch (err) {
|
|
@@ -10672,8 +11332,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
|
|
|
10672
11332
|
});
|
|
10673
11333
|
|
|
10674
11334
|
// src/cli/commands/monitor.ts
|
|
10675
|
-
import { Command as
|
|
10676
|
-
var monitorCommand = new
|
|
11335
|
+
import { Command as Command12 } from "commander";
|
|
11336
|
+
var monitorCommand = new Command12("monitor").description(
|
|
10677
11337
|
"Shell observation for real-time task monitoring"
|
|
10678
11338
|
);
|
|
10679
11339
|
monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
|
|
@@ -10832,497 +11492,148 @@ monitorCommand.command("open").description("Open a new monitored terminal window
|
|
|
10832
11492
|
process.exit(1);
|
|
10833
11493
|
}
|
|
10834
11494
|
if (session.completed_at) {
|
|
10835
|
-
console.error(`Error: Session already completed: ${opts.session}`);
|
|
10836
|
-
process.exit(1);
|
|
10837
|
-
}
|
|
10838
|
-
if (!await getSetting(db, "monitor_method")) {
|
|
10839
|
-
await setSetting(db, "monitor_method", "terminal");
|
|
10840
|
-
}
|
|
10841
|
-
} catch (err) {
|
|
10842
|
-
console.error(`Error: ${err.message}`);
|
|
10843
|
-
process.exit(1);
|
|
10844
|
-
} finally {
|
|
10845
|
-
await db?.close();
|
|
10846
|
-
}
|
|
10847
|
-
const dir = opts.dir ?? process.cwd();
|
|
10848
|
-
const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
|
|
10849
|
-
openTerminalWindow({
|
|
10850
|
-
shellSetup,
|
|
10851
|
-
label: `monitor-${opts.session}`,
|
|
10852
|
-
dir,
|
|
10853
|
-
shell
|
|
10854
|
-
});
|
|
10855
|
-
});
|
|
10856
|
-
|
|
10857
|
-
// src/cli/commands/observer.ts
|
|
10858
|
-
import { Command as Command12 } from "commander";
|
|
10859
|
-
var observerCommand = new Command12("observer").description(
|
|
10860
|
-
"Configure what the UI observer may capture (Layer 2 policy)"
|
|
10861
|
-
);
|
|
10862
|
-
function applyObserverListChange(current, entry, op) {
|
|
10863
|
-
const normalized = entry.trim().toLowerCase();
|
|
10864
|
-
const list = parseObserverList(current);
|
|
10865
|
-
const next = op === "add" ? [.../* @__PURE__ */ new Set([...list, normalized])] : list.filter((item) => item !== normalized);
|
|
10866
|
-
return next.join(",");
|
|
10867
|
-
}
|
|
10868
|
-
observerCommand.command("status").description("Show the active observer policy").option("--json", "Output as JSON").action(async (opts) => {
|
|
10869
|
-
await withDb(async (db) => {
|
|
10870
|
-
const policy = await resolveObserverPolicy(db);
|
|
10871
|
-
if (opts.json) {
|
|
10872
|
-
console.log(JSON.stringify(policy, null, 2));
|
|
10873
|
-
return;
|
|
10874
|
-
}
|
|
10875
|
-
console.log("Observer policy:\n");
|
|
10876
|
-
console.log(` Scope: ${policy.scope}`);
|
|
10877
|
-
console.log(` Consent: ${policy.consent}`);
|
|
10878
|
-
console.log(` Retention: ${policy.retention}`);
|
|
10879
|
-
console.log(
|
|
10880
|
-
` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
|
|
10881
|
-
);
|
|
10882
|
-
console.log(
|
|
10883
|
-
` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
|
|
10884
|
-
);
|
|
10885
|
-
console.log(` Redact titles: ${policy.redactWindowTitles}`);
|
|
10886
|
-
console.log(
|
|
10887
|
-
"\nBuilt-in sensitive surfaces (password managers, auth/UAC dialogs, banking) are always refused and cannot be allowlisted."
|
|
10888
|
-
);
|
|
10889
|
-
});
|
|
10890
|
-
});
|
|
10891
|
-
observerCommand.command("grant <process>").description(
|
|
10892
|
-
"Allow the observer to capture a process (adds it to observer.allowlist)"
|
|
10893
|
-
).action(async (processName) => {
|
|
10894
|
-
await withDb(async (db) => {
|
|
10895
|
-
const next = applyObserverListChange(
|
|
10896
|
-
await getSetting(db, "observer.allowlist"),
|
|
10897
|
-
processName,
|
|
10898
|
-
"add"
|
|
10899
|
-
);
|
|
10900
|
-
await setSetting(db, "observer.allowlist", next);
|
|
10901
|
-
await syncObserverSidecarPolicy(db);
|
|
10902
|
-
console.log(`Granted: ${processName.trim().toLowerCase()}`);
|
|
10903
|
-
console.log(`observer.allowlist = ${next || "(empty)"}`);
|
|
10904
|
-
});
|
|
10905
|
-
});
|
|
10906
|
-
observerCommand.command("revoke <process>").description("Remove a process from observer.allowlist").action(async (processName) => {
|
|
10907
|
-
await withDb(async (db) => {
|
|
10908
|
-
const next = applyObserverListChange(
|
|
10909
|
-
await getSetting(db, "observer.allowlist"),
|
|
10910
|
-
processName,
|
|
10911
|
-
"remove"
|
|
10912
|
-
);
|
|
10913
|
-
await setSetting(db, "observer.allowlist", next);
|
|
10914
|
-
await syncObserverSidecarPolicy(db);
|
|
10915
|
-
console.log(`Revoked: ${processName.trim().toLowerCase()}`);
|
|
10916
|
-
console.log(`observer.allowlist = ${next || "(empty)"}`);
|
|
10917
|
-
});
|
|
10918
|
-
});
|
|
10919
|
-
|
|
10920
|
-
// src/cli/commands/profile.ts
|
|
10921
|
-
import { homedir as homedir12 } from "os";
|
|
10922
|
-
import { dirname as dirname6, join as join20, resolve as resolve8 } from "path";
|
|
10923
|
-
import { Command as Command13 } from "commander";
|
|
10924
|
-
var C2 = {
|
|
10925
|
-
reset: "\x1B[0m",
|
|
10926
|
-
bold: "\x1B[1m",
|
|
10927
|
-
cyan: "\x1B[36m",
|
|
10928
|
-
dim: "\x1B[2m",
|
|
10929
|
-
green: "\x1B[32m"
|
|
10930
|
-
};
|
|
10931
|
-
function defaultPersonalDir() {
|
|
10932
|
-
return join20(homedir12(), "Documents", "zam");
|
|
10933
|
-
}
|
|
10934
|
-
function render(profile) {
|
|
10935
|
-
const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
|
|
10936
|
-
console.log(`${C2.bold}ZAM install profile${C2.reset}`);
|
|
10937
|
-
console.log(` mode: ${C2.cyan}${profile.mode}${C2.reset}`);
|
|
10938
|
-
console.log(` personal dir: ${C2.cyan}${profile.personalDir}${C2.reset}`);
|
|
10939
|
-
console.log(` sync: ${sync}`);
|
|
10940
|
-
console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
|
|
10941
|
-
console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
|
|
10942
|
-
}
|
|
10943
|
-
var profileCommand = new Command13("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
|
|
10944
|
-
if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
|
|
10945
|
-
console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
|
|
10946
|
-
process.exit(1);
|
|
10947
|
-
}
|
|
10948
|
-
let db;
|
|
10949
|
-
try {
|
|
10950
|
-
if (opts.mode) setInstallMode(opts.mode);
|
|
10951
|
-
db = await openDatabaseWithSync({ initialize: true });
|
|
10952
|
-
if (opts.dir) {
|
|
10953
|
-
await setSetting(db, "personal.workspace_dir", resolve8(opts.dir));
|
|
10954
|
-
}
|
|
10955
|
-
const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
|
|
10956
|
-
await db.close();
|
|
10957
|
-
db = void 0;
|
|
10958
|
-
const dbPath = getDefaultDbPath();
|
|
10959
|
-
const profile = {
|
|
10960
|
-
mode: getInstallMode(),
|
|
10961
|
-
personalDir,
|
|
10962
|
-
syncProvider: detectSyncProvider(personalDir),
|
|
10963
|
-
dataDir: dirname6(dbPath),
|
|
10964
|
-
dbPath
|
|
10965
|
-
};
|
|
10966
|
-
if (opts.json) {
|
|
10967
|
-
console.log(JSON.stringify(profile, null, 2));
|
|
10968
|
-
return;
|
|
10969
|
-
}
|
|
10970
|
-
render(profile);
|
|
10971
|
-
} catch (err) {
|
|
10972
|
-
await db?.close();
|
|
10973
|
-
console.error("Error:", err.message);
|
|
10974
|
-
process.exit(1);
|
|
10975
|
-
}
|
|
10976
|
-
});
|
|
10977
|
-
|
|
10978
|
-
// src/cli/commands/provider.ts
|
|
10979
|
-
import { password as password2 } from "@inquirer/prompts";
|
|
10980
|
-
import { Command as Command14 } from "commander";
|
|
10981
|
-
var VALID_API_FLAVORS = [
|
|
10982
|
-
"chat-completions",
|
|
10983
|
-
"anthropic-messages"
|
|
10984
|
-
];
|
|
10985
|
-
var VALID_ROLES = ["vision", "recall", "text"];
|
|
10986
|
-
function upsertProviderRecord(providers, name, patch) {
|
|
10987
|
-
const merged = { ...providers[name] ?? {} };
|
|
10988
|
-
if (patch.url !== void 0) merged.url = patch.url;
|
|
10989
|
-
if (patch.model !== void 0) merged.model = patch.model;
|
|
10990
|
-
if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
|
|
10991
|
-
if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
|
|
10992
|
-
if (patch.label !== void 0) merged.label = patch.label;
|
|
10993
|
-
if (patch.local !== void 0) merged.local = patch.local;
|
|
10994
|
-
if (patch.runner !== void 0) merged.runner = patch.runner;
|
|
10995
|
-
return { ...providers, [name]: merged };
|
|
10996
|
-
}
|
|
10997
|
-
function removeProviderRecord(providers, name) {
|
|
10998
|
-
if (!(name in providers)) return { providers, removed: false };
|
|
10999
|
-
const next = { ...providers };
|
|
11000
|
-
delete next[name];
|
|
11001
|
-
return { providers: next, removed: true };
|
|
11002
|
-
}
|
|
11003
|
-
function rolesReferencing(roles, name) {
|
|
11004
|
-
return VALID_ROLES.filter((role) => {
|
|
11005
|
-
const binding = roles[role];
|
|
11006
|
-
return binding?.primary === name || binding?.fallback === name;
|
|
11007
|
-
});
|
|
11008
|
-
}
|
|
11009
|
-
function bindRoleProviders(roles, role, primary, fallback) {
|
|
11010
|
-
const binding = { primary };
|
|
11011
|
-
if (fallback) binding.fallback = fallback;
|
|
11012
|
-
return { ...roles, [role]: binding };
|
|
11013
|
-
}
|
|
11014
|
-
function maskSecret(key) {
|
|
11015
|
-
return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
|
|
11016
|
-
}
|
|
11017
|
-
function buildProviderListing(providers, hasKey) {
|
|
11018
|
-
return Object.entries(providers).map(([name, rec]) => {
|
|
11019
|
-
const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
|
|
11020
|
-
let keyState;
|
|
11021
|
-
if (!rec.apiKeyRef) keyState = "none";
|
|
11022
|
-
else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
|
|
11023
|
-
const row = {
|
|
11024
|
-
name,
|
|
11025
|
-
url: rec.url,
|
|
11026
|
-
model: rec.model,
|
|
11027
|
-
apiFlavor,
|
|
11028
|
-
apiKeyRef: rec.apiKeyRef,
|
|
11029
|
-
keyState
|
|
11030
|
-
};
|
|
11031
|
-
if (rec.label !== void 0) row.label = rec.label;
|
|
11032
|
-
if (rec.local !== void 0) row.local = rec.local;
|
|
11033
|
-
if (rec.runner !== void 0) row.runner = rec.runner;
|
|
11034
|
-
return row;
|
|
11035
|
-
});
|
|
11036
|
-
}
|
|
11037
|
-
function findOrphanKeyRefs(storedRefs, providers) {
|
|
11038
|
-
const used = new Set(
|
|
11039
|
-
Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
|
|
11040
|
-
);
|
|
11041
|
-
return storedRefs.filter((ref) => !used.has(ref));
|
|
11042
|
-
}
|
|
11043
|
-
async function readJson(db, key, fallback) {
|
|
11044
|
-
const raw = await getSetting(db, key);
|
|
11045
|
-
if (!raw) return fallback;
|
|
11046
|
-
try {
|
|
11047
|
-
return JSON.parse(raw);
|
|
11048
|
-
} catch {
|
|
11049
|
-
return fallback;
|
|
11050
|
-
}
|
|
11051
|
-
}
|
|
11052
|
-
var readProviders = (db) => readJson(db, "llm.providers", {});
|
|
11053
|
-
var readRoles = (db) => readJson(db, "llm.roles", {});
|
|
11054
|
-
async function readScopedProviders(db, machine) {
|
|
11055
|
-
if (machine) return getMachineAiConfig().providers ?? {};
|
|
11056
|
-
if (!db)
|
|
11057
|
-
throw new Error("Database is required for shared provider settings.");
|
|
11058
|
-
return readProviders(db);
|
|
11059
|
-
}
|
|
11060
|
-
async function readScopedRoles(db, machine) {
|
|
11061
|
-
if (machine) return getMachineAiConfig().roles ?? {};
|
|
11062
|
-
if (!db)
|
|
11063
|
-
throw new Error("Database is required for shared provider settings.");
|
|
11064
|
-
return readRoles(db);
|
|
11065
|
-
}
|
|
11066
|
-
async function writeScopedProviders(db, machine, p) {
|
|
11067
|
-
if (machine) {
|
|
11068
|
-
const config = getMachineAiConfig();
|
|
11069
|
-
saveMachineAiConfig({ ...config, providers: p });
|
|
11070
|
-
return;
|
|
11071
|
-
}
|
|
11072
|
-
if (!db)
|
|
11073
|
-
throw new Error("Database is required for shared provider settings.");
|
|
11074
|
-
await setSetting(db, "llm.providers", JSON.stringify(p));
|
|
11075
|
-
}
|
|
11076
|
-
async function writeScopedRoles(db, machine, r) {
|
|
11077
|
-
if (machine) {
|
|
11078
|
-
const config = getMachineAiConfig();
|
|
11079
|
-
saveMachineAiConfig({ ...config, roles: r });
|
|
11080
|
-
return;
|
|
11081
|
-
}
|
|
11082
|
-
if (!db)
|
|
11083
|
-
throw new Error("Database is required for shared provider settings.");
|
|
11084
|
-
await setSetting(db, "llm.roles", JSON.stringify(r));
|
|
11085
|
-
}
|
|
11086
|
-
async function withProviderScope(machine, action) {
|
|
11087
|
-
if (machine) {
|
|
11088
|
-
await action(void 0);
|
|
11089
|
-
return;
|
|
11090
|
-
}
|
|
11091
|
-
await withDb(action);
|
|
11092
|
-
}
|
|
11093
|
-
var providerCommand = new Command14("provider").description(
|
|
11094
|
-
"Configure role-based AI providers (url/model/flavor/key, per role)"
|
|
11095
|
-
);
|
|
11096
|
-
providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
|
|
11097
|
-
const machine = Boolean(opts.machine);
|
|
11098
|
-
await withProviderScope(machine, async (db) => {
|
|
11099
|
-
const providers = await readScopedProviders(db, machine);
|
|
11100
|
-
const roles = await readScopedRoles(db, machine);
|
|
11101
|
-
const rows = buildProviderListing(
|
|
11102
|
-
providers,
|
|
11103
|
-
(ref) => getProviderApiKey(ref) !== null
|
|
11104
|
-
);
|
|
11105
|
-
const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
|
|
11106
|
-
if (opts.json) {
|
|
11107
|
-
console.log(
|
|
11108
|
-
JSON.stringify(
|
|
11109
|
-
{
|
|
11110
|
-
scope: machine ? "machine" : "shared",
|
|
11111
|
-
providers: rows,
|
|
11112
|
-
roles,
|
|
11113
|
-
orphans
|
|
11114
|
-
},
|
|
11115
|
-
null,
|
|
11116
|
-
2
|
|
11117
|
-
)
|
|
11118
|
-
);
|
|
11119
|
-
return;
|
|
11120
|
-
}
|
|
11121
|
-
console.log(
|
|
11122
|
-
`Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
|
|
11123
|
-
`
|
|
11124
|
-
);
|
|
11125
|
-
if (rows.length === 0) {
|
|
11126
|
-
console.log(
|
|
11127
|
-
" (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
|
|
11128
|
-
);
|
|
11129
|
-
} else {
|
|
11130
|
-
for (const row of rows) {
|
|
11131
|
-
const key = row.keyState === "set" ? "\x1B[32mkey \u2713\x1B[0m" : row.keyState === "missing" ? `\x1B[31mkey \u2717 (set: zam provider set-key ${row.apiKeyRef})\x1B[0m` : "\x1B[90mno key\x1B[0m";
|
|
11132
|
-
console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
|
|
11133
|
-
console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
|
|
11134
|
-
console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
|
|
11135
|
-
console.log(` flavor: ${row.apiFlavor}`);
|
|
11136
|
-
if (row.label) console.log(` label: ${row.label}`);
|
|
11137
|
-
if (row.local !== void 0) {
|
|
11138
|
-
console.log(` local: ${row.local ? "yes" : "no"}`);
|
|
11139
|
-
}
|
|
11140
|
-
if (row.runner) console.log(` runner: ${row.runner}`);
|
|
11141
|
-
if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
|
|
11142
|
-
}
|
|
11143
|
-
}
|
|
11144
|
-
console.log("\nRoles (llm.roles):\n");
|
|
11145
|
-
for (const role of VALID_ROLES) {
|
|
11146
|
-
const binding = roles[role];
|
|
11147
|
-
if (!binding?.primary) {
|
|
11148
|
-
console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
|
|
11149
|
-
} else {
|
|
11150
|
-
const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
|
|
11151
|
-
console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
|
|
11152
|
-
}
|
|
11153
|
-
}
|
|
11154
|
-
if (orphans.length > 0) {
|
|
11155
|
-
console.log(
|
|
11156
|
-
`
|
|
11157
|
-
\x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
|
|
11158
|
-
);
|
|
11495
|
+
console.error(`Error: Session already completed: ${opts.session}`);
|
|
11496
|
+
process.exit(1);
|
|
11497
|
+
}
|
|
11498
|
+
if (!await getSetting(db, "monitor_method")) {
|
|
11499
|
+
await setSetting(db, "monitor_method", "terminal");
|
|
11159
11500
|
}
|
|
11501
|
+
} catch (err) {
|
|
11502
|
+
console.error(`Error: ${err.message}`);
|
|
11503
|
+
process.exit(1);
|
|
11504
|
+
} finally {
|
|
11505
|
+
await db?.close();
|
|
11506
|
+
}
|
|
11507
|
+
const dir = opts.dir ?? process.cwd();
|
|
11508
|
+
const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
|
|
11509
|
+
openTerminalWindow({
|
|
11510
|
+
shellSetup,
|
|
11511
|
+
label: `monitor-${opts.session}`,
|
|
11512
|
+
dir,
|
|
11513
|
+
shell
|
|
11160
11514
|
});
|
|
11161
11515
|
});
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
).
|
|
11166
|
-
"
|
|
11167
|
-
|
|
11168
|
-
)
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
).
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
).option("--
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
if (
|
|
11178
|
-
console.
|
|
11179
|
-
|
|
11180
|
-
);
|
|
11181
|
-
process.exit(1);
|
|
11516
|
+
|
|
11517
|
+
// src/cli/commands/observer.ts
|
|
11518
|
+
import { Command as Command13 } from "commander";
|
|
11519
|
+
var observerCommand = new Command13("observer").description(
|
|
11520
|
+
"Configure what the UI observer may capture (Layer 2 policy)"
|
|
11521
|
+
);
|
|
11522
|
+
function applyObserverListChange(current, entry, op) {
|
|
11523
|
+
const normalized = entry.trim().toLowerCase();
|
|
11524
|
+
const list = parseObserverList(current);
|
|
11525
|
+
const next = op === "add" ? [.../* @__PURE__ */ new Set([...list, normalized])] : list.filter((item) => item !== normalized);
|
|
11526
|
+
return next.join(",");
|
|
11527
|
+
}
|
|
11528
|
+
observerCommand.command("status").description("Show the active observer policy").option("--json", "Output as JSON").action(async (opts) => {
|
|
11529
|
+
await withDb(async (db) => {
|
|
11530
|
+
const policy = await resolveObserverPolicy(db);
|
|
11531
|
+
if (opts.json) {
|
|
11532
|
+
console.log(JSON.stringify(policy, null, 2));
|
|
11533
|
+
return;
|
|
11182
11534
|
}
|
|
11183
|
-
|
|
11184
|
-
}
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
await withProviderScope(machine, async (db) => {
|
|
11188
|
-
const providers = await readScopedProviders(db, machine);
|
|
11189
|
-
const next = upsertProviderRecord(providers, name, {
|
|
11190
|
-
label: opts.label,
|
|
11191
|
-
url: opts.url,
|
|
11192
|
-
model: opts.model,
|
|
11193
|
-
apiFlavor,
|
|
11194
|
-
apiKeyRef,
|
|
11195
|
-
local: opts.local ? true : void 0,
|
|
11196
|
-
runner: opts.runner
|
|
11197
|
-
});
|
|
11198
|
-
await writeScopedProviders(db, machine, next);
|
|
11199
|
-
if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
|
|
11200
|
-
const rec = next[name];
|
|
11535
|
+
console.log("Observer policy:\n");
|
|
11536
|
+
console.log(` Scope: ${policy.scope}`);
|
|
11537
|
+
console.log(` Consent: ${policy.consent}`);
|
|
11538
|
+
console.log(` Retention: ${policy.retention}`);
|
|
11201
11539
|
console.log(
|
|
11202
|
-
`
|
|
11540
|
+
` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
|
|
11203
11541
|
);
|
|
11204
|
-
console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
|
|
11205
|
-
console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
|
|
11206
|
-
if (rec.label) console.log(` label: ${rec.label}`);
|
|
11207
|
-
if (rec.local !== void 0) {
|
|
11208
|
-
console.log(` local: ${rec.local ? "yes" : "no"}`);
|
|
11209
|
-
}
|
|
11210
|
-
if (rec.runner) console.log(` runner: ${rec.runner}`);
|
|
11211
11542
|
console.log(
|
|
11212
|
-
`
|
|
11543
|
+
` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
|
|
11213
11544
|
);
|
|
11214
|
-
console.log(`
|
|
11215
|
-
if (opts.key) {
|
|
11216
|
-
console.log(` key: stored (${maskSecret(opts.key)})`);
|
|
11217
|
-
} else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
|
|
11218
|
-
console.log(
|
|
11219
|
-
`
|
|
11220
|
-
\u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
|
|
11221
|
-
);
|
|
11222
|
-
}
|
|
11223
|
-
if (!rec.url) {
|
|
11224
|
-
console.log(
|
|
11225
|
-
"\n \u26A0 No --url set; this provider inherits the base llm.url."
|
|
11226
|
-
);
|
|
11227
|
-
}
|
|
11545
|
+
console.log(` Redact titles: ${policy.redactWindowTitles}`);
|
|
11228
11546
|
console.log(
|
|
11229
|
-
|
|
11230
|
-
Bind it to a role: zam provider use recall --primary ${name}`
|
|
11547
|
+
"\nBuilt-in sensitive surfaces (password managers, auth/UAC dialogs, banking) are always refused and cannot be allowlisted."
|
|
11231
11548
|
);
|
|
11232
11549
|
});
|
|
11233
11550
|
});
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
const
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
if (!removed) {
|
|
11243
|
-
console.log(`No such provider: ${name}`);
|
|
11244
|
-
return;
|
|
11245
|
-
}
|
|
11246
|
-
await writeScopedProviders(db, machine, next);
|
|
11247
|
-
console.log(
|
|
11248
|
-
`Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
|
|
11249
|
-
);
|
|
11250
|
-
const referencing = rolesReferencing(
|
|
11251
|
-
await readScopedRoles(db, machine),
|
|
11252
|
-
name
|
|
11551
|
+
observerCommand.command("grant <process>").description(
|
|
11552
|
+
"Allow the observer to capture a process (adds it to observer.allowlist)"
|
|
11553
|
+
).action(async (processName) => {
|
|
11554
|
+
await withDb(async (db) => {
|
|
11555
|
+
const next = applyObserverListChange(
|
|
11556
|
+
await getSetting(db, "observer.allowlist"),
|
|
11557
|
+
processName,
|
|
11558
|
+
"add"
|
|
11253
11559
|
);
|
|
11254
|
-
|
|
11255
|
-
|
|
11256
|
-
|
|
11257
|
-
|
|
11258
|
-
}
|
|
11560
|
+
await setSetting(db, "observer.allowlist", next);
|
|
11561
|
+
await syncObserverSidecarPolicy(db);
|
|
11562
|
+
console.log(`Granted: ${processName.trim().toLowerCase()}`);
|
|
11563
|
+
console.log(`observer.allowlist = ${next || "(empty)"}`);
|
|
11259
11564
|
});
|
|
11260
11565
|
});
|
|
11261
|
-
|
|
11262
|
-
|
|
11263
|
-
|
|
11264
|
-
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
console.error("--primary is required.");
|
|
11268
|
-
process.exit(1);
|
|
11269
|
-
}
|
|
11270
|
-
const machine = Boolean(opts.machine);
|
|
11271
|
-
await withProviderScope(machine, async (db) => {
|
|
11272
|
-
const providers = await readScopedProviders(db, machine);
|
|
11273
|
-
await writeScopedRoles(
|
|
11274
|
-
db,
|
|
11275
|
-
machine,
|
|
11276
|
-
bindRoleProviders(
|
|
11277
|
-
await readScopedRoles(db, machine),
|
|
11278
|
-
role,
|
|
11279
|
-
opts.primary,
|
|
11280
|
-
opts.fallback
|
|
11281
|
-
)
|
|
11282
|
-
);
|
|
11283
|
-
const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
|
|
11284
|
-
console.log(
|
|
11285
|
-
`Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
|
|
11566
|
+
observerCommand.command("revoke <process>").description("Remove a process from observer.allowlist").action(async (processName) => {
|
|
11567
|
+
await withDb(async (db) => {
|
|
11568
|
+
const next = applyObserverListChange(
|
|
11569
|
+
await getSetting(db, "observer.allowlist"),
|
|
11570
|
+
processName,
|
|
11571
|
+
"remove"
|
|
11286
11572
|
);
|
|
11287
|
-
|
|
11288
|
-
|
|
11289
|
-
|
|
11290
|
-
|
|
11291
|
-
if (ref && !(ref in providers)) {
|
|
11292
|
-
console.log(
|
|
11293
|
-
` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
|
|
11294
|
-
);
|
|
11295
|
-
}
|
|
11296
|
-
}
|
|
11573
|
+
await setSetting(db, "observer.allowlist", next);
|
|
11574
|
+
await syncObserverSidecarPolicy(db);
|
|
11575
|
+
console.log(`Revoked: ${processName.trim().toLowerCase()}`);
|
|
11576
|
+
console.log(`observer.allowlist = ${next || "(empty)"}`);
|
|
11297
11577
|
});
|
|
11298
11578
|
});
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11579
|
+
|
|
11580
|
+
// src/cli/commands/profile.ts
|
|
11581
|
+
import { homedir as homedir12 } from "os";
|
|
11582
|
+
import { dirname as dirname6, join as join20, resolve as resolve8 } from "path";
|
|
11583
|
+
import { Command as Command14 } from "commander";
|
|
11584
|
+
var C2 = {
|
|
11585
|
+
reset: "\x1B[0m",
|
|
11586
|
+
bold: "\x1B[1m",
|
|
11587
|
+
cyan: "\x1B[36m",
|
|
11588
|
+
dim: "\x1B[2m",
|
|
11589
|
+
green: "\x1B[32m"
|
|
11590
|
+
};
|
|
11591
|
+
function defaultPersonalDir() {
|
|
11592
|
+
return join20(homedir12(), "Documents", "zam");
|
|
11593
|
+
}
|
|
11594
|
+
function render(profile) {
|
|
11595
|
+
const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
|
|
11596
|
+
console.log(`${C2.bold}ZAM install profile${C2.reset}`);
|
|
11597
|
+
console.log(` mode: ${C2.cyan}${profile.mode}${C2.reset}`);
|
|
11598
|
+
console.log(` personal dir: ${C2.cyan}${profile.personalDir}${C2.reset}`);
|
|
11599
|
+
console.log(` sync: ${sync}`);
|
|
11600
|
+
console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
|
|
11601
|
+
console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
|
|
11602
|
+
}
|
|
11603
|
+
var profileCommand = new Command14("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
|
|
11604
|
+
if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
|
|
11605
|
+
console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
|
|
11606
|
+
process.exit(1);
|
|
11607
|
+
}
|
|
11608
|
+
let db;
|
|
11305
11609
|
try {
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11610
|
+
if (opts.mode) setInstallMode(opts.mode);
|
|
11611
|
+
db = await openDatabaseWithSync({ initialize: true });
|
|
11612
|
+
if (opts.dir) {
|
|
11613
|
+
await setSetting(db, "personal.workspace_dir", resolve8(opts.dir));
|
|
11310
11614
|
}
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11615
|
+
const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
|
|
11616
|
+
await db.close();
|
|
11617
|
+
db = void 0;
|
|
11618
|
+
const dbPath = getDefaultDbPath();
|
|
11619
|
+
const profile = {
|
|
11620
|
+
mode: getInstallMode(),
|
|
11621
|
+
personalDir,
|
|
11622
|
+
syncProvider: detectSyncProvider(personalDir),
|
|
11623
|
+
dataDir: dirname6(dbPath),
|
|
11624
|
+
dbPath
|
|
11625
|
+
};
|
|
11626
|
+
if (opts.json) {
|
|
11627
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
11628
|
+
return;
|
|
11317
11629
|
}
|
|
11630
|
+
render(profile);
|
|
11631
|
+
} catch (err) {
|
|
11632
|
+
await db?.close();
|
|
11318
11633
|
console.error("Error:", err.message);
|
|
11319
11634
|
process.exit(1);
|
|
11320
11635
|
}
|
|
11321
11636
|
});
|
|
11322
|
-
providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
|
|
11323
|
-
clearProviderApiKey(ref);
|
|
11324
|
-
console.log(`Cleared API key for "${ref}".`);
|
|
11325
|
-
});
|
|
11326
11637
|
|
|
11327
11638
|
// src/cli/commands/review.ts
|
|
11328
11639
|
import { Command as Command15 } from "commander";
|