zam-core 0.5.0 → 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 +1947 -1449
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +29 -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();
|
|
@@ -4477,6 +4492,13 @@ function upsertConfiguredWorkspace(workspace, path = defaultConfigPath()) {
|
|
|
4477
4492
|
saveConfiguredWorkspaces(next, path);
|
|
4478
4493
|
return next;
|
|
4479
4494
|
}
|
|
4495
|
+
function removeConfiguredWorkspace(id, path = defaultConfigPath()) {
|
|
4496
|
+
const next = getConfiguredWorkspaces(path).filter(
|
|
4497
|
+
(workspace) => workspace.id !== id
|
|
4498
|
+
);
|
|
4499
|
+
saveConfiguredWorkspaces(next, path);
|
|
4500
|
+
return next;
|
|
4501
|
+
}
|
|
4480
4502
|
function detectSyncProvider(dir) {
|
|
4481
4503
|
const p = dir.toLowerCase();
|
|
4482
4504
|
if (p.includes("onedrive")) return "OneDrive";
|
|
@@ -5352,15 +5374,37 @@ var agentCommand = new Command("agent").description("Provision and inspect the a
|
|
|
5352
5374
|
// src/cli/commands/bridge.ts
|
|
5353
5375
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
5354
5376
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
5355
|
-
import {
|
|
5377
|
+
import {
|
|
5378
|
+
existsSync as existsSync17,
|
|
5379
|
+
mkdirSync as mkdirSync10,
|
|
5380
|
+
readdirSync as readdirSync3,
|
|
5381
|
+
readFileSync as readFileSync11,
|
|
5382
|
+
rmSync as rmSync3
|
|
5383
|
+
} from "fs";
|
|
5356
5384
|
import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
|
|
5357
5385
|
import { basename as basename5, join as join17, resolve as resolve5 } from "path";
|
|
5358
|
-
import { Command as
|
|
5386
|
+
import { Command as Command5 } from "commander";
|
|
5359
5387
|
|
|
5360
5388
|
// src/cli/llm/client.ts
|
|
5361
5389
|
import { spawn as spawn2 } from "child_process";
|
|
5362
5390
|
import { existsSync as existsSync14 } from "fs";
|
|
5363
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
|
+
}
|
|
5364
5408
|
var DEFAULT_LLM_MODEL = "qwen3.5:4b";
|
|
5365
5409
|
var DEFAULT_LLM_API_KEY = "sk-none";
|
|
5366
5410
|
async function getLlmConfig(db) {
|
|
@@ -5471,6 +5515,7 @@ function materializeProvider(rec, base, role, meta) {
|
|
|
5471
5515
|
label: rec.label,
|
|
5472
5516
|
source: meta.source,
|
|
5473
5517
|
local: rec.local ?? isLocalEndpoint(url),
|
|
5518
|
+
...rec.runner ? { runner: rec.runner } : {},
|
|
5474
5519
|
...role === "vision" ? { maxFrames: base.maxFrames } : {}
|
|
5475
5520
|
};
|
|
5476
5521
|
}
|
|
@@ -5553,10 +5598,7 @@ async function readChatContent(res, label) {
|
|
|
5553
5598
|
}
|
|
5554
5599
|
async function generateQuestionViaLLM(db, input8) {
|
|
5555
5600
|
const cfg = await getProviderForRole(db, "recall");
|
|
5556
|
-
|
|
5557
|
-
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5558
|
-
}
|
|
5559
|
-
assertChatCompletions(cfg);
|
|
5601
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
5560
5602
|
const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
|
|
5561
5603
|
const verb = BLOOM_VERBS2[bloom];
|
|
5562
5604
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
@@ -5576,31 +5618,36 @@ ${input8.sourceLinkContent ? `Source Reference:
|
|
|
5576
5618
|
${input8.sourceLinkContent}` : ""}
|
|
5577
5619
|
|
|
5578
5620
|
Active-Recall Question:`;
|
|
5579
|
-
const res = await fetchWithInteractiveTimeout(
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
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
|
+
};
|
|
5597
5647
|
}
|
|
5598
5648
|
async function evaluateAnswerViaLLM(db, input8) {
|
|
5599
5649
|
const cfg = await getProviderForRole(db, "recall");
|
|
5600
|
-
|
|
5601
|
-
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5602
|
-
}
|
|
5603
|
-
assertChatCompletions(cfg);
|
|
5650
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
5604
5651
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
5605
5652
|
const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
|
|
5606
5653
|
const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
|
|
@@ -5630,51 +5677,59 @@ ${input8.sourceLinkContent ? `Source Code Reference:
|
|
|
5630
5677
|
${input8.sourceLinkContent}` : ""}
|
|
5631
5678
|
|
|
5632
5679
|
Evaluation:`;
|
|
5633
|
-
const res = await fetchWithInteractiveTimeout(
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
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
|
+
};
|
|
5651
5706
|
}
|
|
5652
5707
|
async function translateQuestionViaLLM(db, question) {
|
|
5653
5708
|
const cfg = await getProviderForRole(db, "recall");
|
|
5654
|
-
|
|
5655
|
-
throw new Error("LLM integration is disabled in settings");
|
|
5656
|
-
}
|
|
5657
|
-
assertChatCompletions(cfg);
|
|
5709
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
5658
5710
|
const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
5659
5711
|
const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
|
|
5660
5712
|
Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
|
|
5661
|
-
const res = await fetchWithInteractiveTimeout(
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
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
|
+
);
|
|
5678
5733
|
return readChatContent(res, "Translation");
|
|
5679
5734
|
}
|
|
5680
5735
|
async function isLlmOnline(url) {
|
|
@@ -5747,6 +5802,107 @@ async function checkProviderChain(primary) {
|
|
|
5747
5802
|
}
|
|
5748
5803
|
return { primary: first };
|
|
5749
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
|
+
}
|
|
5750
5906
|
async function isVisionProviderModelExplicit(db, active) {
|
|
5751
5907
|
if (await getSetting(db, "llm.vision.model")) return true;
|
|
5752
5908
|
const providers = await readJsonSetting(db, "llm.providers");
|
|
@@ -5848,13 +6004,8 @@ async function getProviderRoleStatus(db, role) {
|
|
|
5848
6004
|
fallback: summarizeFallback(cfg.fallback)
|
|
5849
6005
|
};
|
|
5850
6006
|
}
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
const chain = await checkProviderChain(cfg);
|
|
5854
|
-
selected = chain.firstUsable ?? chain.primary;
|
|
5855
|
-
} else {
|
|
5856
|
-
selected = await checkProviderEndpoint(cfg);
|
|
5857
|
-
}
|
|
6007
|
+
const chain = await checkProviderChain(cfg);
|
|
6008
|
+
const selected = chain.firstUsable ?? chain.primary;
|
|
5858
6009
|
const active = selected.endpoint;
|
|
5859
6010
|
const usable = selected.online && selected.modelAvailable;
|
|
5860
6011
|
const reason = usable ? void 0 : selected.online ? "model-not-found" : "offline";
|
|
@@ -5876,7 +6027,35 @@ async function getProviderRoleStatus(db, role) {
|
|
|
5876
6027
|
fallback: summarizeFallback(cfg.fallback)
|
|
5877
6028
|
};
|
|
5878
6029
|
}
|
|
5879
|
-
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
|
+
}
|
|
5880
6059
|
let runner = "unknown";
|
|
5881
6060
|
let port = "8000";
|
|
5882
6061
|
try {
|
|
@@ -5892,8 +6071,8 @@ function detectRunner(url, model) {
|
|
|
5892
6071
|
}
|
|
5893
6072
|
return { runner, port };
|
|
5894
6073
|
}
|
|
5895
|
-
function spawnLocalRunner(url, model) {
|
|
5896
|
-
const { runner, port } = detectRunner(url, model);
|
|
6074
|
+
function spawnLocalRunner(url, model, hint) {
|
|
6075
|
+
const { runner, port } = detectRunner(url, model, hint);
|
|
5897
6076
|
try {
|
|
5898
6077
|
if (runner === "fastflowlm") {
|
|
5899
6078
|
const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
|
|
@@ -5914,8 +6093,8 @@ function spawnLocalRunner(url, model) {
|
|
|
5914
6093
|
} catch {
|
|
5915
6094
|
}
|
|
5916
6095
|
}
|
|
5917
|
-
async function startLocalRunner(url, model, locale) {
|
|
5918
|
-
const { runner, port } = detectRunner(url, model);
|
|
6096
|
+
async function startLocalRunner(url, model, locale, hint) {
|
|
6097
|
+
const { runner, port } = detectRunner(url, model, hint);
|
|
5919
6098
|
if (runner === "fastflowlm") {
|
|
5920
6099
|
const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
|
|
5921
6100
|
if (!hasCommand("flm") && flmExe === "flm") {
|
|
@@ -5974,7 +6153,7 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5974
6153
|
let attempts = 0;
|
|
5975
6154
|
const dotsPerLine = 30;
|
|
5976
6155
|
while (true) {
|
|
5977
|
-
await new Promise((
|
|
6156
|
+
await new Promise((resolve9) => setTimeout(resolve9, 500));
|
|
5978
6157
|
if (await isLlmOnline(url)) {
|
|
5979
6158
|
if (attempts > 0) process.stdout.write("\n");
|
|
5980
6159
|
return true;
|
|
@@ -5999,105 +6178,39 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5999
6178
|
}
|
|
6000
6179
|
}
|
|
6001
6180
|
async function ensureLocalLlmRunning(db) {
|
|
6002
|
-
const
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
}
|
|
6006
|
-
if (
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
return { usable: false, reason: "unsupported-provider" };
|
|
6011
|
-
}
|
|
6012
|
-
const { url, model, apiKey, locale } = cfg;
|
|
6013
|
-
const isLocal = isLocalEndpoint(url);
|
|
6014
|
-
console.log(`Checking if local LLM server is online at ${url}...`);
|
|
6015
|
-
let online = await isLlmOnline(url);
|
|
6016
|
-
if (!online && isLocal) {
|
|
6017
|
-
console.log(`\x1B[33m\u26A0 Local LLM server is offline on ${url}.\x1B[0m`);
|
|
6018
|
-
online = await startLocalRunner(url, model, locale);
|
|
6019
|
-
}
|
|
6020
|
-
if (!online) {
|
|
6021
|
-
console.warn(
|
|
6022
|
-
`\x1B[33m\u26A0 LLM server is not reachable at ${url}. Continuing without AI coaching.\x1B[0m
|
|
6023
|
-
`
|
|
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`
|
|
6024
6189
|
);
|
|
6025
|
-
return { usable:
|
|
6190
|
+
return { usable: true };
|
|
6026
6191
|
}
|
|
6027
|
-
|
|
6028
|
-
const available = await getAvailableModels(url, apiKey);
|
|
6029
|
-
const modelKnown = available.length === 0 || available.some((m) => m.toLowerCase() === model.toLowerCase());
|
|
6030
|
-
if (!modelKnown) {
|
|
6192
|
+
if (result.reason === "unsupported-provider") {
|
|
6031
6193
|
console.warn(
|
|
6032
|
-
`\x1B[31m\u2717
|
|
6194
|
+
`\x1B[31m\u2717 Recall provider is not supported for active recall.\x1B[0m`
|
|
6033
6195
|
);
|
|
6034
|
-
|
|
6196
|
+
} else if (result.reason === "model-not-found") {
|
|
6035
6197
|
console.warn(
|
|
6036
|
-
|
|
6198
|
+
`\x1B[31m\u2717 Configured model "${result.model}" is not available on the server.\x1B[0m`
|
|
6037
6199
|
);
|
|
6200
|
+
console.warn(` Available models: ${result.availableModels.join(", ")}`);
|
|
6201
|
+
} else if (result.reason === "offline") {
|
|
6038
6202
|
console.warn(
|
|
6039
|
-
|
|
6203
|
+
`\x1B[33m\u26A0 No recall LLM endpoint is reachable. Continuing without AI coaching.\x1B[0m
|
|
6204
|
+
`
|
|
6040
6205
|
);
|
|
6041
|
-
return { usable: false, reason: "model-not-found" };
|
|
6042
6206
|
}
|
|
6043
|
-
return { usable:
|
|
6207
|
+
return { usable: false, reason: result.reason };
|
|
6044
6208
|
}
|
|
6045
6209
|
async function ensureLlmReadyHeadless(db, opts = {}) {
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
return {
|
|
6051
|
-
usable: false,
|
|
6052
|
-
reason: "disabled",
|
|
6053
|
-
online: false,
|
|
6054
|
-
model,
|
|
6055
|
-
availableModels: []
|
|
6056
|
-
};
|
|
6057
|
-
}
|
|
6058
|
-
if (cfg.apiFlavor !== "chat-completions") {
|
|
6059
|
-
return {
|
|
6060
|
-
usable: false,
|
|
6061
|
-
reason: "unsupported-provider",
|
|
6062
|
-
online: false,
|
|
6063
|
-
model,
|
|
6064
|
-
availableModels: []
|
|
6065
|
-
};
|
|
6066
|
-
}
|
|
6067
|
-
const isLocal = isLocalEndpoint(url);
|
|
6068
|
-
let online = await isLlmOnline(url);
|
|
6069
|
-
if (!online && isLocal) {
|
|
6070
|
-
spawnLocalRunner(url, model);
|
|
6071
|
-
const deadline = Date.now() + timeoutMs;
|
|
6072
|
-
while (Date.now() < deadline) {
|
|
6073
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
6074
|
-
if (await isLlmOnline(url)) {
|
|
6075
|
-
online = true;
|
|
6076
|
-
break;
|
|
6077
|
-
}
|
|
6078
|
-
}
|
|
6079
|
-
}
|
|
6080
|
-
if (!online) {
|
|
6081
|
-
return {
|
|
6082
|
-
usable: false,
|
|
6083
|
-
reason: "offline",
|
|
6084
|
-
online: false,
|
|
6085
|
-
model,
|
|
6086
|
-
availableModels: []
|
|
6087
|
-
};
|
|
6088
|
-
}
|
|
6089
|
-
const availableModels = await getAvailableModels(url, apiKey);
|
|
6090
|
-
const modelKnown = availableModels.length === 0 || availableModels.some((m) => m.toLowerCase() === model.toLowerCase());
|
|
6091
|
-
if (!modelKnown) {
|
|
6092
|
-
return {
|
|
6093
|
-
usable: false,
|
|
6094
|
-
reason: "model-not-found",
|
|
6095
|
-
online: true,
|
|
6096
|
-
model,
|
|
6097
|
-
availableModels
|
|
6098
|
-
};
|
|
6099
|
-
}
|
|
6100
|
-
return { usable: true, online: true, model, availableModels };
|
|
6210
|
+
return prepareRecallChain(db, {
|
|
6211
|
+
timeoutMs: opts.timeoutMs ?? 25e3,
|
|
6212
|
+
interactive: false
|
|
6213
|
+
});
|
|
6101
6214
|
}
|
|
6102
6215
|
async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
6103
6216
|
const {
|
|
@@ -6129,8 +6242,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
|
6129
6242
|
const dotsPerLine = 30;
|
|
6130
6243
|
while (true) {
|
|
6131
6244
|
let timeoutId;
|
|
6132
|
-
const timeoutPromise = new Promise((
|
|
6133
|
-
timeoutId = setTimeout(() =>
|
|
6245
|
+
const timeoutPromise = new Promise((resolve9) => {
|
|
6246
|
+
timeoutId = setTimeout(() => resolve9("timeout"), timeoutMs);
|
|
6134
6247
|
});
|
|
6135
6248
|
const dotsInterval = setInterval(() => {
|
|
6136
6249
|
process.stdout.write(".");
|
|
@@ -6187,15 +6300,22 @@ async function ensureHighQualityQuestion(db, token) {
|
|
|
6187
6300
|
bloomLevel: token.bloomLevel,
|
|
6188
6301
|
sourceLinkContent
|
|
6189
6302
|
});
|
|
6190
|
-
if (generated
|
|
6191
|
-
await updateToken(db, token.slug, { question: generated });
|
|
6192
|
-
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
|
+
};
|
|
6193
6310
|
}
|
|
6194
6311
|
} catch {
|
|
6195
6312
|
}
|
|
6196
6313
|
}
|
|
6197
6314
|
if (token.question && token.question.trim().length > 0) {
|
|
6198
|
-
return
|
|
6315
|
+
return {
|
|
6316
|
+
question: token.question.trim(),
|
|
6317
|
+
source: "original"
|
|
6318
|
+
};
|
|
6199
6319
|
}
|
|
6200
6320
|
return null;
|
|
6201
6321
|
}
|
|
@@ -6240,25 +6360,25 @@ async function observeUiSnapshotViaLLM(db, input8) {
|
|
|
6240
6360
|
const imageUrls = [];
|
|
6241
6361
|
const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
|
|
6242
6362
|
if (isVideo) {
|
|
6243
|
-
const { mkdirSync:
|
|
6363
|
+
const { mkdirSync: mkdirSync14, readdirSync: readdirSync4, rmSync: rmSync4 } = await import("fs");
|
|
6244
6364
|
const { execSync: execSync6 } = await import("child_process");
|
|
6245
6365
|
const tempDir = join14(
|
|
6246
6366
|
tmpdir2(),
|
|
6247
6367
|
`zam-frames-${randomBytes(4).toString("hex")}`
|
|
6248
6368
|
);
|
|
6249
|
-
|
|
6369
|
+
mkdirSync14(tempDir, { recursive: true });
|
|
6250
6370
|
try {
|
|
6251
6371
|
execSync6(
|
|
6252
6372
|
`ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
|
|
6253
6373
|
{ stdio: "ignore" }
|
|
6254
6374
|
);
|
|
6255
|
-
let files =
|
|
6375
|
+
let files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
|
|
6256
6376
|
if (files.length === 0) {
|
|
6257
6377
|
execSync6(
|
|
6258
6378
|
`ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
|
|
6259
6379
|
{ stdio: "ignore" }
|
|
6260
6380
|
);
|
|
6261
|
-
files =
|
|
6381
|
+
files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
|
|
6262
6382
|
}
|
|
6263
6383
|
const maxFrames = cfg.maxFrames ?? 100;
|
|
6264
6384
|
let sampledFiles = files;
|
|
@@ -6280,7 +6400,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
|
|
|
6280
6400
|
}
|
|
6281
6401
|
} finally {
|
|
6282
6402
|
try {
|
|
6283
|
-
|
|
6403
|
+
rmSync4(tempDir, { recursive: true, force: true });
|
|
6284
6404
|
} catch {
|
|
6285
6405
|
}
|
|
6286
6406
|
}
|
|
@@ -6403,7 +6523,7 @@ async function requestChatCompletionsVisionDraft(args) {
|
|
|
6403
6523
|
}
|
|
6404
6524
|
],
|
|
6405
6525
|
temperature: 0,
|
|
6406
|
-
max_tokens: args.input.maxTokens ??
|
|
6526
|
+
max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
|
|
6407
6527
|
...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
|
|
6408
6528
|
}),
|
|
6409
6529
|
locale: args.locale,
|
|
@@ -6459,7 +6579,7 @@ async function requestAnthropicVisionDraft(args) {
|
|
|
6459
6579
|
},
|
|
6460
6580
|
body: JSON.stringify({
|
|
6461
6581
|
model: args.model,
|
|
6462
|
-
max_tokens: args.input.maxTokens ??
|
|
6582
|
+
max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
|
|
6463
6583
|
system: VISION_SYSTEM_PROMPT,
|
|
6464
6584
|
messages: [
|
|
6465
6585
|
{
|
|
@@ -6614,26 +6734,9 @@ function isRecord2(value) {
|
|
|
6614
6734
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6615
6735
|
}
|
|
6616
6736
|
|
|
6617
|
-
// src/cli/commands/
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
if (stored) return stored;
|
|
6621
|
-
const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
|
|
6622
|
-
await setSetting(db, "user.id", userId);
|
|
6623
|
-
return userId;
|
|
6624
|
-
}
|
|
6625
|
-
async function resolveUser(opts, db, resolveOpts) {
|
|
6626
|
-
if (opts.user) return opts.user;
|
|
6627
|
-
const stored = await getSetting(db, "user.id");
|
|
6628
|
-
if (stored) return stored;
|
|
6629
|
-
const message = "No user specified. Set a default with: zam whoami --set <id>";
|
|
6630
|
-
if (resolveOpts?.json) {
|
|
6631
|
-
console.log(JSON.stringify({ error: message }, null, 2));
|
|
6632
|
-
} else {
|
|
6633
|
-
console.error(message);
|
|
6634
|
-
}
|
|
6635
|
-
process.exit(1);
|
|
6636
|
-
}
|
|
6737
|
+
// src/cli/commands/provider.ts
|
|
6738
|
+
import { password } from "@inquirer/prompts";
|
|
6739
|
+
import { Command as Command2 } from "commander";
|
|
6637
6740
|
|
|
6638
6741
|
// src/cli/commands/shared/db.ts
|
|
6639
6742
|
function defaultErrorHandler(message) {
|
|
@@ -6655,717 +6758,1187 @@ function jsonOut(data) {
|
|
|
6655
6758
|
console.log(JSON.stringify(data, null, 2));
|
|
6656
6759
|
}
|
|
6657
6760
|
|
|
6658
|
-
// src/cli/commands/
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
import { join as join16, resolve as resolve4 } from "path";
|
|
6663
|
-
import { confirm, input } from "@inquirer/prompts";
|
|
6664
|
-
import { Command as Command3 } from "commander";
|
|
6665
|
-
|
|
6666
|
-
// src/cli/commands/setup.ts
|
|
6667
|
-
import {
|
|
6668
|
-
copyFileSync as copyFileSync2,
|
|
6669
|
-
existsSync as existsSync15,
|
|
6670
|
-
mkdirSync as mkdirSync8,
|
|
6671
|
-
readFileSync as readFileSync10,
|
|
6672
|
-
writeFileSync as writeFileSync7
|
|
6673
|
-
} from "fs";
|
|
6674
|
-
import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
|
|
6675
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6676
|
-
import { Command as Command2 } from "commander";
|
|
6677
|
-
var packageRoot = [
|
|
6678
|
-
fileURLToPath2(new URL("../..", import.meta.url)),
|
|
6679
|
-
fileURLToPath2(new URL("../../..", import.meta.url))
|
|
6680
|
-
].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
|
|
6681
|
-
var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
|
|
6682
|
-
var SKILL_PAIRS = [
|
|
6683
|
-
{
|
|
6684
|
-
from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
|
|
6685
|
-
to: join15(".claude", "skills", "zam", "SKILL.md"),
|
|
6686
|
-
agents: ["claude"]
|
|
6687
|
-
},
|
|
6688
|
-
{
|
|
6689
|
-
from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
|
|
6690
|
-
to: join15(".agent", "skills", "zam", "SKILL.md"),
|
|
6691
|
-
agents: ["agent"]
|
|
6692
|
-
},
|
|
6693
|
-
{
|
|
6694
|
-
from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
|
|
6695
|
-
to: join15(".agents", "skills", "zam", "SKILL.md"),
|
|
6696
|
-
agents: ["copilot", "codex"]
|
|
6697
|
-
}
|
|
6761
|
+
// src/cli/commands/provider.ts
|
|
6762
|
+
var VALID_API_FLAVORS = [
|
|
6763
|
+
"chat-completions",
|
|
6764
|
+
"anthropic-messages"
|
|
6698
6765
|
];
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
};
|
|
6710
|
-
const selected = /* @__PURE__ */ new Set();
|
|
6711
|
-
for (const raw of value.split(",")) {
|
|
6712
|
-
const key = raw.trim().toLowerCase();
|
|
6713
|
-
const mapped = aliases[key];
|
|
6714
|
-
if (!mapped) {
|
|
6715
|
-
throw new Error(
|
|
6716
|
-
`Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
|
|
6717
|
-
);
|
|
6718
|
-
}
|
|
6719
|
-
for (const item of mapped) selected.add(item);
|
|
6720
|
-
}
|
|
6721
|
-
return selected;
|
|
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 };
|
|
6722
6777
|
}
|
|
6723
|
-
function
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
if (!existsSync15(from)) {
|
|
6729
|
-
console.warn(` warn source not found, skipping: ${from}`);
|
|
6730
|
-
continue;
|
|
6731
|
-
}
|
|
6732
|
-
if (existsSync15(dest) && !force) {
|
|
6733
|
-
console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
|
|
6734
|
-
continue;
|
|
6735
|
-
}
|
|
6736
|
-
if (dryRun) {
|
|
6737
|
-
console.log(` would copy ${to}`);
|
|
6738
|
-
anyAction = true;
|
|
6739
|
-
continue;
|
|
6740
|
-
}
|
|
6741
|
-
mkdirSync8(dirname5(dest), { recursive: true });
|
|
6742
|
-
copyFileSync2(from, dest);
|
|
6743
|
-
console.log(` copy ${to}`);
|
|
6744
|
-
anyAction = true;
|
|
6745
|
-
}
|
|
6746
|
-
if (!anyAction && !force) {
|
|
6747
|
-
console.log(
|
|
6748
|
-
"\nSkill files are already up to date. Run with --force to overwrite."
|
|
6749
|
-
);
|
|
6750
|
-
}
|
|
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 };
|
|
6751
6783
|
}
|
|
6752
|
-
function
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
return `ZAM database via Turso remote at ${target.location}`;
|
|
6758
|
-
case "turso-native":
|
|
6759
|
-
return `ZAM database via Turso native driver at ${target.location}`;
|
|
6760
|
-
case "turso-replica":
|
|
6761
|
-
return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
|
|
6762
|
-
}
|
|
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
|
+
});
|
|
6763
6789
|
}
|
|
6764
|
-
|
|
6765
|
-
|
|
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;
|
|
6766
6827
|
try {
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
console.log(` init ${formatDatabaseInitTarget(target)}`);
|
|
6771
|
-
} catch (err) {
|
|
6772
|
-
const msg = err.message;
|
|
6773
|
-
if (!msg.includes("already")) {
|
|
6774
|
-
console.warn(` warn database init: ${msg}`);
|
|
6775
|
-
} else {
|
|
6776
|
-
console.log(` skip database already initialized`);
|
|
6777
|
-
}
|
|
6828
|
+
return JSON.parse(raw);
|
|
6829
|
+
} catch {
|
|
6830
|
+
return fallback;
|
|
6778
6831
|
}
|
|
6779
6832
|
}
|
|
6780
|
-
var
|
|
6781
|
-
var
|
|
6782
|
-
function
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
if (!dryRun) {
|
|
6788
|
-
mkdirSync8(dirname5(dest), { recursive: true });
|
|
6789
|
-
writeFileSync7(dest, `${block}
|
|
6790
|
-
`, "utf8");
|
|
6791
|
-
}
|
|
6792
|
-
return "write";
|
|
6793
|
-
}
|
|
6794
|
-
const existing = readFileSync10(dest, "utf8");
|
|
6795
|
-
if (existing.includes(block)) return "skip";
|
|
6796
|
-
const start = existing.indexOf(ZAM_BLOCK_START);
|
|
6797
|
-
const end = existing.indexOf(ZAM_BLOCK_END);
|
|
6798
|
-
const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
|
|
6799
|
-
|
|
6800
|
-
${block}
|
|
6801
|
-
`;
|
|
6802
|
-
if (!dryRun) writeFileSync7(dest, next, "utf8");
|
|
6803
|
-
return start >= 0 && end > start ? "update" : "write";
|
|
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);
|
|
6804
6840
|
}
|
|
6805
|
-
function
|
|
6806
|
-
if (
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
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;
|
|
6810
6852
|
}
|
|
6853
|
+
if (!db)
|
|
6854
|
+
throw new Error("Database is required for shared provider settings.");
|
|
6855
|
+
await setSetting(db, "llm.providers", JSON.stringify(p));
|
|
6811
6856
|
}
|
|
6812
|
-
function
|
|
6813
|
-
if (
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
if (!opts.updateExisting) {
|
|
6817
|
-
console.log(` skip CLAUDE.md (already present)`);
|
|
6818
|
-
return;
|
|
6819
|
-
}
|
|
6820
|
-
const action = upsertMarkedBlock(
|
|
6821
|
-
dest,
|
|
6822
|
-
`## ZAM learning sessions
|
|
6823
|
-
|
|
6824
|
-
ZAM is available in this repository. Use the \`zam\` skill in Claude Code to turn real work into an observed learning session with active recall and FSRS scheduling.
|
|
6825
|
-
|
|
6826
|
-
- Skill files live under \`.claude/skills/zam/\`.
|
|
6827
|
-
- Fast-changing review data lives in \`~/.zam/\`, not in this repository.
|
|
6828
|
-
- Run \`zam setup --target . --agents claude --force\` after upgrading ZAM to refresh the skill.`,
|
|
6829
|
-
Boolean(opts.dryRun)
|
|
6830
|
-
);
|
|
6831
|
-
logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
|
|
6857
|
+
async function writeScopedRoles(db, machine, r) {
|
|
6858
|
+
if (machine) {
|
|
6859
|
+
const config = getMachineAiConfig();
|
|
6860
|
+
saveMachineAiConfig({ ...config, roles: r });
|
|
6832
6861
|
return;
|
|
6833
6862
|
}
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
## Regular use
|
|
6844
|
-
Run \`/zam\` to start a learning session on whatever you are working on.
|
|
6845
|
-
|
|
6846
|
-
## What lives here
|
|
6847
|
-
- \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
|
|
6848
|
-
- \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
|
|
6849
|
-
|
|
6850
|
-
## Fast-changing data
|
|
6851
|
-
Learning tokens, cards, and review history live in local SQLite by default.
|
|
6852
|
-
Use \`zam connector setup turso\` to store cloud credentials in
|
|
6853
|
-
\`~/.zam/credentials.json\` and use a Turso database across machines.
|
|
6854
|
-
`;
|
|
6855
|
-
if (opts.dryRun) {
|
|
6856
|
-
console.log(` would write CLAUDE.md`);
|
|
6857
|
-
} else {
|
|
6858
|
-
writeFileSync7(dest, content, "utf8");
|
|
6859
|
-
console.log(` write CLAUDE.md`);
|
|
6863
|
+
if (!db)
|
|
6864
|
+
throw new Error("Database is required for shared provider settings.");
|
|
6865
|
+
await setSetting(db, "llm.roles", JSON.stringify(r));
|
|
6866
|
+
}
|
|
6867
|
+
async function withProviderScope(machine, action) {
|
|
6868
|
+
if (machine) {
|
|
6869
|
+
await action(void 0);
|
|
6870
|
+
return;
|
|
6860
6871
|
}
|
|
6872
|
+
await withDb(action);
|
|
6861
6873
|
}
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
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
|
+
);
|
|
6868
6900
|
return;
|
|
6869
6901
|
}
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
ZAM is available in this repository. Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` where supported to turn real work into an observed learning session with active recall and FSRS scheduling.
|
|
6875
|
-
|
|
6876
|
-
- Skill files live under \`.agents/skills/zam/\`.
|
|
6877
|
-
- Fast-changing review data lives in \`~/.zam/\`, not in this repository.
|
|
6878
|
-
- Run \`zam setup --target . --agents copilot,codex --force\` after upgrading ZAM to refresh the skill.`,
|
|
6879
|
-
Boolean(opts.dryRun)
|
|
6902
|
+
console.log(
|
|
6903
|
+
`Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
|
|
6904
|
+
`
|
|
6880
6905
|
);
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
const action = upsertMarkedBlock(
|
|
6922
|
-
dest,
|
|
6923
|
-
`## ZAM learning sessions
|
|
6924
|
-
|
|
6925
|
-
ZAM is available in this repository through \`.agents/skills/zam/\`. Use the \`zam\` skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
|
|
6926
|
-
|
|
6927
|
-
- Fast-changing review data lives in \`~/.zam/\`, not in this repository.
|
|
6928
|
-
- Run \`zam setup --target . --agents copilot --force\` after upgrading ZAM to refresh the skill.`,
|
|
6929
|
-
Boolean(opts.dryRun)
|
|
6930
|
-
);
|
|
6931
|
-
logInstructionAction(
|
|
6932
|
-
action,
|
|
6933
|
-
".github/copilot-instructions.md",
|
|
6934
|
-
Boolean(opts.dryRun)
|
|
6935
|
-
);
|
|
6936
|
-
}
|
|
6937
|
-
var setupCommand = new Command2("setup").description(
|
|
6938
|
-
"Distribute ZAM skill files into this personal instance and initialize the database"
|
|
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}`);
|
|
6923
|
+
}
|
|
6924
|
+
}
|
|
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
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
if (orphans.length > 0) {
|
|
6936
|
+
console.log(
|
|
6937
|
+
`
|
|
6938
|
+
\x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
|
|
6939
|
+
);
|
|
6940
|
+
}
|
|
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, ...)"
|
|
6939
6946
|
).option(
|
|
6940
|
-
"--
|
|
6941
|
-
"
|
|
6942
|
-
false
|
|
6943
|
-
).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(
|
|
6944
|
-
"--agents <list>",
|
|
6945
|
-
"comma-separated agents to wire: all, claude, copilot, codex, agent"
|
|
6947
|
+
"--flavor <flavor>",
|
|
6948
|
+
`Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
|
|
6946
6949
|
).option(
|
|
6947
|
-
"--
|
|
6948
|
-
"
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
console.error(
|
|
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
|
+
);
|
|
6957
6962
|
process.exit(1);
|
|
6958
6963
|
}
|
|
6959
|
-
|
|
6960
|
-
|
|
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];
|
|
6961
6982
|
console.log(
|
|
6962
|
-
`
|
|
6963
|
-
`
|
|
6983
|
+
`Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
|
|
6964
6984
|
);
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
if (
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
updateExisting: updateExistingInstructions
|
|
6971
|
-
});
|
|
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"}`);
|
|
6972
6990
|
}
|
|
6973
|
-
if (
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
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}`
|
|
7002
|
+
);
|
|
6978
7003
|
}
|
|
6979
|
-
if (
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
});
|
|
7004
|
+
if (!rec.url) {
|
|
7005
|
+
console.log(
|
|
7006
|
+
"\n \u26A0 No --url set; this provider inherits the base llm.url."
|
|
7007
|
+
);
|
|
6984
7008
|
}
|
|
6985
7009
|
console.log(
|
|
6986
|
-
|
|
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
|
|
6987
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);
|
|
6988
7046
|
}
|
|
6989
|
-
)
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
function runGit(cwd, args) {
|
|
6993
|
-
try {
|
|
6994
|
-
return execFileSync3("git", args, {
|
|
6995
|
-
cwd,
|
|
6996
|
-
stdio: "pipe",
|
|
6997
|
-
encoding: "utf8"
|
|
6998
|
-
}).trim();
|
|
6999
|
-
} catch (err) {
|
|
7000
|
-
throw new Error(`Git command failed: ${err.message}`);
|
|
7047
|
+
if (!opts.primary) {
|
|
7048
|
+
console.error("--primary is required.");
|
|
7049
|
+
process.exit(1);
|
|
7001
7050
|
}
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
"team",
|
|
7015
|
-
"family",
|
|
7016
|
-
"community",
|
|
7017
|
-
"organization",
|
|
7018
|
-
"custom"
|
|
7019
|
-
];
|
|
7020
|
-
var WORKSPACE_SOURCE_CONTROLS = [
|
|
7021
|
-
"github",
|
|
7022
|
-
"azure-devops",
|
|
7023
|
-
"git",
|
|
7024
|
-
"none"
|
|
7025
|
-
];
|
|
7026
|
-
function parseWorkspaceKind(value) {
|
|
7027
|
-
const kind = (value ?? "custom").toLowerCase();
|
|
7028
|
-
if (WORKSPACE_KINDS.includes(kind)) {
|
|
7029
|
-
return kind;
|
|
7030
|
-
}
|
|
7031
|
-
throw new Error(
|
|
7032
|
-
`Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
|
|
7033
|
-
);
|
|
7034
|
-
}
|
|
7035
|
-
function parseWorkspaceSourceControl(value) {
|
|
7036
|
-
if (!value) return void 0;
|
|
7037
|
-
const source = value.toLowerCase();
|
|
7038
|
-
if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
|
|
7039
|
-
return source;
|
|
7040
|
-
}
|
|
7041
|
-
throw new Error(
|
|
7042
|
-
`Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
|
|
7043
|
-
);
|
|
7044
|
-
}
|
|
7045
|
-
function parseScopes(value) {
|
|
7046
|
-
if (!value) return void 0;
|
|
7047
|
-
const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
7048
|
-
return scopes.length > 0 ? scopes : void 0;
|
|
7049
|
-
}
|
|
7050
|
-
function requireWorkspace(id) {
|
|
7051
|
-
const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
|
|
7052
|
-
if (!workspace) {
|
|
7053
|
-
throw new Error(
|
|
7054
|
-
`Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
|
|
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
|
+
)
|
|
7055
7063
|
);
|
|
7056
|
-
|
|
7057
|
-
return workspace;
|
|
7058
|
-
}
|
|
7059
|
-
workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
|
|
7060
|
-
const workspaces = getConfiguredWorkspaces();
|
|
7061
|
-
if (opts.json) {
|
|
7062
|
-
console.log(JSON.stringify({ workspaces }, null, 2));
|
|
7063
|
-
return;
|
|
7064
|
-
}
|
|
7065
|
-
console.log("Configured ZAM workspaces:\n");
|
|
7066
|
-
if (workspaces.length === 0) {
|
|
7064
|
+
const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
|
|
7067
7065
|
console.log(
|
|
7068
|
-
"
|
|
7066
|
+
`Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
|
|
7069
7067
|
);
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
}
|
|
7080
|
-
if (workspace.knowledgeScopes?.length) {
|
|
7081
|
-
console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
|
|
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
|
+
}
|
|
7082
7077
|
}
|
|
7083
|
-
}
|
|
7078
|
+
});
|
|
7084
7079
|
});
|
|
7085
|
-
|
|
7086
|
-
"
|
|
7087
|
-
|
|
7088
|
-
"
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
`Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
|
|
7092
|
-
).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
|
|
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) => {
|
|
7093
7086
|
try {
|
|
7094
|
-
const
|
|
7095
|
-
if (!
|
|
7096
|
-
console.error(
|
|
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.");
|
|
7097
7090
|
process.exit(1);
|
|
7098
7091
|
}
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
kind: parseWorkspaceKind(opts.kind),
|
|
7102
|
-
path,
|
|
7103
|
-
...opts.label ? { label: opts.label } : {},
|
|
7104
|
-
...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
|
|
7105
|
-
...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
|
|
7106
|
-
...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
|
|
7107
|
-
};
|
|
7108
|
-
upsertConfiguredWorkspace(workspace);
|
|
7109
|
-
console.log(`Registered workspace "${id}" at ${path}.`);
|
|
7092
|
+
setProviderApiKey(ref, key.trim());
|
|
7093
|
+
console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
|
|
7110
7094
|
} catch (err) {
|
|
7111
|
-
|
|
7095
|
+
if (err.name === "ExitPromptError") {
|
|
7096
|
+
console.log("\nCancelled.");
|
|
7097
|
+
process.exit(0);
|
|
7098
|
+
}
|
|
7099
|
+
console.error("Error:", err.message);
|
|
7112
7100
|
process.exit(1);
|
|
7113
7101
|
}
|
|
7114
7102
|
});
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
)
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
)
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
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
|
+
});
|
|
7107
|
+
|
|
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;
|
|
7115
|
+
}
|
|
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));
|
|
7123
|
+
} else {
|
|
7124
|
+
console.error(message);
|
|
7125
|
+
}
|
|
7126
|
+
process.exit(1);
|
|
7127
|
+
}
|
|
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
|
+
);
|
|
7157
7185
|
}
|
|
7158
|
-
|
|
7159
|
-
console.error(`Error: ${err.message}`);
|
|
7160
|
-
process.exit(1);
|
|
7186
|
+
for (const item of mapped) selected.add(item);
|
|
7161
7187
|
}
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
let workspaceDir = "";
|
|
7188
|
+
return selected;
|
|
7189
|
+
}
|
|
7190
|
+
function pathExists(path) {
|
|
7166
7191
|
try {
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
await db.close();
|
|
7192
|
+
lstatSync(path);
|
|
7193
|
+
return true;
|
|
7170
7194
|
} catch {
|
|
7171
|
-
|
|
7195
|
+
return false;
|
|
7172
7196
|
}
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
process.exit(1);
|
|
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;
|
|
7184
7207
|
}
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
process.exit(1);
|
|
7208
|
+
}
|
|
7209
|
+
function linkPointsTo(sourceDir, destinationDir) {
|
|
7210
|
+
try {
|
|
7211
|
+
return lstatSync(destinationDir).isSymbolicLink() && pathsResolveToSameDirectory(sourceDir, destinationDir);
|
|
7212
|
+
} catch {
|
|
7213
|
+
return false;
|
|
7192
7214
|
}
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
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")
|
|
7199
7223
|
);
|
|
7224
|
+
} catch {
|
|
7225
|
+
return false;
|
|
7200
7226
|
}
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
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) {
|
|
7242
7273
|
console.warn(
|
|
7243
|
-
|
|
7274
|
+
` warn ${dirname5(to)} already exists and is not managed by ZAM; use --force to replace it`
|
|
7244
7275
|
);
|
|
7245
7276
|
}
|
|
7277
|
+
results.push({
|
|
7278
|
+
source: sourceDir,
|
|
7279
|
+
destination: destinationDir,
|
|
7280
|
+
action: "skipped",
|
|
7281
|
+
reason: "unmanaged-destination"
|
|
7282
|
+
});
|
|
7283
|
+
continue;
|
|
7246
7284
|
}
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
console.log(" 5. Click 'Create repository'\n");
|
|
7260
|
-
const githubUrl = await input({
|
|
7261
|
-
message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
|
|
7262
|
-
});
|
|
7263
|
-
if (githubUrl) {
|
|
7264
|
-
try {
|
|
7265
|
-
console.log("Linking remote repository and pushing...");
|
|
7266
|
-
let hasOrigin = false;
|
|
7267
|
-
try {
|
|
7268
|
-
runGit(workspaceDir, ["remote", "get-url", "origin"]);
|
|
7269
|
-
hasOrigin = true;
|
|
7270
|
-
} catch {
|
|
7271
|
-
}
|
|
7272
|
-
runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
|
|
7273
|
-
runGit(workspaceDir, ["push", "-u", "origin", "main"]);
|
|
7274
|
-
console.log(
|
|
7275
|
-
"\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
|
|
7276
|
-
);
|
|
7277
|
-
} catch (err) {
|
|
7278
|
-
console.error(
|
|
7279
|
-
`\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
|
|
7280
|
-
);
|
|
7281
|
-
console.log(
|
|
7282
|
-
"You can push manually later using: git push -u origin main"
|
|
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"
|
|
7283
7297
|
);
|
|
7298
|
+
log(` ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
|
|
7284
7299
|
}
|
|
7300
|
+
results.push({ source: sourceDir, destination: destinationDir, action });
|
|
7285
7301
|
}
|
|
7286
|
-
|
|
7287
|
-
async function backupDatabaseTo(db, targetDir) {
|
|
7288
|
-
const backupDir = join16(targetDir, "zam-backups");
|
|
7289
|
-
mkdirSync9(backupDir, { recursive: true });
|
|
7290
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7291
|
-
const dest = join16(backupDir, `zam-${stamp}.db`);
|
|
7292
|
-
await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
7293
|
-
return dest;
|
|
7302
|
+
return results;
|
|
7294
7303
|
}
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
})
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
const reason = `The database is ${target.kind} (${target.location}); file backup applies only to a local database \u2014 your Turso remote is already the cloud backup.`;
|
|
7306
|
-
if (opts.json) {
|
|
7307
|
-
console.log(JSON.stringify({ ok: false, reason }));
|
|
7308
|
-
return;
|
|
7309
|
-
}
|
|
7310
|
-
console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
|
|
7311
|
-
process.exit(1);
|
|
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}`;
|
|
7312
7314
|
}
|
|
7313
|
-
|
|
7315
|
+
}
|
|
7316
|
+
async function initDatabase(skipInit) {
|
|
7317
|
+
if (skipInit) return;
|
|
7314
7318
|
try {
|
|
7315
|
-
|
|
7316
|
-
const
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
console.log(JSON.stringify({ ok: true, path: dest }));
|
|
7320
|
-
} else {
|
|
7321
|
-
console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
|
|
7322
|
-
}
|
|
7319
|
+
const target = getDatabaseTargetInfo();
|
|
7320
|
+
const db = await openDatabaseWithSync({ initialize: true });
|
|
7321
|
+
await db.close();
|
|
7322
|
+
console.log(` init ${formatDatabaseInitTarget(target)}`);
|
|
7323
7323
|
} catch (err) {
|
|
7324
|
-
const
|
|
7325
|
-
if (
|
|
7326
|
-
console.
|
|
7324
|
+
const msg = err.message;
|
|
7325
|
+
if (!msg.includes("already")) {
|
|
7326
|
+
console.warn(` warn database init: ${msg}`);
|
|
7327
7327
|
} else {
|
|
7328
|
-
console.
|
|
7328
|
+
console.log(` skip database already initialized`);
|
|
7329
7329
|
}
|
|
7330
|
-
process.exit(1);
|
|
7331
|
-
} finally {
|
|
7332
|
-
await db?.close();
|
|
7333
7330
|
}
|
|
7334
|
-
});
|
|
7335
|
-
|
|
7336
|
-
// src/cli/commands/bridge.ts
|
|
7337
|
-
var isServeMode = false;
|
|
7338
|
-
function jsonOut2(data) {
|
|
7339
|
-
console.log(JSON.stringify(data, null, 2));
|
|
7340
7331
|
}
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
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";
|
|
7344
7345
|
}
|
|
7345
|
-
|
|
7346
|
-
|
|
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";
|
|
7347
7356
|
}
|
|
7348
|
-
function
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
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}`);
|
|
7352
7362
|
}
|
|
7353
|
-
return parsed;
|
|
7354
|
-
}
|
|
7355
|
-
async function withDb2(fn) {
|
|
7356
|
-
await withDb(fn, jsonError);
|
|
7357
7363
|
}
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
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(
|
|
7373
|
+
dest,
|
|
7374
|
+
`## ZAM learning sessions
|
|
7375
|
+
|
|
7376
|
+
ZAM is available in this repository. Use the \`zam\` skill in Claude Code to turn real work into an observed learning session with active recall and FSRS scheduling.
|
|
7377
|
+
|
|
7378
|
+
- Skill files live under \`.claude/skills/zam/\`.
|
|
7379
|
+
- Fast-changing review data lives in \`~/.zam/\`, not in this repository.
|
|
7380
|
+
- The skill directory is linked to this ZAM installation and updates with it.`,
|
|
7381
|
+
Boolean(opts.dryRun)
|
|
7382
|
+
);
|
|
7383
|
+
logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
|
|
7384
|
+
return;
|
|
7385
|
+
}
|
|
7386
|
+
const name = basename4(cwd);
|
|
7387
|
+
const content = `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
|
|
7388
|
+
|
|
7389
|
+
This is a ZAM personal instance. ZAM builds lasting skills through spaced
|
|
7390
|
+
repetition during real work \xE2\u20AC\u201D not separate study sessions.
|
|
7391
|
+
|
|
7392
|
+
## First time here?
|
|
7393
|
+
Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
|
|
7394
|
+
|
|
7395
|
+
## Regular use
|
|
7396
|
+
Run \`/zam\` to start a learning session on whatever you are working on.
|
|
7397
|
+
|
|
7398
|
+
## What lives here
|
|
7399
|
+
- \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
|
|
7400
|
+
- \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
|
|
7401
|
+
|
|
7402
|
+
## Fast-changing data
|
|
7403
|
+
Learning tokens, cards, and review history live in local SQLite by default.
|
|
7404
|
+
Use \`zam connector setup turso\` to store cloud credentials in
|
|
7405
|
+
\`~/.zam/credentials.json\` and use a Turso database across machines.
|
|
7406
|
+
`;
|
|
7407
|
+
if (opts.dryRun) {
|
|
7408
|
+
console.log(` would write CLAUDE.md`);
|
|
7409
|
+
} else {
|
|
7410
|
+
writeFileSync7(dest, content, "utf8");
|
|
7411
|
+
console.log(` write CLAUDE.md`);
|
|
7412
|
+
}
|
|
7413
|
+
}
|
|
7414
|
+
function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
|
|
7415
|
+
if (skipAgentsMd) return;
|
|
7416
|
+
const dest = join15(cwd, "AGENTS.md");
|
|
7417
|
+
if (existsSync15(dest)) {
|
|
7418
|
+
if (!opts.updateExisting) {
|
|
7419
|
+
console.log(` skip AGENTS.md (already present)`);
|
|
7420
|
+
return;
|
|
7421
|
+
}
|
|
7422
|
+
const action = upsertMarkedBlock(
|
|
7423
|
+
dest,
|
|
7424
|
+
`## ZAM learning sessions
|
|
7425
|
+
|
|
7426
|
+
ZAM is available in this repository. Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` where supported to turn real work into an observed learning session with active recall and FSRS scheduling.
|
|
7427
|
+
|
|
7428
|
+
- Skill files live under \`.agents/skills/zam/\`.
|
|
7429
|
+
- Fast-changing review data lives in \`~/.zam/\`, not in this repository.
|
|
7430
|
+
- The skill directory is linked to this ZAM installation and updates with it.`,
|
|
7431
|
+
Boolean(opts.dryRun)
|
|
7432
|
+
);
|
|
7433
|
+
logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
|
|
7434
|
+
return;
|
|
7435
|
+
}
|
|
7436
|
+
const name = basename4(cwd);
|
|
7437
|
+
const content = `# ZAM Personal Kernel - ${name}
|
|
7438
|
+
|
|
7439
|
+
This is a ZAM personal instance. ZAM builds lasting skills through spaced
|
|
7440
|
+
repetition during real work, not separate study sessions.
|
|
7441
|
+
|
|
7442
|
+
## First time here?
|
|
7443
|
+
Run \`zam setup\` from the shell. When this repository includes
|
|
7444
|
+
\`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
|
|
7445
|
+
or invoke \`$setup\`.
|
|
7446
|
+
|
|
7447
|
+
## Regular use
|
|
7448
|
+
Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
|
|
7449
|
+
learning session on whatever you are working on.
|
|
7450
|
+
|
|
7451
|
+
## What lives here
|
|
7452
|
+
- \`beliefs/\` - your worldview, approved by git commit
|
|
7453
|
+
- \`goals/\` - your objectives, decomposed into tasks and learning tokens
|
|
7454
|
+
|
|
7455
|
+
## Fast-changing data
|
|
7456
|
+
Learning tokens, cards, and review history live in local SQLite by default.
|
|
7457
|
+
Use \`zam connector setup turso\` to store cloud credentials in
|
|
7458
|
+
\`~/.zam/credentials.json\` and use a Turso database across machines.
|
|
7459
|
+
|
|
7460
|
+
## Codex skills
|
|
7461
|
+
Codex discovers repository skills under \`.agents/skills/\`. Run
|
|
7462
|
+
\`zam setup --force\` after upgrading \`zam-core\` to refresh them.
|
|
7463
|
+
`;
|
|
7464
|
+
if (opts.dryRun) {
|
|
7465
|
+
console.log(` would write AGENTS.md`);
|
|
7466
|
+
} else {
|
|
7467
|
+
writeFileSync7(dest, content, "utf8");
|
|
7468
|
+
console.log(` write AGENTS.md`);
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
|
|
7472
|
+
const dest = join15(cwd, ".github", "copilot-instructions.md");
|
|
7473
|
+
const action = upsertMarkedBlock(
|
|
7474
|
+
dest,
|
|
7475
|
+
`## ZAM learning sessions
|
|
7476
|
+
|
|
7477
|
+
ZAM is available in this repository through \`.claude/skills/zam/\`. Use the \`zam\` project skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
|
|
7478
|
+
|
|
7479
|
+
- Fast-changing review data lives in \`~/.zam/\`, not in this repository.
|
|
7480
|
+
- The skill directory is linked to this ZAM installation and updates with it.`,
|
|
7481
|
+
Boolean(opts.dryRun)
|
|
7482
|
+
);
|
|
7483
|
+
logInstructionAction(
|
|
7484
|
+
action,
|
|
7485
|
+
".github/copilot-instructions.md",
|
|
7486
|
+
Boolean(opts.dryRun)
|
|
7487
|
+
);
|
|
7488
|
+
}
|
|
7489
|
+
var setupCommand = new Command3("setup").description(
|
|
7490
|
+
"Link ZAM skill directories into this workspace and initialize the database"
|
|
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(
|
|
7492
|
+
"--agents <list>",
|
|
7493
|
+
"comma-separated agents to wire: all, claude, copilot, codex, agent"
|
|
7494
|
+
).option(
|
|
7495
|
+
"--dry-run",
|
|
7496
|
+
"show what would be written without changing files",
|
|
7497
|
+
false
|
|
7498
|
+
).action(
|
|
7499
|
+
async (opts) => {
|
|
7500
|
+
let agents;
|
|
7501
|
+
try {
|
|
7502
|
+
agents = parseSetupAgents(opts.agents);
|
|
7503
|
+
} catch (err) {
|
|
7504
|
+
console.error(`Error: ${err.message}`);
|
|
7505
|
+
process.exit(1);
|
|
7506
|
+
}
|
|
7507
|
+
const target = resolve3(opts.target ?? process.cwd());
|
|
7508
|
+
const updateExistingInstructions = Boolean(opts.target) || opts.force;
|
|
7509
|
+
console.log(
|
|
7510
|
+
`Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
|
|
7511
|
+
`
|
|
7512
|
+
);
|
|
7513
|
+
wireSkills(target, agents, {
|
|
7514
|
+
force: opts.force,
|
|
7515
|
+
dryRun: opts.dryRun
|
|
7516
|
+
});
|
|
7517
|
+
await initDatabase(opts.skipInit || opts.dryRun);
|
|
7518
|
+
if (agents.has("claude")) {
|
|
7519
|
+
writeClaudeMd(opts.skipClaudeMd, target, {
|
|
7520
|
+
dryRun: opts.dryRun,
|
|
7521
|
+
updateExisting: updateExistingInstructions
|
|
7522
|
+
});
|
|
7523
|
+
}
|
|
7524
|
+
if (agents.has("codex") || agents.has("agent")) {
|
|
7525
|
+
writeAgentsMd(opts.skipAgentsMd, target, {
|
|
7526
|
+
dryRun: opts.dryRun,
|
|
7527
|
+
updateExisting: updateExistingInstructions
|
|
7528
|
+
});
|
|
7529
|
+
}
|
|
7530
|
+
if (agents.has("copilot") && (opts.target || opts.agents)) {
|
|
7531
|
+
writeCopilotInstructions(target, {
|
|
7532
|
+
dryRun: opts.dryRun,
|
|
7533
|
+
updateExisting: updateExistingInstructions
|
|
7534
|
+
});
|
|
7535
|
+
}
|
|
7536
|
+
console.log(
|
|
7537
|
+
"\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Copilot/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
|
|
7538
|
+
);
|
|
7539
|
+
}
|
|
7540
|
+
);
|
|
7541
|
+
|
|
7542
|
+
// src/cli/commands/workspace.ts
|
|
7543
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
7544
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
7545
|
+
import { homedir as homedir9 } from "os";
|
|
7546
|
+
import { join as join16, resolve as resolve4 } from "path";
|
|
7547
|
+
import { confirm, input } from "@inquirer/prompts";
|
|
7548
|
+
import { Command as Command4 } from "commander";
|
|
7549
|
+
function runGit(cwd, args) {
|
|
7550
|
+
try {
|
|
7551
|
+
return execFileSync3("git", args, {
|
|
7552
|
+
cwd,
|
|
7553
|
+
stdio: "pipe",
|
|
7554
|
+
encoding: "utf8"
|
|
7555
|
+
}).trim();
|
|
7556
|
+
} catch (err) {
|
|
7557
|
+
throw new Error(`Git command failed: ${err.message}`);
|
|
7558
|
+
}
|
|
7559
|
+
}
|
|
7560
|
+
function ghRepoCreateArgs(repoName, repoVisibility) {
|
|
7561
|
+
return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
|
|
7562
|
+
}
|
|
7563
|
+
function gitRemoteArgs(githubUrl, hasOrigin) {
|
|
7564
|
+
return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
|
|
7565
|
+
}
|
|
7566
|
+
var workspaceCommand = new Command4("workspace").description(
|
|
7567
|
+
"Manage your ZAM learning workspace"
|
|
7568
|
+
);
|
|
7569
|
+
var WORKSPACE_KINDS = [
|
|
7570
|
+
"personal",
|
|
7571
|
+
"team",
|
|
7572
|
+
"family",
|
|
7573
|
+
"community",
|
|
7574
|
+
"organization",
|
|
7575
|
+
"custom"
|
|
7576
|
+
];
|
|
7577
|
+
var WORKSPACE_SOURCE_CONTROLS = [
|
|
7578
|
+
"github",
|
|
7579
|
+
"azure-devops",
|
|
7580
|
+
"git",
|
|
7581
|
+
"none"
|
|
7582
|
+
];
|
|
7583
|
+
function parseWorkspaceKind(value) {
|
|
7584
|
+
const kind = (value ?? "custom").toLowerCase();
|
|
7585
|
+
if (WORKSPACE_KINDS.includes(kind)) {
|
|
7586
|
+
return kind;
|
|
7587
|
+
}
|
|
7588
|
+
throw new Error(
|
|
7589
|
+
`Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
|
|
7590
|
+
);
|
|
7591
|
+
}
|
|
7592
|
+
function parseWorkspaceSourceControl(value) {
|
|
7593
|
+
if (!value) return void 0;
|
|
7594
|
+
const source = value.toLowerCase();
|
|
7595
|
+
if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
|
|
7596
|
+
return source;
|
|
7597
|
+
}
|
|
7598
|
+
throw new Error(
|
|
7599
|
+
`Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
|
|
7600
|
+
);
|
|
7601
|
+
}
|
|
7602
|
+
function parseScopes(value) {
|
|
7603
|
+
if (!value) return void 0;
|
|
7604
|
+
const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
7605
|
+
return scopes.length > 0 ? scopes : void 0;
|
|
7606
|
+
}
|
|
7607
|
+
function requireWorkspace(id) {
|
|
7608
|
+
const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
|
|
7609
|
+
if (!workspace) {
|
|
7610
|
+
throw new Error(
|
|
7611
|
+
`Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
|
|
7612
|
+
);
|
|
7613
|
+
}
|
|
7614
|
+
return workspace;
|
|
7615
|
+
}
|
|
7616
|
+
workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
|
|
7617
|
+
const workspaces = getConfiguredWorkspaces();
|
|
7618
|
+
if (opts.json) {
|
|
7619
|
+
console.log(JSON.stringify({ workspaces }, null, 2));
|
|
7620
|
+
return;
|
|
7621
|
+
}
|
|
7622
|
+
console.log("Configured ZAM workspaces:\n");
|
|
7623
|
+
if (workspaces.length === 0) {
|
|
7624
|
+
console.log(
|
|
7625
|
+
" (none) \u2014 add one: zam workspace add personal --path <dir>"
|
|
7626
|
+
);
|
|
7627
|
+
return;
|
|
7628
|
+
}
|
|
7629
|
+
for (const workspace of workspaces) {
|
|
7630
|
+
const label = workspace.label ? ` (${workspace.label})` : "";
|
|
7631
|
+
console.log(` ${workspace.id}${label}`);
|
|
7632
|
+
console.log(` kind: ${workspace.kind}`);
|
|
7633
|
+
console.log(` path: ${workspace.path}`);
|
|
7634
|
+
if (workspace.sourceControl) {
|
|
7635
|
+
console.log(` source: ${workspace.sourceControl}`);
|
|
7636
|
+
}
|
|
7637
|
+
if (workspace.knowledgeScopes?.length) {
|
|
7638
|
+
console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7641
|
+
});
|
|
7642
|
+
workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
|
|
7643
|
+
"--kind <kind>",
|
|
7644
|
+
`Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
|
|
7645
|
+
"custom"
|
|
7646
|
+
).option("--label <label>", "Human-readable label").option(
|
|
7647
|
+
"--source-control <provider>",
|
|
7648
|
+
`Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
|
|
7649
|
+
).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
|
|
7650
|
+
try {
|
|
7651
|
+
const path = resolve4(String(opts.path));
|
|
7652
|
+
if (!existsSync16(path)) {
|
|
7653
|
+
console.error(`Workspace path does not exist: ${path}`);
|
|
7654
|
+
process.exit(1);
|
|
7655
|
+
}
|
|
7656
|
+
const workspace = {
|
|
7657
|
+
id,
|
|
7658
|
+
kind: parseWorkspaceKind(opts.kind),
|
|
7659
|
+
path,
|
|
7660
|
+
...opts.label ? { label: opts.label } : {},
|
|
7661
|
+
...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
|
|
7662
|
+
...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
|
|
7663
|
+
...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
|
|
7664
|
+
};
|
|
7665
|
+
wireSkills(path, parseSetupAgents());
|
|
7666
|
+
upsertConfiguredWorkspace(workspace);
|
|
7667
|
+
console.log(`Registered and linked workspace "${id}" at ${path}.`);
|
|
7668
|
+
} catch (err) {
|
|
7669
|
+
console.error(`Error: ${err.message}`);
|
|
7670
|
+
process.exit(1);
|
|
7671
|
+
}
|
|
7672
|
+
});
|
|
7673
|
+
workspaceCommand.command("remove <id>").description("Unregister a ZAM workspace without deleting its files").action((id) => {
|
|
7674
|
+
const existing = getConfiguredWorkspaces().find((item) => item.id === id);
|
|
7675
|
+
if (!existing) {
|
|
7676
|
+
console.error(`Workspace "${id}" is not configured.`);
|
|
7677
|
+
process.exit(1);
|
|
7678
|
+
}
|
|
7679
|
+
removeConfiguredWorkspace(id);
|
|
7680
|
+
console.log(
|
|
7681
|
+
`Unregistered workspace "${id}". Files in ${existing.path} were not changed.`
|
|
7682
|
+
);
|
|
7683
|
+
});
|
|
7684
|
+
workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
|
|
7685
|
+
"--agents <list>",
|
|
7686
|
+
"comma-separated agents to wire: all, claude, copilot, codex, agent"
|
|
7687
|
+
).option(
|
|
7688
|
+
"--force",
|
|
7689
|
+
"overwrite existing skill files and refresh ZAM blocks",
|
|
7690
|
+
false
|
|
7691
|
+
).option(
|
|
7692
|
+
"--dry-run",
|
|
7693
|
+
"show what would be written without changing files",
|
|
7694
|
+
false
|
|
7695
|
+
).action((id, opts) => {
|
|
7696
|
+
try {
|
|
7697
|
+
const workspace = requireWorkspace(id);
|
|
7698
|
+
const agents = parseSetupAgents(opts.agents);
|
|
7699
|
+
console.log(
|
|
7700
|
+
`Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
|
|
7701
|
+
`
|
|
7702
|
+
);
|
|
7703
|
+
wireSkills(workspace.path, agents, {
|
|
7704
|
+
force: Boolean(opts.force),
|
|
7705
|
+
dryRun: Boolean(opts.dryRun)
|
|
7706
|
+
});
|
|
7707
|
+
if (agents.has("claude")) {
|
|
7708
|
+
writeClaudeMd(false, workspace.path, {
|
|
7709
|
+
dryRun: Boolean(opts.dryRun),
|
|
7710
|
+
updateExisting: true
|
|
7711
|
+
});
|
|
7712
|
+
}
|
|
7713
|
+
if (agents.has("codex") || agents.has("agent")) {
|
|
7714
|
+
writeAgentsMd(false, workspace.path, {
|
|
7715
|
+
dryRun: Boolean(opts.dryRun),
|
|
7716
|
+
updateExisting: true
|
|
7717
|
+
});
|
|
7718
|
+
}
|
|
7719
|
+
if (agents.has("copilot")) {
|
|
7720
|
+
writeCopilotInstructions(workspace.path, {
|
|
7721
|
+
dryRun: Boolean(opts.dryRun),
|
|
7722
|
+
updateExisting: true
|
|
7723
|
+
});
|
|
7724
|
+
}
|
|
7725
|
+
} catch (err) {
|
|
7726
|
+
console.error(`Error: ${err.message}`);
|
|
7727
|
+
process.exit(1);
|
|
7728
|
+
}
|
|
7729
|
+
});
|
|
7730
|
+
workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
|
|
7731
|
+
let db;
|
|
7732
|
+
let workspaceDir = "";
|
|
7733
|
+
try {
|
|
7734
|
+
db = await openDatabase();
|
|
7735
|
+
workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
|
|
7736
|
+
await db.close();
|
|
7737
|
+
} catch {
|
|
7738
|
+
await db?.close();
|
|
7739
|
+
}
|
|
7740
|
+
if (!workspaceDir) {
|
|
7741
|
+
console.error(
|
|
7742
|
+
"\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
|
|
7743
|
+
);
|
|
7744
|
+
process.exit(1);
|
|
7745
|
+
}
|
|
7746
|
+
if (!existsSync16(workspaceDir)) {
|
|
7747
|
+
console.error(
|
|
7748
|
+
`\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
|
|
7749
|
+
);
|
|
7750
|
+
process.exit(1);
|
|
7751
|
+
}
|
|
7752
|
+
console.log(`
|
|
7753
|
+
Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
|
|
7754
|
+
if (!hasCommand("git")) {
|
|
7755
|
+
console.error(
|
|
7756
|
+
"\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
|
|
7757
|
+
);
|
|
7758
|
+
process.exit(1);
|
|
7759
|
+
}
|
|
7760
|
+
const gitignorePath = join16(workspaceDir, ".gitignore");
|
|
7761
|
+
if (!existsSync16(gitignorePath)) {
|
|
7762
|
+
writeFileSync8(
|
|
7763
|
+
gitignorePath,
|
|
7764
|
+
"node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
|
|
7765
|
+
"utf8"
|
|
7766
|
+
);
|
|
7767
|
+
}
|
|
7768
|
+
const hasGitRepo = existsSync16(join16(workspaceDir, ".git"));
|
|
7769
|
+
if (!hasGitRepo) {
|
|
7770
|
+
console.log("Initializing local Git repository...");
|
|
7771
|
+
runGit(workspaceDir, ["init", "-b", "main"]);
|
|
7772
|
+
runGit(workspaceDir, ["add", "."]);
|
|
7773
|
+
runGit(workspaceDir, [
|
|
7774
|
+
"commit",
|
|
7775
|
+
"-m",
|
|
7776
|
+
"chore: initial workspace sandbox bootstrap"
|
|
7777
|
+
]);
|
|
7778
|
+
console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
|
|
7779
|
+
} else {
|
|
7780
|
+
console.log("Git repository is already initialized.");
|
|
7781
|
+
}
|
|
7782
|
+
const repoName = await input({
|
|
7783
|
+
message: "Choose a name for your GitHub repository:",
|
|
7784
|
+
default: "zam-personal"
|
|
7785
|
+
});
|
|
7786
|
+
const isPrivate = await confirm({
|
|
7787
|
+
message: "Should the repository be private?",
|
|
7788
|
+
default: true
|
|
7789
|
+
});
|
|
7790
|
+
const repoVisibility = isPrivate ? "--private" : "--public";
|
|
7791
|
+
if (hasCommand("gh")) {
|
|
7792
|
+
console.log("GitHub CLI detected! Automating repository creation...");
|
|
7793
|
+
const proceedGh = await confirm({
|
|
7794
|
+
message: "Would you like ZAM to create the repository using the GitHub CLI?",
|
|
7795
|
+
default: true
|
|
7796
|
+
});
|
|
7797
|
+
if (proceedGh) {
|
|
7798
|
+
try {
|
|
7799
|
+
console.log(`Creating GitHub repository ${repoName}...`);
|
|
7800
|
+
execFileSync3("gh", ghRepoCreateArgs(repoName, repoVisibility), {
|
|
7801
|
+
cwd: workspaceDir,
|
|
7802
|
+
stdio: "inherit"
|
|
7803
|
+
});
|
|
7804
|
+
console.log(
|
|
7805
|
+
"\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
|
|
7806
|
+
);
|
|
7807
|
+
process.exit(0);
|
|
7808
|
+
} catch (err) {
|
|
7809
|
+
console.warn(
|
|
7810
|
+
`\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
|
|
7811
|
+
);
|
|
7812
|
+
}
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7815
|
+
console.log(
|
|
7816
|
+
"\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
|
|
7817
|
+
);
|
|
7818
|
+
console.log(" 1. Go to https://github.com/new");
|
|
7819
|
+
console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
|
|
7820
|
+
console.log(
|
|
7821
|
+
` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
|
|
7822
|
+
);
|
|
7823
|
+
console.log(
|
|
7824
|
+
" 4. Do NOT initialize it with README, .gitignore, or license"
|
|
7825
|
+
);
|
|
7826
|
+
console.log(" 5. Click 'Create repository'\n");
|
|
7827
|
+
const githubUrl = await input({
|
|
7828
|
+
message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
|
|
7829
|
+
});
|
|
7830
|
+
if (githubUrl) {
|
|
7831
|
+
try {
|
|
7832
|
+
console.log("Linking remote repository and pushing...");
|
|
7833
|
+
let hasOrigin = false;
|
|
7834
|
+
try {
|
|
7835
|
+
runGit(workspaceDir, ["remote", "get-url", "origin"]);
|
|
7836
|
+
hasOrigin = true;
|
|
7837
|
+
} catch {
|
|
7838
|
+
}
|
|
7839
|
+
runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
|
|
7840
|
+
runGit(workspaceDir, ["push", "-u", "origin", "main"]);
|
|
7841
|
+
console.log(
|
|
7842
|
+
"\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
|
|
7843
|
+
);
|
|
7844
|
+
} catch (err) {
|
|
7845
|
+
console.error(
|
|
7846
|
+
`\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
|
|
7847
|
+
);
|
|
7848
|
+
console.log(
|
|
7849
|
+
"You can push manually later using: git push -u origin main"
|
|
7850
|
+
);
|
|
7851
|
+
}
|
|
7852
|
+
}
|
|
7853
|
+
});
|
|
7854
|
+
async function backupDatabaseTo(db, targetDir) {
|
|
7855
|
+
const backupDir = join16(targetDir, "zam-backups");
|
|
7856
|
+
mkdirSync9(backupDir, { recursive: true });
|
|
7857
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7858
|
+
const dest = join16(backupDir, `zam-${stamp}.db`);
|
|
7859
|
+
await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
7860
|
+
return dest;
|
|
7861
|
+
}
|
|
7862
|
+
workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
|
|
7863
|
+
const dir = join16(homedir9(), ".zam");
|
|
7864
|
+
console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
|
|
7865
|
+
});
|
|
7866
|
+
workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
|
|
7867
|
+
"--dir <path>",
|
|
7868
|
+
"Target directory (default: workspace dir, else ~/Documents/zam)"
|
|
7869
|
+
).option("--json", "Output as JSON").action(async (opts) => {
|
|
7870
|
+
const target = getDatabaseTargetInfo();
|
|
7871
|
+
if (target.kind !== "local") {
|
|
7872
|
+
const reason = `The database is ${target.kind} (${target.location}); file backup applies only to a local database \u2014 your Turso remote is already the cloud backup.`;
|
|
7873
|
+
if (opts.json) {
|
|
7874
|
+
console.log(JSON.stringify({ ok: false, reason }));
|
|
7875
|
+
return;
|
|
7876
|
+
}
|
|
7877
|
+
console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
|
|
7878
|
+
process.exit(1);
|
|
7879
|
+
}
|
|
7880
|
+
let db;
|
|
7881
|
+
try {
|
|
7882
|
+
db = await openDatabase();
|
|
7883
|
+
const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir9(), "Documents", "zam");
|
|
7884
|
+
const dest = await backupDatabaseTo(db, workspaceDir);
|
|
7885
|
+
if (opts.json) {
|
|
7886
|
+
console.log(JSON.stringify({ ok: true, path: dest }));
|
|
7887
|
+
} else {
|
|
7888
|
+
console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
|
|
7889
|
+
}
|
|
7890
|
+
} catch (err) {
|
|
7891
|
+
const reason = err.message;
|
|
7892
|
+
if (opts.json) {
|
|
7893
|
+
console.log(JSON.stringify({ ok: false, reason }));
|
|
7894
|
+
} else {
|
|
7895
|
+
console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
|
|
7896
|
+
}
|
|
7897
|
+
process.exit(1);
|
|
7898
|
+
} finally {
|
|
7899
|
+
await db?.close();
|
|
7900
|
+
}
|
|
7901
|
+
});
|
|
7902
|
+
|
|
7903
|
+
// src/cli/commands/bridge.ts
|
|
7904
|
+
var isServeMode = false;
|
|
7905
|
+
function jsonOut2(data) {
|
|
7906
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7907
|
+
}
|
|
7908
|
+
function jsonError(message) {
|
|
7909
|
+
if (isServeMode) {
|
|
7910
|
+
throw new Error(JSON.stringify({ error: message }));
|
|
7911
|
+
}
|
|
7912
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
7913
|
+
process.exit(1);
|
|
7914
|
+
}
|
|
7915
|
+
function parseNonNegativeIntegerOption(name, value) {
|
|
7916
|
+
const parsed = Number(value);
|
|
7917
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
7918
|
+
jsonError(`${name} must be a non-negative integer`);
|
|
7919
|
+
}
|
|
7920
|
+
return parsed;
|
|
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
|
+
}
|
|
7928
|
+
async function withDb2(fn) {
|
|
7929
|
+
await withDb(fn, jsonError);
|
|
7930
|
+
}
|
|
7931
|
+
async function getReviewTarget2(db, cardId, userId) {
|
|
7932
|
+
const target = await db.prepare(
|
|
7933
|
+
`SELECT c.id AS card_id, c.token_id, c.user_id, t.slug
|
|
7934
|
+
FROM cards c
|
|
7935
|
+
JOIN tokens t ON t.id = c.token_id
|
|
7936
|
+
WHERE c.id = ?`
|
|
7937
|
+
).get(cardId);
|
|
7938
|
+
if (!target) {
|
|
7939
|
+
jsonError(`Card not found: ${cardId}`);
|
|
7940
|
+
}
|
|
7941
|
+
if (target.user_id !== userId) {
|
|
7369
7942
|
jsonError(`Card ${cardId} does not belong to user ${userId}`);
|
|
7370
7943
|
}
|
|
7371
7944
|
return target;
|
|
@@ -7389,7 +7962,7 @@ function parseTokenUpdates(opts) {
|
|
|
7389
7962
|
}
|
|
7390
7963
|
return updates;
|
|
7391
7964
|
}
|
|
7392
|
-
var bridgeCommand = new
|
|
7965
|
+
var bridgeCommand = new Command5("bridge").description(
|
|
7393
7966
|
"Machine-readable JSON protocol for AI integration"
|
|
7394
7967
|
);
|
|
7395
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) => {
|
|
@@ -7447,6 +8020,37 @@ bridgeCommand.command("workspace-info").description("Report the workspace dir, i
|
|
|
7447
8020
|
});
|
|
7448
8021
|
});
|
|
7449
8022
|
});
|
|
8023
|
+
function sameWorkspacePath(left, right) {
|
|
8024
|
+
const normalize = (value) => {
|
|
8025
|
+
const path = resolve5(value);
|
|
8026
|
+
return process.platform === "win32" ? path.toLowerCase() : path;
|
|
8027
|
+
};
|
|
8028
|
+
return normalize(left) === normalize(right);
|
|
8029
|
+
}
|
|
8030
|
+
async function ensureDesktopWorkspace(db) {
|
|
8031
|
+
const configured = getConfiguredWorkspaces();
|
|
8032
|
+
const savedWorkspaceDir = await getSetting(db, "personal.workspace_dir");
|
|
8033
|
+
const workspaceDir = savedWorkspaceDir || configured.find((workspace) => workspace.kind === "personal")?.path || join17(homedir10(), "Documents", "zam");
|
|
8034
|
+
mkdirSync10(workspaceDir, { recursive: true });
|
|
8035
|
+
if (!savedWorkspaceDir) {
|
|
8036
|
+
await setSetting(db, "personal.workspace_dir", workspaceDir);
|
|
8037
|
+
}
|
|
8038
|
+
if (!configured.some(
|
|
8039
|
+
(workspace) => sameWorkspacePath(workspace.path, workspaceDir)
|
|
8040
|
+
)) {
|
|
8041
|
+
const id = configured.some((workspace) => workspace.id === "personal") ? workspaceIdFromPath(workspaceDir) : "personal";
|
|
8042
|
+
upsertConfiguredWorkspace({
|
|
8043
|
+
id,
|
|
8044
|
+
label: basename5(workspaceDir) || "ZAM",
|
|
8045
|
+
kind: "personal",
|
|
8046
|
+
path: workspaceDir
|
|
8047
|
+
});
|
|
8048
|
+
}
|
|
8049
|
+
const skillLinks = getConfiguredWorkspaces().flatMap(
|
|
8050
|
+
(workspace) => existsSync17(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
|
|
8051
|
+
);
|
|
8052
|
+
return { workspaceDir, skillLinks };
|
|
8053
|
+
}
|
|
7450
8054
|
bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
|
|
7451
8055
|
await withDb2(async (db) => {
|
|
7452
8056
|
const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
|
|
@@ -7487,6 +8091,7 @@ bridgeCommand.command("workspace-add").description("Register an existing directo
|
|
|
7487
8091
|
kind: parseBridgeWorkspaceKind(opts.kind),
|
|
7488
8092
|
path
|
|
7489
8093
|
};
|
|
8094
|
+
const skillLinks = wireSkills(path, parseSetupAgents(), { quiet: true });
|
|
7490
8095
|
await withDb2(async (db) => {
|
|
7491
8096
|
await setSetting(db, "personal.workspace_dir", path);
|
|
7492
8097
|
upsertConfiguredWorkspace(workspace);
|
|
@@ -7494,7 +8099,39 @@ bridgeCommand.command("workspace-add").description("Register an existing directo
|
|
|
7494
8099
|
ok: true,
|
|
7495
8100
|
workspace,
|
|
7496
8101
|
workspaces: getConfiguredWorkspaces(),
|
|
7497
|
-
workspaceDir: path
|
|
8102
|
+
workspaceDir: path,
|
|
8103
|
+
skillLinks
|
|
8104
|
+
});
|
|
8105
|
+
});
|
|
8106
|
+
});
|
|
8107
|
+
bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspace without deleting its files (JSON)").requiredOption("--id <id>", "Workspace id").action(async (opts) => {
|
|
8108
|
+
const id = String(opts.id ?? "").trim();
|
|
8109
|
+
if (!id) jsonError("A non-empty --id is required");
|
|
8110
|
+
const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
|
|
8111
|
+
if (!workspace) jsonError(`Workspace "${id}" is not configured`);
|
|
8112
|
+
await withDb2(async (db) => {
|
|
8113
|
+
const activeWorkspaceDir = await getSetting(db, "personal.workspace_dir") || null;
|
|
8114
|
+
const remaining = removeConfiguredWorkspace(id);
|
|
8115
|
+
let workspaceDir = activeWorkspaceDir;
|
|
8116
|
+
let skillLinks = [];
|
|
8117
|
+
if (activeWorkspaceDir && sameWorkspacePath(activeWorkspaceDir, workspace.path)) {
|
|
8118
|
+
if (remaining.length > 0) {
|
|
8119
|
+
workspaceDir = remaining[0].path;
|
|
8120
|
+
await setSetting(db, "personal.workspace_dir", workspaceDir);
|
|
8121
|
+
} else {
|
|
8122
|
+
workspaceDir = join17(homedir10(), "Documents", "zam");
|
|
8123
|
+
await setSetting(db, "personal.workspace_dir", workspaceDir);
|
|
8124
|
+
const ensured = await ensureDesktopWorkspace(db);
|
|
8125
|
+
workspaceDir = ensured.workspaceDir;
|
|
8126
|
+
skillLinks = ensured.skillLinks;
|
|
8127
|
+
}
|
|
8128
|
+
}
|
|
8129
|
+
jsonOut2({
|
|
8130
|
+
ok: true,
|
|
8131
|
+
removed: workspace,
|
|
8132
|
+
workspaces: getConfiguredWorkspaces(),
|
|
8133
|
+
workspaceDir,
|
|
8134
|
+
skillLinks
|
|
7498
8135
|
});
|
|
7499
8136
|
});
|
|
7500
8137
|
});
|
|
@@ -7502,9 +8139,11 @@ bridgeCommand.command("set-workspace-dir").description("Set the personal workspa
|
|
|
7502
8139
|
const raw = String(opts.dir ?? "").trim();
|
|
7503
8140
|
if (!raw) jsonError("A non-empty --dir is required");
|
|
7504
8141
|
const dir = resolve5(raw);
|
|
8142
|
+
if (!existsSync17(dir)) jsonError(`Workspace path does not exist: ${dir}`);
|
|
8143
|
+
const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
|
|
7505
8144
|
await withDb2(async (db) => {
|
|
7506
8145
|
await setSetting(db, "personal.workspace_dir", dir);
|
|
7507
|
-
jsonOut2({ ok: true, workspaceDir: dir });
|
|
8146
|
+
jsonOut2({ ok: true, workspaceDir: dir, skillLinks });
|
|
7508
8147
|
});
|
|
7509
8148
|
});
|
|
7510
8149
|
bridgeCommand.command("agent-list").description("List agent harnesses with detection state + the default (JSON)").action(async () => {
|
|
@@ -7593,6 +8232,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
7593
8232
|
const item = queue.items[0];
|
|
7594
8233
|
const isLlmEnabled = await getSetting(db, "llm.enabled") === "true";
|
|
7595
8234
|
let resolvedQuestion = item.question;
|
|
8235
|
+
let questionSource = "original";
|
|
8236
|
+
let questionModel;
|
|
7596
8237
|
if (isLlmEnabled) {
|
|
7597
8238
|
try {
|
|
7598
8239
|
const healed = await ensureHighQualityQuestion(db, {
|
|
@@ -7605,7 +8246,9 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
7605
8246
|
question: item.question
|
|
7606
8247
|
});
|
|
7607
8248
|
if (healed) {
|
|
7608
|
-
resolvedQuestion = healed;
|
|
8249
|
+
resolvedQuestion = healed.question;
|
|
8250
|
+
questionSource = healed.source;
|
|
8251
|
+
questionModel = healed.model;
|
|
7609
8252
|
}
|
|
7610
8253
|
} catch {
|
|
7611
8254
|
}
|
|
@@ -7634,6 +8277,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
7634
8277
|
hasReview: true,
|
|
7635
8278
|
card: item,
|
|
7636
8279
|
prompt,
|
|
8280
|
+
questionSource,
|
|
8281
|
+
questionModel: questionModel ?? null,
|
|
7637
8282
|
resolvedContext,
|
|
7638
8283
|
queueSize: fullQueue.items.length
|
|
7639
8284
|
});
|
|
@@ -7932,7 +8577,7 @@ bridgeCommand.command("discover-skills").description(
|
|
|
7932
8577
|
const monitorDir = join17(homedir10(), ".zam", "monitor");
|
|
7933
8578
|
let files;
|
|
7934
8579
|
try {
|
|
7935
|
-
files =
|
|
8580
|
+
files = readdirSync3(monitorDir).filter((f) => f.endsWith(".jsonl"));
|
|
7936
8581
|
} catch {
|
|
7937
8582
|
jsonOut2({ proposals: [], message: "No monitor logs found." });
|
|
7938
8583
|
return;
|
|
@@ -8455,7 +9100,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
|
|
|
8455
9100
|
if (!post.allowed) {
|
|
8456
9101
|
if (!opts.output) {
|
|
8457
9102
|
try {
|
|
8458
|
-
|
|
9103
|
+
rmSync3(outputPath, { force: true });
|
|
8459
9104
|
} catch {
|
|
8460
9105
|
}
|
|
8461
9106
|
}
|
|
@@ -8594,7 +9239,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
8594
9239
|
}
|
|
8595
9240
|
const sessionId = opts.session;
|
|
8596
9241
|
const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
|
|
8597
|
-
const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync:
|
|
9242
|
+
const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
|
|
8598
9243
|
if (!existsSync25(statePath)) {
|
|
8599
9244
|
jsonOut2({
|
|
8600
9245
|
sessionId,
|
|
@@ -8619,7 +9264,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
8619
9264
|
};
|
|
8620
9265
|
let attempts = 0;
|
|
8621
9266
|
while (isProcessRunning(pid) && attempts < 20) {
|
|
8622
|
-
await new Promise((
|
|
9267
|
+
await new Promise((resolve9) => setTimeout(resolve9, 250));
|
|
8623
9268
|
attempts++;
|
|
8624
9269
|
}
|
|
8625
9270
|
if (isProcessRunning(pid)) {
|
|
@@ -8629,7 +9274,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
8629
9274
|
}
|
|
8630
9275
|
}
|
|
8631
9276
|
try {
|
|
8632
|
-
|
|
9277
|
+
rmSync4(statePath, { force: true });
|
|
8633
9278
|
} catch {
|
|
8634
9279
|
}
|
|
8635
9280
|
if (!existsSync25(outputPath)) {
|
|
@@ -8658,7 +9303,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
8658
9303
|
return;
|
|
8659
9304
|
}
|
|
8660
9305
|
try {
|
|
8661
|
-
|
|
9306
|
+
rmSync4(outputPath, { force: true });
|
|
8662
9307
|
} catch {
|
|
8663
9308
|
}
|
|
8664
9309
|
jsonOut2({
|
|
@@ -8725,12 +9370,194 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
|
|
|
8725
9370
|
});
|
|
8726
9371
|
bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
|
|
8727
9372
|
await withDb2(async (db) => {
|
|
8728
|
-
const [recall, vision, text] = await Promise.all([
|
|
8729
|
-
getProviderRoleStatus(db, "recall"),
|
|
8730
|
-
getProviderRoleStatus(db, "vision"),
|
|
8731
|
-
getProviderRoleStatus(db, "text")
|
|
8732
|
-
]);
|
|
8733
|
-
jsonOut2({ roles: { recall, vision, text } });
|
|
9373
|
+
const [recall, vision, text] = await Promise.all([
|
|
9374
|
+
getProviderRoleStatus(db, "recall"),
|
|
9375
|
+
getProviderRoleStatus(db, "vision"),
|
|
9376
|
+
getProviderRoleStatus(db, "text")
|
|
9377
|
+
]);
|
|
9378
|
+
jsonOut2({ roles: { recall, vision, text } });
|
|
9379
|
+
});
|
|
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 });
|
|
8734
9561
|
});
|
|
8735
9562
|
});
|
|
8736
9563
|
bridgeCommand.command("check-vision").description(
|
|
@@ -8779,7 +9606,10 @@ bridgeCommand.command("translate-question").description("Translate a question dy
|
|
|
8779
9606
|
});
|
|
8780
9607
|
bridgeCommand.command("evaluate-answer").description(
|
|
8781
9608
|
"Evaluate the learner's active-recall answer using the local LLM (JSON)"
|
|
8782
|
-
).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) => {
|
|
8783
9613
|
await withDb2(async (db) => {
|
|
8784
9614
|
const isEnabled = await getSetting(db, "llm.enabled") === "true";
|
|
8785
9615
|
if (!isEnabled) {
|
|
@@ -8790,8 +9620,8 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
8790
9620
|
});
|
|
8791
9621
|
return;
|
|
8792
9622
|
}
|
|
8793
|
-
let resolvedContextContent = null;
|
|
8794
|
-
if (opts.sourceLink) {
|
|
9623
|
+
let resolvedContextContent = opts.sourceContent ?? null;
|
|
9624
|
+
if (resolvedContextContent == null && opts.sourceLink) {
|
|
8795
9625
|
try {
|
|
8796
9626
|
const resolved = await resolveReviewContext(opts.sourceLink);
|
|
8797
9627
|
resolvedContextContent = resolved?.content ?? null;
|
|
@@ -8799,7 +9629,7 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
8799
9629
|
}
|
|
8800
9630
|
}
|
|
8801
9631
|
try {
|
|
8802
|
-
const
|
|
9632
|
+
const result = await evaluateAnswerViaLLM(db, {
|
|
8803
9633
|
slug: opts.slug,
|
|
8804
9634
|
concept: opts.concept,
|
|
8805
9635
|
domain: opts.domain,
|
|
@@ -8809,7 +9639,11 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
8809
9639
|
userAnswer: opts.userAnswer,
|
|
8810
9640
|
sourceLinkContent: resolvedContextContent
|
|
8811
9641
|
});
|
|
8812
|
-
jsonOut2({
|
|
9642
|
+
jsonOut2({
|
|
9643
|
+
success: true,
|
|
9644
|
+
evaluation: result.text,
|
|
9645
|
+
evaluationModel: result.model
|
|
9646
|
+
});
|
|
8813
9647
|
} catch (err) {
|
|
8814
9648
|
jsonOut2({
|
|
8815
9649
|
success: false,
|
|
@@ -8823,10 +9657,13 @@ bridgeCommand.command("desktop-bootstrap").description("Initialize first-run des
|
|
|
8823
9657
|
await withDb2(async (db) => {
|
|
8824
9658
|
const userId = await ensureDefaultUser(db, opts.user);
|
|
8825
9659
|
const { enabled, url, model, locale } = await getLlmConfig(db);
|
|
9660
|
+
const { workspaceDir, skillLinks } = await ensureDesktopWorkspace(db);
|
|
8826
9661
|
jsonOut2({
|
|
8827
9662
|
userId,
|
|
8828
9663
|
locale,
|
|
8829
|
-
llm: { enabled, url, model }
|
|
9664
|
+
llm: { enabled, url, model },
|
|
9665
|
+
workspaceDir,
|
|
9666
|
+
skillLinks
|
|
8830
9667
|
});
|
|
8831
9668
|
});
|
|
8832
9669
|
});
|
|
@@ -9045,8 +9882,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
9045
9882
|
});
|
|
9046
9883
|
|
|
9047
9884
|
// src/cli/commands/card.ts
|
|
9048
|
-
import { Command as
|
|
9049
|
-
var cardCommand = new
|
|
9885
|
+
import { Command as Command6 } from "commander";
|
|
9886
|
+
var cardCommand = new Command6("card").description(
|
|
9050
9887
|
"Manage spaced-repetition cards"
|
|
9051
9888
|
);
|
|
9052
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) => {
|
|
@@ -9244,9 +10081,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
|
|
|
9244
10081
|
});
|
|
9245
10082
|
|
|
9246
10083
|
// src/cli/commands/connector.ts
|
|
9247
|
-
import { input as input2, password } from "@inquirer/prompts";
|
|
9248
|
-
import { Command as
|
|
9249
|
-
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(
|
|
9250
10087
|
"Manage external service connectors"
|
|
9251
10088
|
);
|
|
9252
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(
|
|
@@ -9271,7 +10108,7 @@ connectorCommand.command("setup").description("Configure a connector").argument(
|
|
|
9271
10108
|
const project = await input2({
|
|
9272
10109
|
message: "Project name:"
|
|
9273
10110
|
});
|
|
9274
|
-
const pat = await
|
|
10111
|
+
const pat = await password2({
|
|
9275
10112
|
message: "Personal Access Token:"
|
|
9276
10113
|
});
|
|
9277
10114
|
if (!orgUrl || !project || !pat) {
|
|
@@ -9362,7 +10199,7 @@ async function setupTurso(urlArg, tokenArg, mode) {
|
|
|
9362
10199
|
const url = urlArg ?? await input2({
|
|
9363
10200
|
message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
|
|
9364
10201
|
});
|
|
9365
|
-
const token = tokenArg ?? await
|
|
10202
|
+
const token = tokenArg ?? await password2({
|
|
9366
10203
|
message: "Auth token:"
|
|
9367
10204
|
});
|
|
9368
10205
|
if (!url || !token) {
|
|
@@ -9391,7 +10228,7 @@ async function setupTurso(urlArg, tokenArg, mode) {
|
|
|
9391
10228
|
import { execSync as execSync5 } from "child_process";
|
|
9392
10229
|
import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync9 } from "fs";
|
|
9393
10230
|
import { join as join18 } from "path";
|
|
9394
|
-
import { Command as
|
|
10231
|
+
import { Command as Command8 } from "commander";
|
|
9395
10232
|
function installHook2() {
|
|
9396
10233
|
const gitDir = join18(process.cwd(), ".git");
|
|
9397
10234
|
if (!existsSync18(gitDir)) {
|
|
@@ -9421,7 +10258,7 @@ zam git-sync --commit HEAD --quiet
|
|
|
9421
10258
|
process.exit(1);
|
|
9422
10259
|
}
|
|
9423
10260
|
}
|
|
9424
|
-
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) => {
|
|
9425
10262
|
if (opts.install) {
|
|
9426
10263
|
installHook2();
|
|
9427
10264
|
return;
|
|
@@ -9510,10 +10347,10 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
|
|
|
9510
10347
|
});
|
|
9511
10348
|
|
|
9512
10349
|
// src/cli/commands/goal.ts
|
|
9513
|
-
import { existsSync as existsSync19, mkdirSync as
|
|
10350
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
|
|
9514
10351
|
import { resolve as resolve6 } from "path";
|
|
9515
10352
|
import { input as input3 } from "@inquirer/prompts";
|
|
9516
|
-
import { Command as
|
|
10353
|
+
import { Command as Command9 } from "commander";
|
|
9517
10354
|
async function resolveGoalsDir() {
|
|
9518
10355
|
let goalsDir;
|
|
9519
10356
|
let db;
|
|
@@ -9526,7 +10363,7 @@ async function resolveGoalsDir() {
|
|
|
9526
10363
|
}
|
|
9527
10364
|
return goalsDir ? resolve6(goalsDir) : resolve6("goals");
|
|
9528
10365
|
}
|
|
9529
|
-
var goalCommand = new
|
|
10366
|
+
var goalCommand = new Command9("goal").description(
|
|
9530
10367
|
"Manage learning goals (markdown files)"
|
|
9531
10368
|
);
|
|
9532
10369
|
goalCommand.command("list").description("List all goals").option(
|
|
@@ -9636,7 +10473,7 @@ ${"\u2500".repeat(50)}`);
|
|
|
9636
10473
|
goalCommand.command("create").description("Create a new goal").option("--slug <slug>", "Goal slug (used as filename)").option("--title <title>", "Goal title").option("--parent <slug>", "Parent goal slug").option("--description <text>", "Goal description").option("--json", "Output as JSON").action(async (opts) => {
|
|
9637
10474
|
const goalsDir = await resolveGoalsDir();
|
|
9638
10475
|
if (!existsSync19(goalsDir)) {
|
|
9639
|
-
|
|
10476
|
+
mkdirSync11(goalsDir, { recursive: true });
|
|
9640
10477
|
}
|
|
9641
10478
|
let slug = opts.slug;
|
|
9642
10479
|
let title = opts.title;
|
|
@@ -9695,19 +10532,19 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
|
|
|
9695
10532
|
});
|
|
9696
10533
|
|
|
9697
10534
|
// src/cli/commands/init.ts
|
|
9698
|
-
import { existsSync as existsSync20, mkdirSync as
|
|
10535
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
|
|
9699
10536
|
import { homedir as homedir11 } from "os";
|
|
9700
|
-
import { join as join19 } from "path";
|
|
10537
|
+
import { join as join19, resolve as resolve7 } from "path";
|
|
9701
10538
|
import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
|
|
9702
|
-
import { Command as
|
|
10539
|
+
import { Command as Command10 } from "commander";
|
|
9703
10540
|
var HOME2 = homedir11();
|
|
9704
10541
|
function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
|
|
9705
10542
|
console.log(`${color}${char.repeat(len)}\x1B[0m`);
|
|
9706
10543
|
}
|
|
9707
10544
|
function bootstrapSandboxWorkspace(workspaceDir) {
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
10545
|
+
mkdirSync12(join19(workspaceDir, "beliefs"), { recursive: true });
|
|
10546
|
+
mkdirSync12(join19(workspaceDir, "goals"), { recursive: true });
|
|
10547
|
+
mkdirSync12(join19(workspaceDir, "skills"), { recursive: true });
|
|
9711
10548
|
const worldviewFile = join19(workspaceDir, "beliefs", "worldview.md");
|
|
9712
10549
|
if (!existsSync20(worldviewFile)) {
|
|
9713
10550
|
writeFileSync10(
|
|
@@ -9736,7 +10573,7 @@ Here, I declare the core concepts and principles I want to master.
|
|
|
9736
10573
|
);
|
|
9737
10574
|
}
|
|
9738
10575
|
}
|
|
9739
|
-
var initCommand = new
|
|
10576
|
+
var initCommand = new Command10("init").description("Launch the guided interactive onboarding wizard").action(async () => {
|
|
9740
10577
|
printLine();
|
|
9741
10578
|
console.log(
|
|
9742
10579
|
"\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
|
|
@@ -9747,12 +10584,21 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
|
|
|
9747
10584
|
printLine();
|
|
9748
10585
|
console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
|
|
9749
10586
|
const defaultWorkspace = join19(HOME2, "Documents", "zam");
|
|
9750
|
-
const workspacePath =
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
10587
|
+
const workspacePath = resolve7(
|
|
10588
|
+
await input4({
|
|
10589
|
+
message: "Choose your ZAM workspace directory:",
|
|
10590
|
+
default: defaultWorkspace
|
|
10591
|
+
})
|
|
10592
|
+
);
|
|
9754
10593
|
try {
|
|
9755
10594
|
bootstrapSandboxWorkspace(workspacePath);
|
|
10595
|
+
wireSkills(workspacePath, parseSetupAgents());
|
|
10596
|
+
upsertConfiguredWorkspace({
|
|
10597
|
+
id: "personal",
|
|
10598
|
+
label: "Personal",
|
|
10599
|
+
kind: "personal",
|
|
10600
|
+
path: workspacePath
|
|
10601
|
+
});
|
|
9756
10602
|
console.log(
|
|
9757
10603
|
`\x1B[32m\u2713 Local Sandbox created at: ${workspacePath}\x1B[0m`
|
|
9758
10604
|
);
|
|
@@ -9903,7 +10749,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
|
|
|
9903
10749
|
|
|
9904
10750
|
// src/cli/commands/learn.ts
|
|
9905
10751
|
import { input as input6 } from "@inquirer/prompts";
|
|
9906
|
-
import { Command as
|
|
10752
|
+
import { Command as Command11 } from "commander";
|
|
9907
10753
|
|
|
9908
10754
|
// src/cli/learn-format.ts
|
|
9909
10755
|
var BLOOM_VERBS3 = {
|
|
@@ -10241,7 +11087,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
|
|
|
10241
11087
|
function isExitPrompt(err) {
|
|
10242
11088
|
return err instanceof Error && err.name === "ExitPromptError";
|
|
10243
11089
|
}
|
|
10244
|
-
var learnCommand = new
|
|
11090
|
+
var learnCommand = new Command11("learn").description(
|
|
10245
11091
|
"Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
|
|
10246
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) => {
|
|
10247
11093
|
let db;
|
|
@@ -10297,7 +11143,7 @@ ${t(locale, "welcome", { count: queue.items.length })}`);
|
|
|
10297
11143
|
question: item.question
|
|
10298
11144
|
});
|
|
10299
11145
|
if (healed) {
|
|
10300
|
-
resolvedQuestion = healed;
|
|
11146
|
+
resolvedQuestion = healed.question;
|
|
10301
11147
|
}
|
|
10302
11148
|
} catch {
|
|
10303
11149
|
}
|
|
@@ -10362,7 +11208,8 @@ ${"\u2500".repeat(50)}`);
|
|
|
10362
11208
|
`
|
|
10363
11209
|
${t(locale, "feedback_title", { line: "\u2500".repeat(34) })}`
|
|
10364
11210
|
);
|
|
10365
|
-
|
|
11211
|
+
console.log(` \x1B[2m[${evaluation.model}]\x1B[0m`);
|
|
11212
|
+
for (const line of evaluation.text.split("\n")) {
|
|
10366
11213
|
console.log(` ${line}`);
|
|
10367
11214
|
}
|
|
10368
11215
|
} catch (err) {
|
|
@@ -10485,8 +11332,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
|
|
|
10485
11332
|
});
|
|
10486
11333
|
|
|
10487
11334
|
// src/cli/commands/monitor.ts
|
|
10488
|
-
import { Command as
|
|
10489
|
-
var monitorCommand = new
|
|
11335
|
+
import { Command as Command12 } from "commander";
|
|
11336
|
+
var monitorCommand = new Command12("monitor").description(
|
|
10490
11337
|
"Shell observation for real-time task monitoring"
|
|
10491
11338
|
);
|
|
10492
11339
|
monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
|
|
@@ -10549,593 +11396,244 @@ monitorCommand.command("stop").description("Output shell code to remove monitori
|
|
|
10549
11396
|
session_id: opts.session
|
|
10550
11397
|
};
|
|
10551
11398
|
writeMonitorEvent(opts.session, meta);
|
|
10552
|
-
}
|
|
10553
|
-
let shell;
|
|
10554
|
-
try {
|
|
10555
|
-
shell = normalizeShell(opts.shell);
|
|
10556
|
-
} catch (err) {
|
|
10557
|
-
console.error(`# Error: ${err.message}`);
|
|
10558
|
-
process.exit(1);
|
|
10559
|
-
}
|
|
10560
|
-
if (shell === "bash") {
|
|
10561
|
-
console.log(generateBashUnhooks());
|
|
10562
|
-
} else if (isPowerShellShell(shell)) {
|
|
10563
|
-
console.log(generatePowerShellUnhooks());
|
|
10564
|
-
} else {
|
|
10565
|
-
console.log(generateZshUnhooks());
|
|
10566
|
-
}
|
|
10567
|
-
});
|
|
10568
|
-
monitorCommand.command("status").description("Show monitoring status for a session").requiredOption("--session <id>", "Session ID").option("--json", "Output as JSON").action((opts) => {
|
|
10569
|
-
const stats = getMonitorLogStats(opts.session);
|
|
10570
|
-
if (!stats.exists) {
|
|
10571
|
-
if (opts.json) {
|
|
10572
|
-
console.log(JSON.stringify({ exists: false }));
|
|
10573
|
-
} else {
|
|
10574
|
-
console.log(`No monitor log found for session ${opts.session}`);
|
|
10575
|
-
}
|
|
10576
|
-
return;
|
|
10577
|
-
}
|
|
10578
|
-
const events = readMonitorLog(opts.session);
|
|
10579
|
-
const commands = pairCommands(events);
|
|
10580
|
-
const errors = commands.filter(
|
|
10581
|
-
(c) => c.exitCode != null && c.exitCode !== 0
|
|
10582
|
-
).length;
|
|
10583
|
-
const meta = events.find(
|
|
10584
|
-
(e) => e.type === "monitor_meta" && e.event === "start"
|
|
10585
|
-
);
|
|
10586
|
-
const stopped = events.some(
|
|
10587
|
-
(e) => e.type === "monitor_meta" && e.event === "stop"
|
|
10588
|
-
);
|
|
10589
|
-
const result = {
|
|
10590
|
-
sessionId: opts.session,
|
|
10591
|
-
exists: true,
|
|
10592
|
-
active: !stopped,
|
|
10593
|
-
shell: meta?.shell ?? "unknown",
|
|
10594
|
-
totalCommands: commands.length,
|
|
10595
|
-
errors,
|
|
10596
|
-
sizeBytes: stats.sizeBytes,
|
|
10597
|
-
timeSpan: commands.length > 0 ? {
|
|
10598
|
-
start: commands[0].startedAt,
|
|
10599
|
-
end: commands[commands.length - 1].endedAt ?? commands[commands.length - 1].startedAt
|
|
10600
|
-
} : null
|
|
10601
|
-
};
|
|
10602
|
-
if (opts.json) {
|
|
10603
|
-
console.log(JSON.stringify(result, null, 2));
|
|
10604
|
-
return;
|
|
10605
|
-
}
|
|
10606
|
-
console.log(`Monitor: ${opts.session}`);
|
|
10607
|
-
console.log(` Status: ${result.active ? "active" : "stopped"}`);
|
|
10608
|
-
console.log(` Shell: ${result.shell}`);
|
|
10609
|
-
console.log(` Commands: ${result.totalCommands}`);
|
|
10610
|
-
console.log(` Errors: ${result.errors}`);
|
|
10611
|
-
if (result.timeSpan) {
|
|
10612
|
-
console.log(` From: ${result.timeSpan.start}`);
|
|
10613
|
-
console.log(` To: ${result.timeSpan.end}`);
|
|
10614
|
-
}
|
|
10615
|
-
});
|
|
10616
|
-
function buildMonitorSetupCommand(dir, sessionId, shell) {
|
|
10617
|
-
const zamInvocation = resolveZamInvocation(shell);
|
|
10618
|
-
if (isPowerShellShell(shell)) {
|
|
10619
|
-
return [
|
|
10620
|
-
`Set-Location -LiteralPath ${psSingleQuoted2(dir)}`,
|
|
10621
|
-
`$__zamHook = ${zamInvocation} monitor start --session ${psSingleQuoted2(sessionId)} --shell ${shell}`,
|
|
10622
|
-
"Invoke-Expression ($__zamHook -join [Environment]::NewLine)",
|
|
10623
|
-
"Remove-Variable __zamHook"
|
|
10624
|
-
].join("; ");
|
|
10625
|
-
}
|
|
10626
|
-
return `cd ${JSON.stringify(dir)} && eval "$(${zamInvocation} monitor start --session ${sessionId} --shell ${shell})"`;
|
|
10627
|
-
}
|
|
10628
|
-
monitorCommand.command("open").description("Open a new monitored terminal window for a session").requiredOption("--session <id>", "Session ID to monitor").option("--dir <path>", "Working directory (defaults to cwd)").option(
|
|
10629
|
-
"--shell <type>",
|
|
10630
|
-
"Shell type: zsh | bash | pwsh | powershell (auto-detected)"
|
|
10631
|
-
).action(async (opts) => {
|
|
10632
|
-
let shell;
|
|
10633
|
-
try {
|
|
10634
|
-
shell = normalizeShell(opts.shell);
|
|
10635
|
-
} catch (err) {
|
|
10636
|
-
console.error(`Error: ${err.message}`);
|
|
10637
|
-
process.exit(1);
|
|
10638
|
-
}
|
|
10639
|
-
let db;
|
|
10640
|
-
try {
|
|
10641
|
-
db = await openDatabase();
|
|
10642
|
-
const session = await db.prepare("SELECT id, completed_at FROM sessions WHERE id = ?").get(opts.session);
|
|
10643
|
-
if (!session) {
|
|
10644
|
-
console.error(`Error: Session not found: ${opts.session}`);
|
|
10645
|
-
process.exit(1);
|
|
10646
|
-
}
|
|
10647
|
-
if (session.completed_at) {
|
|
10648
|
-
console.error(`Error: Session already completed: ${opts.session}`);
|
|
10649
|
-
process.exit(1);
|
|
10650
|
-
}
|
|
10651
|
-
if (!await getSetting(db, "monitor_method")) {
|
|
10652
|
-
await setSetting(db, "monitor_method", "terminal");
|
|
10653
|
-
}
|
|
10654
|
-
} catch (err) {
|
|
10655
|
-
console.error(`Error: ${err.message}`);
|
|
10656
|
-
process.exit(1);
|
|
10657
|
-
} finally {
|
|
10658
|
-
await db?.close();
|
|
10659
|
-
}
|
|
10660
|
-
const dir = opts.dir ?? process.cwd();
|
|
10661
|
-
const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
|
|
10662
|
-
openTerminalWindow({
|
|
10663
|
-
shellSetup,
|
|
10664
|
-
label: `monitor-${opts.session}`,
|
|
10665
|
-
dir,
|
|
10666
|
-
shell
|
|
10667
|
-
});
|
|
10668
|
-
});
|
|
10669
|
-
|
|
10670
|
-
// src/cli/commands/observer.ts
|
|
10671
|
-
import { Command as Command12 } from "commander";
|
|
10672
|
-
var observerCommand = new Command12("observer").description(
|
|
10673
|
-
"Configure what the UI observer may capture (Layer 2 policy)"
|
|
10674
|
-
);
|
|
10675
|
-
function applyObserverListChange(current, entry, op) {
|
|
10676
|
-
const normalized = entry.trim().toLowerCase();
|
|
10677
|
-
const list = parseObserverList(current);
|
|
10678
|
-
const next = op === "add" ? [.../* @__PURE__ */ new Set([...list, normalized])] : list.filter((item) => item !== normalized);
|
|
10679
|
-
return next.join(",");
|
|
10680
|
-
}
|
|
10681
|
-
observerCommand.command("status").description("Show the active observer policy").option("--json", "Output as JSON").action(async (opts) => {
|
|
10682
|
-
await withDb(async (db) => {
|
|
10683
|
-
const policy = await resolveObserverPolicy(db);
|
|
10684
|
-
if (opts.json) {
|
|
10685
|
-
console.log(JSON.stringify(policy, null, 2));
|
|
10686
|
-
return;
|
|
10687
|
-
}
|
|
10688
|
-
console.log("Observer policy:\n");
|
|
10689
|
-
console.log(` Scope: ${policy.scope}`);
|
|
10690
|
-
console.log(` Consent: ${policy.consent}`);
|
|
10691
|
-
console.log(` Retention: ${policy.retention}`);
|
|
10692
|
-
console.log(
|
|
10693
|
-
` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
|
|
10694
|
-
);
|
|
10695
|
-
console.log(
|
|
10696
|
-
` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
|
|
10697
|
-
);
|
|
10698
|
-
console.log(` Redact titles: ${policy.redactWindowTitles}`);
|
|
10699
|
-
console.log(
|
|
10700
|
-
"\nBuilt-in sensitive surfaces (password managers, auth/UAC dialogs, banking) are always refused and cannot be allowlisted."
|
|
10701
|
-
);
|
|
10702
|
-
});
|
|
10703
|
-
});
|
|
10704
|
-
observerCommand.command("grant <process>").description(
|
|
10705
|
-
"Allow the observer to capture a process (adds it to observer.allowlist)"
|
|
10706
|
-
).action(async (processName) => {
|
|
10707
|
-
await withDb(async (db) => {
|
|
10708
|
-
const next = applyObserverListChange(
|
|
10709
|
-
await getSetting(db, "observer.allowlist"),
|
|
10710
|
-
processName,
|
|
10711
|
-
"add"
|
|
10712
|
-
);
|
|
10713
|
-
await setSetting(db, "observer.allowlist", next);
|
|
10714
|
-
await syncObserverSidecarPolicy(db);
|
|
10715
|
-
console.log(`Granted: ${processName.trim().toLowerCase()}`);
|
|
10716
|
-
console.log(`observer.allowlist = ${next || "(empty)"}`);
|
|
10717
|
-
});
|
|
10718
|
-
});
|
|
10719
|
-
observerCommand.command("revoke <process>").description("Remove a process from observer.allowlist").action(async (processName) => {
|
|
10720
|
-
await withDb(async (db) => {
|
|
10721
|
-
const next = applyObserverListChange(
|
|
10722
|
-
await getSetting(db, "observer.allowlist"),
|
|
10723
|
-
processName,
|
|
10724
|
-
"remove"
|
|
10725
|
-
);
|
|
10726
|
-
await setSetting(db, "observer.allowlist", next);
|
|
10727
|
-
await syncObserverSidecarPolicy(db);
|
|
10728
|
-
console.log(`Revoked: ${processName.trim().toLowerCase()}`);
|
|
10729
|
-
console.log(`observer.allowlist = ${next || "(empty)"}`);
|
|
10730
|
-
});
|
|
10731
|
-
});
|
|
10732
|
-
|
|
10733
|
-
// src/cli/commands/profile.ts
|
|
10734
|
-
import { homedir as homedir12 } from "os";
|
|
10735
|
-
import { dirname as dirname6, join as join20, resolve as resolve7 } from "path";
|
|
10736
|
-
import { Command as Command13 } from "commander";
|
|
10737
|
-
var C2 = {
|
|
10738
|
-
reset: "\x1B[0m",
|
|
10739
|
-
bold: "\x1B[1m",
|
|
10740
|
-
cyan: "\x1B[36m",
|
|
10741
|
-
dim: "\x1B[2m",
|
|
10742
|
-
green: "\x1B[32m"
|
|
10743
|
-
};
|
|
10744
|
-
function defaultPersonalDir() {
|
|
10745
|
-
return join20(homedir12(), "Documents", "zam");
|
|
10746
|
-
}
|
|
10747
|
-
function render(profile) {
|
|
10748
|
-
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}`;
|
|
10749
|
-
console.log(`${C2.bold}ZAM install profile${C2.reset}`);
|
|
10750
|
-
console.log(` mode: ${C2.cyan}${profile.mode}${C2.reset}`);
|
|
10751
|
-
console.log(` personal dir: ${C2.cyan}${profile.personalDir}${C2.reset}`);
|
|
10752
|
-
console.log(` sync: ${sync}`);
|
|
10753
|
-
console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
|
|
10754
|
-
console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
|
|
10755
|
-
}
|
|
10756
|
-
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) => {
|
|
10757
|
-
if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
|
|
10758
|
-
console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
|
|
10759
|
-
process.exit(1);
|
|
10760
|
-
}
|
|
10761
|
-
let db;
|
|
10762
|
-
try {
|
|
10763
|
-
if (opts.mode) setInstallMode(opts.mode);
|
|
10764
|
-
db = await openDatabaseWithSync({ initialize: true });
|
|
10765
|
-
if (opts.dir) {
|
|
10766
|
-
await setSetting(db, "personal.workspace_dir", resolve7(opts.dir));
|
|
10767
|
-
}
|
|
10768
|
-
const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
|
|
10769
|
-
await db.close();
|
|
10770
|
-
db = void 0;
|
|
10771
|
-
const dbPath = getDefaultDbPath();
|
|
10772
|
-
const profile = {
|
|
10773
|
-
mode: getInstallMode(),
|
|
10774
|
-
personalDir,
|
|
10775
|
-
syncProvider: detectSyncProvider(personalDir),
|
|
10776
|
-
dataDir: dirname6(dbPath),
|
|
10777
|
-
dbPath
|
|
10778
|
-
};
|
|
10779
|
-
if (opts.json) {
|
|
10780
|
-
console.log(JSON.stringify(profile, null, 2));
|
|
10781
|
-
return;
|
|
10782
|
-
}
|
|
10783
|
-
render(profile);
|
|
11399
|
+
}
|
|
11400
|
+
let shell;
|
|
11401
|
+
try {
|
|
11402
|
+
shell = normalizeShell(opts.shell);
|
|
10784
11403
|
} catch (err) {
|
|
10785
|
-
|
|
10786
|
-
console.error("Error:", err.message);
|
|
11404
|
+
console.error(`# Error: ${err.message}`);
|
|
10787
11405
|
process.exit(1);
|
|
10788
11406
|
}
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
"chat-completions",
|
|
10796
|
-
"anthropic-messages"
|
|
10797
|
-
];
|
|
10798
|
-
var VALID_ROLES = ["vision", "recall", "text"];
|
|
10799
|
-
function upsertProviderRecord(providers, name, patch) {
|
|
10800
|
-
const merged = { ...providers[name] ?? {} };
|
|
10801
|
-
if (patch.url !== void 0) merged.url = patch.url;
|
|
10802
|
-
if (patch.model !== void 0) merged.model = patch.model;
|
|
10803
|
-
if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
|
|
10804
|
-
if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
|
|
10805
|
-
if (patch.label !== void 0) merged.label = patch.label;
|
|
10806
|
-
if (patch.local !== void 0) merged.local = patch.local;
|
|
10807
|
-
if (patch.runner !== void 0) merged.runner = patch.runner;
|
|
10808
|
-
return { ...providers, [name]: merged };
|
|
10809
|
-
}
|
|
10810
|
-
function removeProviderRecord(providers, name) {
|
|
10811
|
-
if (!(name in providers)) return { providers, removed: false };
|
|
10812
|
-
const next = { ...providers };
|
|
10813
|
-
delete next[name];
|
|
10814
|
-
return { providers: next, removed: true };
|
|
10815
|
-
}
|
|
10816
|
-
function rolesReferencing(roles, name) {
|
|
10817
|
-
return VALID_ROLES.filter((role) => {
|
|
10818
|
-
const binding = roles[role];
|
|
10819
|
-
return binding?.primary === name || binding?.fallback === name;
|
|
10820
|
-
});
|
|
10821
|
-
}
|
|
10822
|
-
function bindRoleProviders(roles, role, primary, fallback) {
|
|
10823
|
-
const binding = { primary };
|
|
10824
|
-
if (fallback) binding.fallback = fallback;
|
|
10825
|
-
return { ...roles, [role]: binding };
|
|
10826
|
-
}
|
|
10827
|
-
function maskSecret(key) {
|
|
10828
|
-
return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
|
|
10829
|
-
}
|
|
10830
|
-
function buildProviderListing(providers, hasKey) {
|
|
10831
|
-
return Object.entries(providers).map(([name, rec]) => {
|
|
10832
|
-
const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
|
|
10833
|
-
let keyState;
|
|
10834
|
-
if (!rec.apiKeyRef) keyState = "none";
|
|
10835
|
-
else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
|
|
10836
|
-
const row = {
|
|
10837
|
-
name,
|
|
10838
|
-
url: rec.url,
|
|
10839
|
-
model: rec.model,
|
|
10840
|
-
apiFlavor,
|
|
10841
|
-
apiKeyRef: rec.apiKeyRef,
|
|
10842
|
-
keyState
|
|
10843
|
-
};
|
|
10844
|
-
if (rec.label !== void 0) row.label = rec.label;
|
|
10845
|
-
if (rec.local !== void 0) row.local = rec.local;
|
|
10846
|
-
if (rec.runner !== void 0) row.runner = rec.runner;
|
|
10847
|
-
return row;
|
|
10848
|
-
});
|
|
10849
|
-
}
|
|
10850
|
-
function findOrphanKeyRefs(storedRefs, providers) {
|
|
10851
|
-
const used = new Set(
|
|
10852
|
-
Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
|
|
10853
|
-
);
|
|
10854
|
-
return storedRefs.filter((ref) => !used.has(ref));
|
|
10855
|
-
}
|
|
10856
|
-
async function readJson(db, key, fallback) {
|
|
10857
|
-
const raw = await getSetting(db, key);
|
|
10858
|
-
if (!raw) return fallback;
|
|
10859
|
-
try {
|
|
10860
|
-
return JSON.parse(raw);
|
|
10861
|
-
} catch {
|
|
10862
|
-
return fallback;
|
|
11407
|
+
if (shell === "bash") {
|
|
11408
|
+
console.log(generateBashUnhooks());
|
|
11409
|
+
} else if (isPowerShellShell(shell)) {
|
|
11410
|
+
console.log(generatePowerShellUnhooks());
|
|
11411
|
+
} else {
|
|
11412
|
+
console.log(generateZshUnhooks());
|
|
10863
11413
|
}
|
|
10864
|
-
}
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
}
|
|
10873
|
-
async function readScopedRoles(db, machine) {
|
|
10874
|
-
if (machine) return getMachineAiConfig().roles ?? {};
|
|
10875
|
-
if (!db)
|
|
10876
|
-
throw new Error("Database is required for shared provider settings.");
|
|
10877
|
-
return readRoles(db);
|
|
10878
|
-
}
|
|
10879
|
-
async function writeScopedProviders(db, machine, p) {
|
|
10880
|
-
if (machine) {
|
|
10881
|
-
const config = getMachineAiConfig();
|
|
10882
|
-
saveMachineAiConfig({ ...config, providers: p });
|
|
11414
|
+
});
|
|
11415
|
+
monitorCommand.command("status").description("Show monitoring status for a session").requiredOption("--session <id>", "Session ID").option("--json", "Output as JSON").action((opts) => {
|
|
11416
|
+
const stats = getMonitorLogStats(opts.session);
|
|
11417
|
+
if (!stats.exists) {
|
|
11418
|
+
if (opts.json) {
|
|
11419
|
+
console.log(JSON.stringify({ exists: false }));
|
|
11420
|
+
} else {
|
|
11421
|
+
console.log(`No monitor log found for session ${opts.session}`);
|
|
11422
|
+
}
|
|
10883
11423
|
return;
|
|
10884
11424
|
}
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
11425
|
+
const events = readMonitorLog(opts.session);
|
|
11426
|
+
const commands = pairCommands(events);
|
|
11427
|
+
const errors = commands.filter(
|
|
11428
|
+
(c) => c.exitCode != null && c.exitCode !== 0
|
|
11429
|
+
).length;
|
|
11430
|
+
const meta = events.find(
|
|
11431
|
+
(e) => e.type === "monitor_meta" && e.event === "start"
|
|
11432
|
+
);
|
|
11433
|
+
const stopped = events.some(
|
|
11434
|
+
(e) => e.type === "monitor_meta" && e.event === "stop"
|
|
11435
|
+
);
|
|
11436
|
+
const result = {
|
|
11437
|
+
sessionId: opts.session,
|
|
11438
|
+
exists: true,
|
|
11439
|
+
active: !stopped,
|
|
11440
|
+
shell: meta?.shell ?? "unknown",
|
|
11441
|
+
totalCommands: commands.length,
|
|
11442
|
+
errors,
|
|
11443
|
+
sizeBytes: stats.sizeBytes,
|
|
11444
|
+
timeSpan: commands.length > 0 ? {
|
|
11445
|
+
start: commands[0].startedAt,
|
|
11446
|
+
end: commands[commands.length - 1].endedAt ?? commands[commands.length - 1].startedAt
|
|
11447
|
+
} : null
|
|
11448
|
+
};
|
|
11449
|
+
if (opts.json) {
|
|
11450
|
+
console.log(JSON.stringify(result, null, 2));
|
|
10893
11451
|
return;
|
|
10894
11452
|
}
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
}
|
|
10899
|
-
|
|
10900
|
-
if (
|
|
10901
|
-
|
|
10902
|
-
|
|
11453
|
+
console.log(`Monitor: ${opts.session}`);
|
|
11454
|
+
console.log(` Status: ${result.active ? "active" : "stopped"}`);
|
|
11455
|
+
console.log(` Shell: ${result.shell}`);
|
|
11456
|
+
console.log(` Commands: ${result.totalCommands}`);
|
|
11457
|
+
console.log(` Errors: ${result.errors}`);
|
|
11458
|
+
if (result.timeSpan) {
|
|
11459
|
+
console.log(` From: ${result.timeSpan.start}`);
|
|
11460
|
+
console.log(` To: ${result.timeSpan.end}`);
|
|
10903
11461
|
}
|
|
10904
|
-
|
|
11462
|
+
});
|
|
11463
|
+
function buildMonitorSetupCommand(dir, sessionId, shell) {
|
|
11464
|
+
const zamInvocation = resolveZamInvocation(shell);
|
|
11465
|
+
if (isPowerShellShell(shell)) {
|
|
11466
|
+
return [
|
|
11467
|
+
`Set-Location -LiteralPath ${psSingleQuoted2(dir)}`,
|
|
11468
|
+
`$__zamHook = ${zamInvocation} monitor start --session ${psSingleQuoted2(sessionId)} --shell ${shell}`,
|
|
11469
|
+
"Invoke-Expression ($__zamHook -join [Environment]::NewLine)",
|
|
11470
|
+
"Remove-Variable __zamHook"
|
|
11471
|
+
].join("; ");
|
|
11472
|
+
}
|
|
11473
|
+
return `cd ${JSON.stringify(dir)} && eval "$(${zamInvocation} monitor start --session ${sessionId} --shell ${shell})"`;
|
|
10905
11474
|
}
|
|
10906
|
-
|
|
10907
|
-
"
|
|
10908
|
-
)
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
providers: rows,
|
|
10925
|
-
roles,
|
|
10926
|
-
orphans
|
|
10927
|
-
},
|
|
10928
|
-
null,
|
|
10929
|
-
2
|
|
10930
|
-
)
|
|
10931
|
-
);
|
|
10932
|
-
return;
|
|
10933
|
-
}
|
|
10934
|
-
console.log(
|
|
10935
|
-
`Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
|
|
10936
|
-
`
|
|
10937
|
-
);
|
|
10938
|
-
if (rows.length === 0) {
|
|
10939
|
-
console.log(
|
|
10940
|
-
" (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
|
|
10941
|
-
);
|
|
10942
|
-
} else {
|
|
10943
|
-
for (const row of rows) {
|
|
10944
|
-
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";
|
|
10945
|
-
console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
|
|
10946
|
-
console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
|
|
10947
|
-
console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
|
|
10948
|
-
console.log(` flavor: ${row.apiFlavor}`);
|
|
10949
|
-
if (row.label) console.log(` label: ${row.label}`);
|
|
10950
|
-
if (row.local !== void 0) {
|
|
10951
|
-
console.log(` local: ${row.local ? "yes" : "no"}`);
|
|
10952
|
-
}
|
|
10953
|
-
if (row.runner) console.log(` runner: ${row.runner}`);
|
|
10954
|
-
if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
|
|
10955
|
-
}
|
|
11475
|
+
monitorCommand.command("open").description("Open a new monitored terminal window for a session").requiredOption("--session <id>", "Session ID to monitor").option("--dir <path>", "Working directory (defaults to cwd)").option(
|
|
11476
|
+
"--shell <type>",
|
|
11477
|
+
"Shell type: zsh | bash | pwsh | powershell (auto-detected)"
|
|
11478
|
+
).action(async (opts) => {
|
|
11479
|
+
let shell;
|
|
11480
|
+
try {
|
|
11481
|
+
shell = normalizeShell(opts.shell);
|
|
11482
|
+
} catch (err) {
|
|
11483
|
+
console.error(`Error: ${err.message}`);
|
|
11484
|
+
process.exit(1);
|
|
11485
|
+
}
|
|
11486
|
+
let db;
|
|
11487
|
+
try {
|
|
11488
|
+
db = await openDatabase();
|
|
11489
|
+
const session = await db.prepare("SELECT id, completed_at FROM sessions WHERE id = ?").get(opts.session);
|
|
11490
|
+
if (!session) {
|
|
11491
|
+
console.error(`Error: Session not found: ${opts.session}`);
|
|
11492
|
+
process.exit(1);
|
|
10956
11493
|
}
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
if (!binding?.primary) {
|
|
10961
|
-
console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
|
|
10962
|
-
} else {
|
|
10963
|
-
const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
|
|
10964
|
-
console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
|
|
10965
|
-
}
|
|
11494
|
+
if (session.completed_at) {
|
|
11495
|
+
console.error(`Error: Session already completed: ${opts.session}`);
|
|
11496
|
+
process.exit(1);
|
|
10966
11497
|
}
|
|
10967
|
-
if (
|
|
10968
|
-
|
|
10969
|
-
`
|
|
10970
|
-
\x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
|
|
10971
|
-
);
|
|
11498
|
+
if (!await getSetting(db, "monitor_method")) {
|
|
11499
|
+
await setSetting(db, "monitor_method", "terminal");
|
|
10972
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
|
|
10973
11514
|
});
|
|
10974
11515
|
});
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
).
|
|
10979
|
-
"
|
|
10980
|
-
|
|
10981
|
-
)
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
).
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
).option("--
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
if (
|
|
10991
|
-
console.
|
|
10992
|
-
|
|
10993
|
-
);
|
|
10994
|
-
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;
|
|
10995
11534
|
}
|
|
10996
|
-
|
|
10997
|
-
}
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
await withProviderScope(machine, async (db) => {
|
|
11001
|
-
const providers = await readScopedProviders(db, machine);
|
|
11002
|
-
const next = upsertProviderRecord(providers, name, {
|
|
11003
|
-
label: opts.label,
|
|
11004
|
-
url: opts.url,
|
|
11005
|
-
model: opts.model,
|
|
11006
|
-
apiFlavor,
|
|
11007
|
-
apiKeyRef,
|
|
11008
|
-
local: opts.local ? true : void 0,
|
|
11009
|
-
runner: opts.runner
|
|
11010
|
-
});
|
|
11011
|
-
await writeScopedProviders(db, machine, next);
|
|
11012
|
-
if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
|
|
11013
|
-
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}`);
|
|
11014
11539
|
console.log(
|
|
11015
|
-
`
|
|
11540
|
+
` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
|
|
11016
11541
|
);
|
|
11017
|
-
console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
|
|
11018
|
-
console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
|
|
11019
|
-
if (rec.label) console.log(` label: ${rec.label}`);
|
|
11020
|
-
if (rec.local !== void 0) {
|
|
11021
|
-
console.log(` local: ${rec.local ? "yes" : "no"}`);
|
|
11022
|
-
}
|
|
11023
|
-
if (rec.runner) console.log(` runner: ${rec.runner}`);
|
|
11024
11542
|
console.log(
|
|
11025
|
-
`
|
|
11543
|
+
` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
|
|
11026
11544
|
);
|
|
11027
|
-
console.log(`
|
|
11028
|
-
if (opts.key) {
|
|
11029
|
-
console.log(` key: stored (${maskSecret(opts.key)})`);
|
|
11030
|
-
} else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
|
|
11031
|
-
console.log(
|
|
11032
|
-
`
|
|
11033
|
-
\u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
|
|
11034
|
-
);
|
|
11035
|
-
}
|
|
11036
|
-
if (!rec.url) {
|
|
11037
|
-
console.log(
|
|
11038
|
-
"\n \u26A0 No --url set; this provider inherits the base llm.url."
|
|
11039
|
-
);
|
|
11040
|
-
}
|
|
11545
|
+
console.log(` Redact titles: ${policy.redactWindowTitles}`);
|
|
11041
11546
|
console.log(
|
|
11042
|
-
|
|
11043
|
-
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."
|
|
11044
11548
|
);
|
|
11045
11549
|
});
|
|
11046
11550
|
});
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
const
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
if (!removed) {
|
|
11056
|
-
console.log(`No such provider: ${name}`);
|
|
11057
|
-
return;
|
|
11058
|
-
}
|
|
11059
|
-
await writeScopedProviders(db, machine, next);
|
|
11060
|
-
console.log(
|
|
11061
|
-
`Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
|
|
11062
|
-
);
|
|
11063
|
-
const referencing = rolesReferencing(
|
|
11064
|
-
await readScopedRoles(db, machine),
|
|
11065
|
-
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"
|
|
11066
11559
|
);
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
}
|
|
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)"}`);
|
|
11072
11564
|
});
|
|
11073
11565
|
});
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
console.error("--primary is required.");
|
|
11081
|
-
process.exit(1);
|
|
11082
|
-
}
|
|
11083
|
-
const machine = Boolean(opts.machine);
|
|
11084
|
-
await withProviderScope(machine, async (db) => {
|
|
11085
|
-
const providers = await readScopedProviders(db, machine);
|
|
11086
|
-
await writeScopedRoles(
|
|
11087
|
-
db,
|
|
11088
|
-
machine,
|
|
11089
|
-
bindRoleProviders(
|
|
11090
|
-
await readScopedRoles(db, machine),
|
|
11091
|
-
role,
|
|
11092
|
-
opts.primary,
|
|
11093
|
-
opts.fallback
|
|
11094
|
-
)
|
|
11095
|
-
);
|
|
11096
|
-
const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
|
|
11097
|
-
console.log(
|
|
11098
|
-
`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"
|
|
11099
11572
|
);
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
if (ref && !(ref in providers)) {
|
|
11105
|
-
console.log(
|
|
11106
|
-
` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
|
|
11107
|
-
);
|
|
11108
|
-
}
|
|
11109
|
-
}
|
|
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)"}`);
|
|
11110
11577
|
});
|
|
11111
11578
|
});
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
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;
|
|
11118
11609
|
try {
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
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));
|
|
11123
11614
|
}
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
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;
|
|
11130
11629
|
}
|
|
11630
|
+
render(profile);
|
|
11631
|
+
} catch (err) {
|
|
11632
|
+
await db?.close();
|
|
11131
11633
|
console.error("Error:", err.message);
|
|
11132
11634
|
process.exit(1);
|
|
11133
11635
|
}
|
|
11134
11636
|
});
|
|
11135
|
-
providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
|
|
11136
|
-
clearProviderApiKey(ref);
|
|
11137
|
-
console.log(`Cleared API key for "${ref}".`);
|
|
11138
|
-
});
|
|
11139
11637
|
|
|
11140
11638
|
// src/cli/commands/review.ts
|
|
11141
11639
|
import { Command as Command15 } from "commander";
|
|
@@ -11897,7 +12395,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
|
|
|
11897
12395
|
});
|
|
11898
12396
|
|
|
11899
12397
|
// src/cli/commands/snapshot.ts
|
|
11900
|
-
import { existsSync as existsSync22, mkdirSync as
|
|
12398
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
|
|
11901
12399
|
import { homedir as homedir13 } from "os";
|
|
11902
12400
|
import { dirname as dirname7, join as join21 } from "path";
|
|
11903
12401
|
import { Command as Command19 } from "commander";
|
|
@@ -11931,7 +12429,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
|
|
|
11931
12429
|
const out = opts.out ?? join21(personalDir, "snapshots", defaultOutName());
|
|
11932
12430
|
const dir = dirname7(out);
|
|
11933
12431
|
if (dir && dir !== "." && !existsSync22(dir)) {
|
|
11934
|
-
|
|
12432
|
+
mkdirSync13(dir, { recursive: true });
|
|
11935
12433
|
}
|
|
11936
12434
|
writeFileSync11(out, snapshot, "utf-8");
|
|
11937
12435
|
console.log(`Snapshot written: ${out}`);
|
|
@@ -12601,7 +13099,7 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
|
|
|
12601
13099
|
|
|
12602
13100
|
// src/cli/commands/update.ts
|
|
12603
13101
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
12604
|
-
import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync } from "fs";
|
|
13102
|
+
import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "fs";
|
|
12605
13103
|
import { dirname as dirname9, join as join23 } from "path";
|
|
12606
13104
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
12607
13105
|
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
@@ -12715,7 +13213,7 @@ var checkCmd = new Command23("check").description("Check whether a newer ZAM has
|
|
|
12715
13213
|
}
|
|
12716
13214
|
);
|
|
12717
13215
|
function findSourceRepo() {
|
|
12718
|
-
let dir =
|
|
13216
|
+
let dir = realpathSync2(dirname9(fileURLToPath4(import.meta.url)));
|
|
12719
13217
|
let parent = dirname9(dir);
|
|
12720
13218
|
while (parent !== dir) {
|
|
12721
13219
|
if (existsSync24(join23(dir, ".git"))) return dir;
|