zam-core 0.5.1 → 0.5.3
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 +1918 -1363
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +14 -5
- package/dist/index.js +56 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { readFileSync as readFileSync15 } from "fs";
|
|
5
|
-
import { dirname as dirname10, join as
|
|
5
|
+
import { dirname as dirname10, join as join25 } from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
7
7
|
import { Command as Command25 } from "commander";
|
|
8
8
|
|
|
@@ -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();
|
|
@@ -4463,25 +4478,41 @@ function saveMachineAiConfig(ai, path = defaultConfigPath()) {
|
|
|
4463
4478
|
function getConfiguredWorkspaces(path = defaultConfigPath()) {
|
|
4464
4479
|
return loadInstallConfig(path).workspaces ?? [];
|
|
4465
4480
|
}
|
|
4466
|
-
function
|
|
4481
|
+
function setActiveWorkspaceId(id, path = defaultConfigPath()) {
|
|
4467
4482
|
const config = loadInstallConfig(path);
|
|
4468
|
-
|
|
4483
|
+
if (id) {
|
|
4484
|
+
config.activeWorkspaceId = id;
|
|
4485
|
+
} else {
|
|
4486
|
+
delete config.activeWorkspaceId;
|
|
4487
|
+
}
|
|
4469
4488
|
saveInstallConfig(config, path);
|
|
4470
4489
|
}
|
|
4490
|
+
function getActiveWorkspace(path = defaultConfigPath()) {
|
|
4491
|
+
const config = loadInstallConfig(path);
|
|
4492
|
+
const id = config.activeWorkspaceId;
|
|
4493
|
+
return id ? config.workspaces?.find((workspace) => workspace.id === id) : void 0;
|
|
4494
|
+
}
|
|
4471
4495
|
function upsertConfiguredWorkspace(workspace, path = defaultConfigPath()) {
|
|
4472
|
-
const
|
|
4496
|
+
const config = loadInstallConfig(path);
|
|
4497
|
+
const current = config.workspaces ?? [];
|
|
4473
4498
|
const next = [
|
|
4474
4499
|
...current.filter((candidate) => candidate.id !== workspace.id),
|
|
4475
4500
|
workspace
|
|
4476
4501
|
];
|
|
4477
|
-
|
|
4502
|
+
config.workspaces = next;
|
|
4503
|
+
saveInstallConfig(config, path);
|
|
4478
4504
|
return next;
|
|
4479
4505
|
}
|
|
4480
4506
|
function removeConfiguredWorkspace(id, path = defaultConfigPath()) {
|
|
4481
|
-
const
|
|
4507
|
+
const config = loadInstallConfig(path);
|
|
4508
|
+
const next = (config.workspaces ?? []).filter(
|
|
4482
4509
|
(workspace) => workspace.id !== id
|
|
4483
4510
|
);
|
|
4484
|
-
|
|
4511
|
+
config.workspaces = next;
|
|
4512
|
+
if (config.activeWorkspaceId === id) {
|
|
4513
|
+
config.activeWorkspaceId = next[0]?.id;
|
|
4514
|
+
}
|
|
4515
|
+
saveInstallConfig(config, path);
|
|
4485
4516
|
return next;
|
|
4486
4517
|
}
|
|
4487
4518
|
function detectSyncProvider(dir) {
|
|
@@ -4813,7 +4844,7 @@ function getSystemProfile() {
|
|
|
4813
4844
|
import { existsSync as existsSync10 } from "fs";
|
|
4814
4845
|
import { resolve as resolve2 } from "path";
|
|
4815
4846
|
async function getRepoPaths(db) {
|
|
4816
|
-
const personalSetting = await getSetting(db, "repo.personal") ||
|
|
4847
|
+
const personalSetting = await getSetting(db, "repo.personal") || getActiveWorkspace()?.path;
|
|
4817
4848
|
const teamSetting = await getSetting(db, "repo.team");
|
|
4818
4849
|
const orgSetting = await getSetting(db, "repo.org");
|
|
4819
4850
|
return {
|
|
@@ -5357,23 +5388,33 @@ var openCmd = new Command("open").description("Open an agent harness in the work
|
|
|
5357
5388
|
var agentCommand = new Command("agent").description("Provision and inspect the agent that drives ZAM sessions").addCommand(installCmd).addCommand(statusCmd).addCommand(openCmd).addCommand(listCmd).action(printStatus);
|
|
5358
5389
|
|
|
5359
5390
|
// src/cli/commands/bridge.ts
|
|
5360
|
-
import { execFileSync as
|
|
5391
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
5361
5392
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
5362
|
-
import {
|
|
5363
|
-
existsSync as existsSync17,
|
|
5364
|
-
mkdirSync as mkdirSync10,
|
|
5365
|
-
readdirSync as readdirSync3,
|
|
5366
|
-
readFileSync as readFileSync11,
|
|
5367
|
-
rmSync as rmSync3
|
|
5368
|
-
} from "fs";
|
|
5393
|
+
import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
|
|
5369
5394
|
import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
|
|
5370
|
-
import {
|
|
5371
|
-
import { Command as
|
|
5395
|
+
import { join as join18, resolve as resolve4 } from "path";
|
|
5396
|
+
import { Command as Command2 } from "commander";
|
|
5372
5397
|
|
|
5373
5398
|
// src/cli/llm/client.ts
|
|
5374
5399
|
import { spawn as spawn2 } from "child_process";
|
|
5375
5400
|
import { existsSync as existsSync14 } from "fs";
|
|
5376
5401
|
var DEFAULT_LLM_URL = "http://localhost:8000/v1";
|
|
5402
|
+
var DEFAULT_LLM_MAX_TOKENS = 1e4;
|
|
5403
|
+
var RECALL_QUESTION_MAX_OUTPUT_TOKENS = 400;
|
|
5404
|
+
var RECALL_EVALUATION_MAX_OUTPUT_TOKENS = 1200;
|
|
5405
|
+
var RECALL_ENDPOINT_CACHE_MS = 6e4;
|
|
5406
|
+
var cachedRecallEndpoint = null;
|
|
5407
|
+
function recallEndpointSignature(cfg) {
|
|
5408
|
+
return [
|
|
5409
|
+
cfg.enabled ? "on" : "off",
|
|
5410
|
+
cfg.providerName ?? "",
|
|
5411
|
+
cfg.url,
|
|
5412
|
+
cfg.model,
|
|
5413
|
+
cfg.apiFlavor,
|
|
5414
|
+
cfg.fallback?.url ?? "",
|
|
5415
|
+
cfg.fallback?.model ?? ""
|
|
5416
|
+
].join("|");
|
|
5417
|
+
}
|
|
5377
5418
|
var DEFAULT_LLM_MODEL = "qwen3.5:4b";
|
|
5378
5419
|
var DEFAULT_LLM_API_KEY = "sk-none";
|
|
5379
5420
|
async function getLlmConfig(db) {
|
|
@@ -5388,19 +5429,25 @@ async function getLlmConfig(db) {
|
|
|
5388
5429
|
function getCloudModelRecommendation(url) {
|
|
5389
5430
|
const lowercase = url.toLowerCase();
|
|
5390
5431
|
if (lowercase.includes("openrouter.ai")) {
|
|
5391
|
-
return "openrouter/free";
|
|
5432
|
+
return { model: "openrouter/free", flavor: "chat-completions" };
|
|
5392
5433
|
}
|
|
5393
5434
|
if (lowercase.includes("openai.com") || lowercase.includes("api.openai")) {
|
|
5394
|
-
return "gpt-5-mini";
|
|
5435
|
+
return { model: "gpt-5-mini", flavor: "chat-completions" };
|
|
5395
5436
|
}
|
|
5396
5437
|
if (lowercase.includes("googleapis.com") || lowercase.includes("google")) {
|
|
5397
|
-
return "gemini-3.5-flash";
|
|
5438
|
+
return { model: "gemini-3.5-flash", flavor: "chat-completions" };
|
|
5398
5439
|
}
|
|
5399
5440
|
if (lowercase.includes("deepseek.com")) {
|
|
5400
|
-
return "deepseek-v4-flash";
|
|
5441
|
+
return { model: "deepseek-v4-flash", flavor: "chat-completions" };
|
|
5401
5442
|
}
|
|
5402
5443
|
if (lowercase.includes("mimo")) {
|
|
5403
|
-
return "mimo-v2.5";
|
|
5444
|
+
return { model: "mimo-v2.5", flavor: "chat-completions" };
|
|
5445
|
+
}
|
|
5446
|
+
if (lowercase.includes("anthropic.com")) {
|
|
5447
|
+
return {
|
|
5448
|
+
model: "claude-haiku-4-5-20251001",
|
|
5449
|
+
flavor: "anthropic-messages"
|
|
5450
|
+
};
|
|
5404
5451
|
}
|
|
5405
5452
|
return null;
|
|
5406
5453
|
}
|
|
@@ -5484,6 +5531,7 @@ function materializeProvider(rec, base, role, meta) {
|
|
|
5484
5531
|
label: rec.label,
|
|
5485
5532
|
source: meta.source,
|
|
5486
5533
|
local: rec.local ?? isLocalEndpoint(url),
|
|
5534
|
+
...rec.runner ? { runner: rec.runner } : {},
|
|
5487
5535
|
...role === "vision" ? { maxFrames: base.maxFrames } : {}
|
|
5488
5536
|
};
|
|
5489
5537
|
}
|
|
@@ -5493,14 +5541,15 @@ async function getLegacyRoleConfig(db, role, enabled) {
|
|
|
5493
5541
|
const maxFramesStr = await getSetting(db, "llm.vision.max_frames");
|
|
5494
5542
|
const parsed = maxFramesStr ? parseInt(maxFramesStr, 10) : 100;
|
|
5495
5543
|
const url = await getSetting(db, "llm.vision.url") || base.url;
|
|
5544
|
+
const recommendation = getCloudModelRecommendation(url);
|
|
5496
5545
|
let model = await getSetting(db, "llm.vision.model");
|
|
5497
|
-
if (!model) model =
|
|
5546
|
+
if (!model) model = recommendation?.model || base.model;
|
|
5498
5547
|
return {
|
|
5499
5548
|
enabled,
|
|
5500
5549
|
url,
|
|
5501
5550
|
model,
|
|
5502
5551
|
apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
|
|
5503
|
-
apiFlavor: inferApiFlavor(url),
|
|
5552
|
+
apiFlavor: recommendation?.flavor || inferApiFlavor(url),
|
|
5504
5553
|
locale: base.locale,
|
|
5505
5554
|
source: "legacy",
|
|
5506
5555
|
local: isLocalEndpoint(url),
|
|
@@ -5566,10 +5615,7 @@ async function readChatContent(res, label) {
|
|
|
5566
5615
|
}
|
|
5567
5616
|
async function generateQuestionViaLLM(db, input8) {
|
|
5568
5617
|
const cfg = await getProviderForRole(db, "recall");
|
|
5569
|
-
|
|
5570
|
-
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5571
|
-
}
|
|
5572
|
-
assertChatCompletions(cfg);
|
|
5618
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
5573
5619
|
const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
|
|
5574
5620
|
const verb = BLOOM_VERBS2[bloom];
|
|
5575
5621
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
@@ -5589,31 +5635,36 @@ ${input8.sourceLinkContent ? `Source Reference:
|
|
|
5589
5635
|
${input8.sourceLinkContent}` : ""}
|
|
5590
5636
|
|
|
5591
5637
|
Active-Recall Question:`;
|
|
5592
|
-
const res = await fetchWithInteractiveTimeout(
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5638
|
+
const res = await fetchWithInteractiveTimeout(
|
|
5639
|
+
`${endpoint.url}/chat/completions`,
|
|
5640
|
+
{
|
|
5641
|
+
method: "POST",
|
|
5642
|
+
headers: {
|
|
5643
|
+
"Content-Type": "application/json",
|
|
5644
|
+
Authorization: `Bearer ${endpoint.apiKey}`
|
|
5645
|
+
},
|
|
5646
|
+
body: JSON.stringify({
|
|
5647
|
+
model: endpoint.model,
|
|
5648
|
+
messages: [
|
|
5649
|
+
{ role: "system", content: systemPrompt },
|
|
5650
|
+
{ role: "user", content: userPrompt }
|
|
5651
|
+
],
|
|
5652
|
+
temperature: 0.1,
|
|
5653
|
+
max_tokens: RECALL_QUESTION_MAX_OUTPUT_TOKENS
|
|
5654
|
+
}),
|
|
5655
|
+
locale: cfg.locale
|
|
5656
|
+
}
|
|
5657
|
+
);
|
|
5658
|
+
const text = await readChatContent(res, "LLM request");
|
|
5659
|
+
return {
|
|
5660
|
+
text,
|
|
5661
|
+
model: endpoint.model,
|
|
5662
|
+
providerName: endpoint.providerName
|
|
5663
|
+
};
|
|
5610
5664
|
}
|
|
5611
5665
|
async function evaluateAnswerViaLLM(db, input8) {
|
|
5612
5666
|
const cfg = await getProviderForRole(db, "recall");
|
|
5613
|
-
|
|
5614
|
-
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5615
|
-
}
|
|
5616
|
-
assertChatCompletions(cfg);
|
|
5667
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
5617
5668
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
5618
5669
|
const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
|
|
5619
5670
|
const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
|
|
@@ -5643,51 +5694,59 @@ ${input8.sourceLinkContent ? `Source Code Reference:
|
|
|
5643
5694
|
${input8.sourceLinkContent}` : ""}
|
|
5644
5695
|
|
|
5645
5696
|
Evaluation:`;
|
|
5646
|
-
const res = await fetchWithInteractiveTimeout(
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5697
|
+
const res = await fetchWithInteractiveTimeout(
|
|
5698
|
+
`${endpoint.url}/chat/completions`,
|
|
5699
|
+
{
|
|
5700
|
+
method: "POST",
|
|
5701
|
+
headers: {
|
|
5702
|
+
"Content-Type": "application/json",
|
|
5703
|
+
Authorization: `Bearer ${endpoint.apiKey}`
|
|
5704
|
+
},
|
|
5705
|
+
body: JSON.stringify({
|
|
5706
|
+
model: endpoint.model,
|
|
5707
|
+
messages: [
|
|
5708
|
+
{ role: "system", content: systemPrompt },
|
|
5709
|
+
{ role: "user", content: userPrompt }
|
|
5710
|
+
],
|
|
5711
|
+
temperature: 0.2,
|
|
5712
|
+
max_tokens: RECALL_EVALUATION_MAX_OUTPUT_TOKENS
|
|
5713
|
+
}),
|
|
5714
|
+
locale: cfg.locale
|
|
5715
|
+
}
|
|
5716
|
+
);
|
|
5717
|
+
const text = await readChatContent(res, "LLM evaluation");
|
|
5718
|
+
return {
|
|
5719
|
+
text,
|
|
5720
|
+
model: endpoint.model,
|
|
5721
|
+
providerName: endpoint.providerName
|
|
5722
|
+
};
|
|
5664
5723
|
}
|
|
5665
5724
|
async function translateQuestionViaLLM(db, question) {
|
|
5666
5725
|
const cfg = await getProviderForRole(db, "recall");
|
|
5667
|
-
|
|
5668
|
-
throw new Error("LLM integration is disabled in settings");
|
|
5669
|
-
}
|
|
5670
|
-
assertChatCompletions(cfg);
|
|
5726
|
+
const endpoint = await resolveUsableRecallEndpoint(db);
|
|
5671
5727
|
const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
5672
5728
|
const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
|
|
5673
5729
|
Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
|
|
5674
|
-
const res = await fetchWithInteractiveTimeout(
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5730
|
+
const res = await fetchWithInteractiveTimeout(
|
|
5731
|
+
`${endpoint.url}/chat/completions`,
|
|
5732
|
+
{
|
|
5733
|
+
method: "POST",
|
|
5734
|
+
headers: {
|
|
5735
|
+
"Content-Type": "application/json",
|
|
5736
|
+
Authorization: `Bearer ${endpoint.apiKey}`
|
|
5737
|
+
},
|
|
5738
|
+
body: JSON.stringify({
|
|
5739
|
+
model: endpoint.model,
|
|
5740
|
+
messages: [
|
|
5741
|
+
{ role: "system", content: systemPrompt },
|
|
5742
|
+
{ role: "user", content: question }
|
|
5743
|
+
],
|
|
5744
|
+
temperature: 0.1,
|
|
5745
|
+
max_tokens: DEFAULT_LLM_MAX_TOKENS
|
|
5746
|
+
}),
|
|
5747
|
+
locale: cfg.locale
|
|
5748
|
+
}
|
|
5749
|
+
);
|
|
5691
5750
|
return readChatContent(res, "Translation");
|
|
5692
5751
|
}
|
|
5693
5752
|
async function isLlmOnline(url) {
|
|
@@ -5760,6 +5819,107 @@ async function checkProviderChain(primary) {
|
|
|
5760
5819
|
}
|
|
5761
5820
|
return { primary: first };
|
|
5762
5821
|
}
|
|
5822
|
+
async function resolveUsableRecallEndpoint(db) {
|
|
5823
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5824
|
+
if (!cfg.enabled) {
|
|
5825
|
+
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5826
|
+
}
|
|
5827
|
+
assertChatCompletions(cfg);
|
|
5828
|
+
const signature = recallEndpointSignature(cfg);
|
|
5829
|
+
if (cachedRecallEndpoint && cachedRecallEndpoint.signature === signature && cachedRecallEndpoint.expiresAt > Date.now()) {
|
|
5830
|
+
return cachedRecallEndpoint.endpoint;
|
|
5831
|
+
}
|
|
5832
|
+
const chain = await checkProviderChain(cfg);
|
|
5833
|
+
const selected = chain.firstUsable;
|
|
5834
|
+
if (!selected || !isEndpointUsable(selected)) {
|
|
5835
|
+
throw new Error("No recall LLM endpoint is online");
|
|
5836
|
+
}
|
|
5837
|
+
cachedRecallEndpoint = {
|
|
5838
|
+
endpoint: selected.endpoint,
|
|
5839
|
+
signature,
|
|
5840
|
+
expiresAt: Date.now() + RECALL_ENDPOINT_CACHE_MS
|
|
5841
|
+
};
|
|
5842
|
+
return selected.endpoint;
|
|
5843
|
+
}
|
|
5844
|
+
async function prepareRecallChain(db, opts) {
|
|
5845
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5846
|
+
const fail = (reason, partial = {}) => ({
|
|
5847
|
+
usable: false,
|
|
5848
|
+
reason,
|
|
5849
|
+
online: false,
|
|
5850
|
+
model: cfg.model,
|
|
5851
|
+
availableModels: [],
|
|
5852
|
+
providerName: cfg.providerName,
|
|
5853
|
+
label: cfg.label,
|
|
5854
|
+
local: cfg.local ?? isLocalEndpoint(cfg.url),
|
|
5855
|
+
activeTier: "primary",
|
|
5856
|
+
...partial
|
|
5857
|
+
});
|
|
5858
|
+
if (!cfg.enabled) return fail("disabled");
|
|
5859
|
+
if (cfg.apiFlavor !== "chat-completions") return fail("unsupported-provider");
|
|
5860
|
+
const chain = providerChain(cfg);
|
|
5861
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
5862
|
+
let lastReason = "offline";
|
|
5863
|
+
let lastOnline = false;
|
|
5864
|
+
let lastModel = cfg.model;
|
|
5865
|
+
let lastAvailable = [];
|
|
5866
|
+
for (let index = 0; index < chain.length; index++) {
|
|
5867
|
+
const endpoint = chain[index];
|
|
5868
|
+
let online = await isLlmOnline(endpoint.url);
|
|
5869
|
+
if (!online && (endpoint.local ?? isLocalEndpoint(endpoint.url))) {
|
|
5870
|
+
if (opts.interactive) {
|
|
5871
|
+
online = await startLocalRunner(
|
|
5872
|
+
endpoint.url,
|
|
5873
|
+
endpoint.model,
|
|
5874
|
+
cfg.locale,
|
|
5875
|
+
endpoint.runner
|
|
5876
|
+
);
|
|
5877
|
+
} else {
|
|
5878
|
+
spawnLocalRunner(endpoint.url, endpoint.model, endpoint.runner);
|
|
5879
|
+
while (Date.now() < deadline) {
|
|
5880
|
+
await new Promise((resolve10) => setTimeout(resolve10, 1e3));
|
|
5881
|
+
if (await isLlmOnline(endpoint.url)) {
|
|
5882
|
+
online = true;
|
|
5883
|
+
break;
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
lastOnline = online;
|
|
5889
|
+
lastModel = endpoint.model;
|
|
5890
|
+
if (!online) {
|
|
5891
|
+
lastReason = "offline";
|
|
5892
|
+
continue;
|
|
5893
|
+
}
|
|
5894
|
+
const availableModels = await getAvailableModels(
|
|
5895
|
+
endpoint.url,
|
|
5896
|
+
endpoint.apiKey
|
|
5897
|
+
);
|
|
5898
|
+
lastAvailable = availableModels;
|
|
5899
|
+
const modelKnown = availableModels.length === 0 || availableModels.some(
|
|
5900
|
+
(candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
|
|
5901
|
+
);
|
|
5902
|
+
if (!modelKnown) {
|
|
5903
|
+
lastReason = "model-not-found";
|
|
5904
|
+
continue;
|
|
5905
|
+
}
|
|
5906
|
+
return {
|
|
5907
|
+
usable: true,
|
|
5908
|
+
online: true,
|
|
5909
|
+
model: endpoint.model,
|
|
5910
|
+
availableModels,
|
|
5911
|
+
providerName: endpoint.providerName,
|
|
5912
|
+
label: endpoint.label,
|
|
5913
|
+
local: endpoint.local ?? isLocalEndpoint(endpoint.url),
|
|
5914
|
+
activeTier: index === 0 ? "primary" : "fallback"
|
|
5915
|
+
};
|
|
5916
|
+
}
|
|
5917
|
+
return fail(lastReason, {
|
|
5918
|
+
online: lastOnline,
|
|
5919
|
+
model: lastModel,
|
|
5920
|
+
availableModels: lastAvailable
|
|
5921
|
+
});
|
|
5922
|
+
}
|
|
5763
5923
|
async function isVisionProviderModelExplicit(db, active) {
|
|
5764
5924
|
if (await getSetting(db, "llm.vision.model")) return true;
|
|
5765
5925
|
const providers = await readJsonSetting(db, "llm.providers");
|
|
@@ -5791,7 +5951,7 @@ async function checkVisionReadiness(db) {
|
|
|
5791
5951
|
let warning;
|
|
5792
5952
|
if (cfg.enabled && online && modelAvailable && !visionModelExplicit) {
|
|
5793
5953
|
const cloudRec = getCloudModelRecommendation(active.url);
|
|
5794
|
-
if (cloudRec && active.model === cloudRec) {
|
|
5954
|
+
if (cloudRec && active.model === cloudRec.model) {
|
|
5795
5955
|
} else {
|
|
5796
5956
|
warning = `No explicit vision model configured (llm.vision.model). Falling back to base model "${active.model}", which may not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`;
|
|
5797
5957
|
}
|
|
@@ -5861,13 +6021,8 @@ async function getProviderRoleStatus(db, role) {
|
|
|
5861
6021
|
fallback: summarizeFallback(cfg.fallback)
|
|
5862
6022
|
};
|
|
5863
6023
|
}
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
const chain = await checkProviderChain(cfg);
|
|
5867
|
-
selected = chain.firstUsable ?? chain.primary;
|
|
5868
|
-
} else {
|
|
5869
|
-
selected = await checkProviderEndpoint(cfg);
|
|
5870
|
-
}
|
|
6024
|
+
const chain = await checkProviderChain(cfg);
|
|
6025
|
+
const selected = chain.firstUsable ?? chain.primary;
|
|
5871
6026
|
const active = selected.endpoint;
|
|
5872
6027
|
const usable = selected.online && selected.modelAvailable;
|
|
5873
6028
|
const reason = usable ? void 0 : selected.online ? "model-not-found" : "offline";
|
|
@@ -5889,7 +6044,35 @@ async function getProviderRoleStatus(db, role) {
|
|
|
5889
6044
|
fallback: summarizeFallback(cfg.fallback)
|
|
5890
6045
|
};
|
|
5891
6046
|
}
|
|
5892
|
-
function
|
|
6047
|
+
function runnerKindFromHint(hint) {
|
|
6048
|
+
if (!hint) return void 0;
|
|
6049
|
+
const normalized = hint.trim().toLowerCase();
|
|
6050
|
+
if (normalized === "flm" || normalized === "fastflowlm") {
|
|
6051
|
+
return "fastflowlm";
|
|
6052
|
+
}
|
|
6053
|
+
if (normalized === "ollama") return "ollama";
|
|
6054
|
+
if (normalized === "foundry-local" || normalized === "foundry" || normalized === "generic") {
|
|
6055
|
+
return "generic";
|
|
6056
|
+
}
|
|
6057
|
+
return void 0;
|
|
6058
|
+
}
|
|
6059
|
+
function defaultPortForRunner(runner, url) {
|
|
6060
|
+
try {
|
|
6061
|
+
const urlObj = new URL(url);
|
|
6062
|
+
const explicit = urlObj.port;
|
|
6063
|
+
if (explicit) return explicit;
|
|
6064
|
+
if (runner === "ollama") return "11434";
|
|
6065
|
+
if (urlObj.protocol === "https:") return "443";
|
|
6066
|
+
return "80";
|
|
6067
|
+
} catch {
|
|
6068
|
+
return runner === "ollama" ? "11434" : "8000";
|
|
6069
|
+
}
|
|
6070
|
+
}
|
|
6071
|
+
function detectRunner(url, model, hint) {
|
|
6072
|
+
const fromHint = runnerKindFromHint(hint);
|
|
6073
|
+
if (fromHint) {
|
|
6074
|
+
return { runner: fromHint, port: defaultPortForRunner(fromHint, url) };
|
|
6075
|
+
}
|
|
5893
6076
|
let runner = "unknown";
|
|
5894
6077
|
let port = "8000";
|
|
5895
6078
|
try {
|
|
@@ -5905,8 +6088,8 @@ function detectRunner(url, model) {
|
|
|
5905
6088
|
}
|
|
5906
6089
|
return { runner, port };
|
|
5907
6090
|
}
|
|
5908
|
-
function spawnLocalRunner(url, model) {
|
|
5909
|
-
const { runner, port } = detectRunner(url, model);
|
|
6091
|
+
function spawnLocalRunner(url, model, hint) {
|
|
6092
|
+
const { runner, port } = detectRunner(url, model, hint);
|
|
5910
6093
|
try {
|
|
5911
6094
|
if (runner === "fastflowlm") {
|
|
5912
6095
|
const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
|
|
@@ -5927,8 +6110,8 @@ function spawnLocalRunner(url, model) {
|
|
|
5927
6110
|
} catch {
|
|
5928
6111
|
}
|
|
5929
6112
|
}
|
|
5930
|
-
async function startLocalRunner(url, model, locale) {
|
|
5931
|
-
const { runner, port } = detectRunner(url, model);
|
|
6113
|
+
async function startLocalRunner(url, model, locale, hint) {
|
|
6114
|
+
const { runner, port } = detectRunner(url, model, hint);
|
|
5932
6115
|
if (runner === "fastflowlm") {
|
|
5933
6116
|
const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
|
|
5934
6117
|
if (!hasCommand("flm") && flmExe === "flm") {
|
|
@@ -5987,7 +6170,7 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5987
6170
|
let attempts = 0;
|
|
5988
6171
|
const dotsPerLine = 30;
|
|
5989
6172
|
while (true) {
|
|
5990
|
-
await new Promise((
|
|
6173
|
+
await new Promise((resolve10) => setTimeout(resolve10, 500));
|
|
5991
6174
|
if (await isLlmOnline(url)) {
|
|
5992
6175
|
if (attempts > 0) process.stdout.write("\n");
|
|
5993
6176
|
return true;
|
|
@@ -6012,105 +6195,39 @@ async function startLocalRunner(url, model, locale) {
|
|
|
6012
6195
|
}
|
|
6013
6196
|
}
|
|
6014
6197
|
async function ensureLocalLlmRunning(db) {
|
|
6015
|
-
const
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
}
|
|
6019
|
-
if (
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
return { usable: false, reason: "unsupported-provider" };
|
|
6024
|
-
}
|
|
6025
|
-
const { url, model, apiKey, locale } = cfg;
|
|
6026
|
-
const isLocal = isLocalEndpoint(url);
|
|
6027
|
-
console.log(`Checking if local LLM server is online at ${url}...`);
|
|
6028
|
-
let online = await isLlmOnline(url);
|
|
6029
|
-
if (!online && isLocal) {
|
|
6030
|
-
console.log(`\x1B[33m\u26A0 Local LLM server is offline on ${url}.\x1B[0m`);
|
|
6031
|
-
online = await startLocalRunner(url, model, locale);
|
|
6032
|
-
}
|
|
6033
|
-
if (!online) {
|
|
6034
|
-
console.warn(
|
|
6035
|
-
`\x1B[33m\u26A0 LLM server is not reachable at ${url}. Continuing without AI coaching.\x1B[0m
|
|
6036
|
-
`
|
|
6198
|
+
const result = await prepareRecallChain(db, {
|
|
6199
|
+
timeoutMs: 25e3,
|
|
6200
|
+
interactive: true
|
|
6201
|
+
});
|
|
6202
|
+
if (result.usable) {
|
|
6203
|
+
const location = result.local ? "local" : "cloud";
|
|
6204
|
+
console.log(
|
|
6205
|
+
`\x1B[32m\u2713 Recall LLM ready (${location}: ${result.model}).\x1B[0m`
|
|
6037
6206
|
);
|
|
6038
|
-
return { usable:
|
|
6207
|
+
return { usable: true };
|
|
6039
6208
|
}
|
|
6040
|
-
|
|
6041
|
-
const available = await getAvailableModels(url, apiKey);
|
|
6042
|
-
const modelKnown = available.length === 0 || available.some((m) => m.toLowerCase() === model.toLowerCase());
|
|
6043
|
-
if (!modelKnown) {
|
|
6209
|
+
if (result.reason === "unsupported-provider") {
|
|
6044
6210
|
console.warn(
|
|
6045
|
-
`\x1B[31m\u2717
|
|
6211
|
+
`\x1B[31m\u2717 Recall provider is not supported for active recall.\x1B[0m`
|
|
6046
6212
|
);
|
|
6047
|
-
|
|
6213
|
+
} else if (result.reason === "model-not-found") {
|
|
6048
6214
|
console.warn(
|
|
6049
|
-
|
|
6215
|
+
`\x1B[31m\u2717 Configured model "${result.model}" is not available on the server.\x1B[0m`
|
|
6050
6216
|
);
|
|
6217
|
+
console.warn(` Available models: ${result.availableModels.join(", ")}`);
|
|
6218
|
+
} else if (result.reason === "offline") {
|
|
6051
6219
|
console.warn(
|
|
6052
|
-
|
|
6220
|
+
`\x1B[33m\u26A0 No recall LLM endpoint is reachable. Continuing without AI coaching.\x1B[0m
|
|
6221
|
+
`
|
|
6053
6222
|
);
|
|
6054
|
-
return { usable: false, reason: "model-not-found" };
|
|
6055
6223
|
}
|
|
6056
|
-
return { usable:
|
|
6224
|
+
return { usable: false, reason: result.reason };
|
|
6057
6225
|
}
|
|
6058
6226
|
async function ensureLlmReadyHeadless(db, opts = {}) {
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
return {
|
|
6064
|
-
usable: false,
|
|
6065
|
-
reason: "disabled",
|
|
6066
|
-
online: false,
|
|
6067
|
-
model,
|
|
6068
|
-
availableModels: []
|
|
6069
|
-
};
|
|
6070
|
-
}
|
|
6071
|
-
if (cfg.apiFlavor !== "chat-completions") {
|
|
6072
|
-
return {
|
|
6073
|
-
usable: false,
|
|
6074
|
-
reason: "unsupported-provider",
|
|
6075
|
-
online: false,
|
|
6076
|
-
model,
|
|
6077
|
-
availableModels: []
|
|
6078
|
-
};
|
|
6079
|
-
}
|
|
6080
|
-
const isLocal = isLocalEndpoint(url);
|
|
6081
|
-
let online = await isLlmOnline(url);
|
|
6082
|
-
if (!online && isLocal) {
|
|
6083
|
-
spawnLocalRunner(url, model);
|
|
6084
|
-
const deadline = Date.now() + timeoutMs;
|
|
6085
|
-
while (Date.now() < deadline) {
|
|
6086
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
6087
|
-
if (await isLlmOnline(url)) {
|
|
6088
|
-
online = true;
|
|
6089
|
-
break;
|
|
6090
|
-
}
|
|
6091
|
-
}
|
|
6092
|
-
}
|
|
6093
|
-
if (!online) {
|
|
6094
|
-
return {
|
|
6095
|
-
usable: false,
|
|
6096
|
-
reason: "offline",
|
|
6097
|
-
online: false,
|
|
6098
|
-
model,
|
|
6099
|
-
availableModels: []
|
|
6100
|
-
};
|
|
6101
|
-
}
|
|
6102
|
-
const availableModels = await getAvailableModels(url, apiKey);
|
|
6103
|
-
const modelKnown = availableModels.length === 0 || availableModels.some((m) => m.toLowerCase() === model.toLowerCase());
|
|
6104
|
-
if (!modelKnown) {
|
|
6105
|
-
return {
|
|
6106
|
-
usable: false,
|
|
6107
|
-
reason: "model-not-found",
|
|
6108
|
-
online: true,
|
|
6109
|
-
model,
|
|
6110
|
-
availableModels
|
|
6111
|
-
};
|
|
6112
|
-
}
|
|
6113
|
-
return { usable: true, online: true, model, availableModels };
|
|
6227
|
+
return prepareRecallChain(db, {
|
|
6228
|
+
timeoutMs: opts.timeoutMs ?? 25e3,
|
|
6229
|
+
interactive: false
|
|
6230
|
+
});
|
|
6114
6231
|
}
|
|
6115
6232
|
async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
6116
6233
|
const {
|
|
@@ -6142,8 +6259,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
|
6142
6259
|
const dotsPerLine = 30;
|
|
6143
6260
|
while (true) {
|
|
6144
6261
|
let timeoutId;
|
|
6145
|
-
const timeoutPromise = new Promise((
|
|
6146
|
-
timeoutId = setTimeout(() =>
|
|
6262
|
+
const timeoutPromise = new Promise((resolve10) => {
|
|
6263
|
+
timeoutId = setTimeout(() => resolve10("timeout"), timeoutMs);
|
|
6147
6264
|
});
|
|
6148
6265
|
const dotsInterval = setInterval(() => {
|
|
6149
6266
|
process.stdout.write(".");
|
|
@@ -6200,15 +6317,22 @@ async function ensureHighQualityQuestion(db, token) {
|
|
|
6200
6317
|
bloomLevel: token.bloomLevel,
|
|
6201
6318
|
sourceLinkContent
|
|
6202
6319
|
});
|
|
6203
|
-
if (generated
|
|
6204
|
-
await updateToken(db, token.slug, { question: generated });
|
|
6205
|
-
return
|
|
6320
|
+
if (generated.text.trim().length > 0) {
|
|
6321
|
+
await updateToken(db, token.slug, { question: generated.text });
|
|
6322
|
+
return {
|
|
6323
|
+
question: generated.text,
|
|
6324
|
+
source: "llm",
|
|
6325
|
+
model: generated.model
|
|
6326
|
+
};
|
|
6206
6327
|
}
|
|
6207
6328
|
} catch {
|
|
6208
6329
|
}
|
|
6209
6330
|
}
|
|
6210
6331
|
if (token.question && token.question.trim().length > 0) {
|
|
6211
|
-
return
|
|
6332
|
+
return {
|
|
6333
|
+
question: token.question.trim(),
|
|
6334
|
+
source: "original"
|
|
6335
|
+
};
|
|
6212
6336
|
}
|
|
6213
6337
|
return null;
|
|
6214
6338
|
}
|
|
@@ -6253,7 +6377,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
|
|
|
6253
6377
|
const imageUrls = [];
|
|
6254
6378
|
const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
|
|
6255
6379
|
if (isVideo) {
|
|
6256
|
-
const { mkdirSync: mkdirSync14, readdirSync:
|
|
6380
|
+
const { mkdirSync: mkdirSync14, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
|
|
6257
6381
|
const { execSync: execSync6 } = await import("child_process");
|
|
6258
6382
|
const tempDir = join14(
|
|
6259
6383
|
tmpdir2(),
|
|
@@ -6265,13 +6389,13 @@ async function observeUiSnapshotViaLLM(db, input8) {
|
|
|
6265
6389
|
`ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
|
|
6266
6390
|
{ stdio: "ignore" }
|
|
6267
6391
|
);
|
|
6268
|
-
let files =
|
|
6392
|
+
let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
|
|
6269
6393
|
if (files.length === 0) {
|
|
6270
6394
|
execSync6(
|
|
6271
6395
|
`ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
|
|
6272
6396
|
{ stdio: "ignore" }
|
|
6273
6397
|
);
|
|
6274
|
-
files =
|
|
6398
|
+
files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
|
|
6275
6399
|
}
|
|
6276
6400
|
const maxFrames = cfg.maxFrames ?? 100;
|
|
6277
6401
|
let sampledFiles = files;
|
|
@@ -6416,7 +6540,7 @@ async function requestChatCompletionsVisionDraft(args) {
|
|
|
6416
6540
|
}
|
|
6417
6541
|
],
|
|
6418
6542
|
temperature: 0,
|
|
6419
|
-
max_tokens: args.input.maxTokens ??
|
|
6543
|
+
max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
|
|
6420
6544
|
...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
|
|
6421
6545
|
}),
|
|
6422
6546
|
locale: args.locale,
|
|
@@ -6472,7 +6596,7 @@ async function requestAnthropicVisionDraft(args) {
|
|
|
6472
6596
|
},
|
|
6473
6597
|
body: JSON.stringify({
|
|
6474
6598
|
model: args.model,
|
|
6475
|
-
max_tokens: args.input.maxTokens ??
|
|
6599
|
+
max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
|
|
6476
6600
|
system: VISION_SYSTEM_PROMPT,
|
|
6477
6601
|
messages: [
|
|
6478
6602
|
{
|
|
@@ -6627,54 +6751,165 @@ function isRecord2(value) {
|
|
|
6627
6751
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6628
6752
|
}
|
|
6629
6753
|
|
|
6630
|
-
// src/cli/commands/
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
|
|
6635
|
-
await setSetting(db, "user.id", userId);
|
|
6636
|
-
return userId;
|
|
6754
|
+
// src/cli/commands/shared/db.ts
|
|
6755
|
+
function defaultErrorHandler(message) {
|
|
6756
|
+
console.error("Error:", message);
|
|
6757
|
+
process.exit(1);
|
|
6637
6758
|
}
|
|
6638
|
-
async function
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
}
|
|
6646
|
-
|
|
6759
|
+
async function withDb(fn, onError = defaultErrorHandler) {
|
|
6760
|
+
let db;
|
|
6761
|
+
try {
|
|
6762
|
+
db = await openDatabase();
|
|
6763
|
+
await fn(db);
|
|
6764
|
+
} catch (err) {
|
|
6765
|
+
onError(err.message);
|
|
6766
|
+
} finally {
|
|
6767
|
+
await db?.close();
|
|
6647
6768
|
}
|
|
6648
|
-
|
|
6769
|
+
}
|
|
6770
|
+
function jsonOut(data) {
|
|
6771
|
+
console.log(JSON.stringify(data, null, 2));
|
|
6649
6772
|
}
|
|
6650
6773
|
|
|
6651
|
-
// src/cli/
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
{
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6774
|
+
// src/cli/providers/config.ts
|
|
6775
|
+
var VALID_API_FLAVORS = [
|
|
6776
|
+
"chat-completions",
|
|
6777
|
+
"anthropic-messages"
|
|
6778
|
+
];
|
|
6779
|
+
var VALID_ROLES = ["vision", "recall", "text"];
|
|
6780
|
+
function upsertProviderRecord(providers, name, patch) {
|
|
6781
|
+
const merged = { ...providers[name] ?? {} };
|
|
6782
|
+
if (patch.url !== void 0) merged.url = patch.url;
|
|
6783
|
+
if (patch.model !== void 0) merged.model = patch.model;
|
|
6784
|
+
if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
|
|
6785
|
+
if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
|
|
6786
|
+
if (patch.label !== void 0) merged.label = patch.label;
|
|
6787
|
+
if (patch.local !== void 0) merged.local = patch.local;
|
|
6788
|
+
if (patch.runner !== void 0) merged.runner = patch.runner;
|
|
6789
|
+
return { ...providers, [name]: merged };
|
|
6790
|
+
}
|
|
6791
|
+
function removeProviderRecord(providers, name) {
|
|
6792
|
+
if (!(name in providers)) return { providers, removed: false };
|
|
6793
|
+
const next = { ...providers };
|
|
6794
|
+
delete next[name];
|
|
6795
|
+
return { providers: next, removed: true };
|
|
6796
|
+
}
|
|
6797
|
+
function rolesReferencing(roles, name) {
|
|
6798
|
+
return VALID_ROLES.filter((role) => {
|
|
6799
|
+
const binding = roles[role];
|
|
6800
|
+
return binding?.primary === name || binding?.fallback === name;
|
|
6801
|
+
});
|
|
6802
|
+
}
|
|
6803
|
+
function bindRoleProviders(roles, role, primary, fallback) {
|
|
6804
|
+
const binding = { primary };
|
|
6805
|
+
if (fallback) binding.fallback = fallback;
|
|
6806
|
+
return { ...roles, [role]: binding };
|
|
6807
|
+
}
|
|
6808
|
+
function maskSecret(key) {
|
|
6809
|
+
return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
|
|
6810
|
+
}
|
|
6811
|
+
function buildProviderListing(providers, hasKey) {
|
|
6812
|
+
return Object.entries(providers).map(([name, rec]) => {
|
|
6813
|
+
const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
|
|
6814
|
+
let keyState;
|
|
6815
|
+
if (!rec.apiKeyRef) keyState = "none";
|
|
6816
|
+
else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
|
|
6817
|
+
const row = {
|
|
6818
|
+
name,
|
|
6819
|
+
url: rec.url,
|
|
6820
|
+
model: rec.model,
|
|
6821
|
+
apiFlavor,
|
|
6822
|
+
apiKeyRef: rec.apiKeyRef,
|
|
6823
|
+
keyState
|
|
6824
|
+
};
|
|
6825
|
+
if (rec.label !== void 0) row.label = rec.label;
|
|
6826
|
+
if (rec.local !== void 0) row.local = rec.local;
|
|
6827
|
+
if (rec.runner !== void 0) row.runner = rec.runner;
|
|
6828
|
+
return row;
|
|
6829
|
+
});
|
|
6830
|
+
}
|
|
6831
|
+
function findOrphanKeyRefs(storedRefs, providers) {
|
|
6832
|
+
const used = new Set(
|
|
6833
|
+
Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
|
|
6834
|
+
);
|
|
6835
|
+
return storedRefs.filter((ref) => !used.has(ref));
|
|
6836
|
+
}
|
|
6837
|
+
async function readJson(db, key, fallback) {
|
|
6838
|
+
const raw = await getSetting(db, key);
|
|
6839
|
+
if (!raw) return fallback;
|
|
6840
|
+
try {
|
|
6841
|
+
return JSON.parse(raw);
|
|
6842
|
+
} catch {
|
|
6843
|
+
return fallback;
|
|
6844
|
+
}
|
|
6845
|
+
}
|
|
6846
|
+
var readProviders = (db) => readJson(db, "llm.providers", {});
|
|
6847
|
+
var readRoles = (db) => readJson(db, "llm.roles", {});
|
|
6848
|
+
async function readScopedProviders(db, machine) {
|
|
6849
|
+
if (machine) return getMachineAiConfig().providers ?? {};
|
|
6850
|
+
if (!db)
|
|
6851
|
+
throw new Error("Database is required for shared provider settings.");
|
|
6852
|
+
return readProviders(db);
|
|
6853
|
+
}
|
|
6854
|
+
async function readScopedRoles(db, machine) {
|
|
6855
|
+
if (machine) return getMachineAiConfig().roles ?? {};
|
|
6856
|
+
if (!db)
|
|
6857
|
+
throw new Error("Database is required for shared provider settings.");
|
|
6858
|
+
return readRoles(db);
|
|
6859
|
+
}
|
|
6860
|
+
async function writeScopedProviders(db, machine, p) {
|
|
6861
|
+
if (machine) {
|
|
6862
|
+
const config = getMachineAiConfig();
|
|
6863
|
+
saveMachineAiConfig({ ...config, providers: p });
|
|
6864
|
+
return;
|
|
6865
|
+
}
|
|
6866
|
+
if (!db)
|
|
6867
|
+
throw new Error("Database is required for shared provider settings.");
|
|
6868
|
+
await setSetting(db, "llm.providers", JSON.stringify(p));
|
|
6869
|
+
}
|
|
6870
|
+
async function writeScopedRoles(db, machine, r) {
|
|
6871
|
+
if (machine) {
|
|
6872
|
+
const config = getMachineAiConfig();
|
|
6873
|
+
saveMachineAiConfig({ ...config, roles: r });
|
|
6874
|
+
return;
|
|
6875
|
+
}
|
|
6876
|
+
if (!db)
|
|
6877
|
+
throw new Error("Database is required for shared provider settings.");
|
|
6878
|
+
await setSetting(db, "llm.roles", JSON.stringify(r));
|
|
6879
|
+
}
|
|
6880
|
+
async function withProviderScope(machine, action) {
|
|
6881
|
+
if (machine) {
|
|
6882
|
+
await action(void 0);
|
|
6883
|
+
return;
|
|
6884
|
+
}
|
|
6885
|
+
await withDb(action);
|
|
6886
|
+
}
|
|
6887
|
+
|
|
6888
|
+
// src/cli/provisioning/index.ts
|
|
6889
|
+
import {
|
|
6890
|
+
existsSync as existsSync15,
|
|
6891
|
+
lstatSync,
|
|
6892
|
+
mkdirSync as mkdirSync8,
|
|
6893
|
+
readFileSync as readFileSync10,
|
|
6894
|
+
realpathSync,
|
|
6895
|
+
rmSync as rmSync2,
|
|
6896
|
+
symlinkSync,
|
|
6897
|
+
writeFileSync as writeFileSync7
|
|
6898
|
+
} from "fs";
|
|
6899
|
+
import { basename as basename4, dirname as dirname5, join as join15 } from "path";
|
|
6900
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6901
|
+
var packageRoot = [
|
|
6902
|
+
fileURLToPath2(new URL("../..", import.meta.url)),
|
|
6903
|
+
fileURLToPath2(new URL("../../..", import.meta.url))
|
|
6904
|
+
].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
|
|
6905
|
+
var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
|
|
6906
|
+
var SKILL_PAIRS = [
|
|
6907
|
+
{
|
|
6908
|
+
from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
|
|
6909
|
+
to: join15(".claude", "skills", "zam", "SKILL.md"),
|
|
6910
|
+
agents: ["claude", "copilot"]
|
|
6911
|
+
},
|
|
6912
|
+
{
|
|
6678
6913
|
from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
|
|
6679
6914
|
to: join15(".agent", "skills", "zam", "SKILL.md"),
|
|
6680
6915
|
agents: ["agent"]
|
|
@@ -6717,6 +6952,13 @@ function pathExists(path) {
|
|
|
6717
6952
|
return false;
|
|
6718
6953
|
}
|
|
6719
6954
|
}
|
|
6955
|
+
function isSymbolicLink(path) {
|
|
6956
|
+
try {
|
|
6957
|
+
return lstatSync(path).isSymbolicLink();
|
|
6958
|
+
} catch {
|
|
6959
|
+
return false;
|
|
6960
|
+
}
|
|
6961
|
+
}
|
|
6720
6962
|
function comparableRealPath(path) {
|
|
6721
6963
|
const real = realpathSync(path);
|
|
6722
6964
|
return process.platform === "win32" ? real.toLowerCase() : real;
|
|
@@ -6729,24 +6971,29 @@ function pathsResolveToSameDirectory(sourceDir, destinationDir) {
|
|
|
6729
6971
|
}
|
|
6730
6972
|
}
|
|
6731
6973
|
function linkPointsTo(sourceDir, destinationDir) {
|
|
6732
|
-
|
|
6733
|
-
return lstatSync(destinationDir).isSymbolicLink() && pathsResolveToSameDirectory(sourceDir, destinationDir);
|
|
6734
|
-
} catch {
|
|
6735
|
-
return false;
|
|
6736
|
-
}
|
|
6974
|
+
return isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir);
|
|
6737
6975
|
}
|
|
6738
|
-
function
|
|
6976
|
+
function isZamSkillCopy(destinationDir) {
|
|
6739
6977
|
try {
|
|
6740
6978
|
if (!lstatSync(destinationDir).isDirectory()) return false;
|
|
6741
|
-
const
|
|
6742
|
-
if (
|
|
6743
|
-
return /^name:\s*zam\s*$/m.test(
|
|
6744
|
-
readFileSync10(join15(destinationDir, "SKILL.md"), "utf8")
|
|
6745
|
-
);
|
|
6979
|
+
const skillFile = join15(destinationDir, "SKILL.md");
|
|
6980
|
+
if (!existsSync15(skillFile)) return false;
|
|
6981
|
+
return /^name:\s*zam\s*$/m.test(readFileSync10(skillFile, "utf8"));
|
|
6746
6982
|
} catch {
|
|
6747
6983
|
return false;
|
|
6748
6984
|
}
|
|
6749
6985
|
}
|
|
6986
|
+
function classifySkillDestination(sourceDir, destinationDir) {
|
|
6987
|
+
if (!existsSync15(sourceDir)) return "source-missing";
|
|
6988
|
+
if (!isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir)) {
|
|
6989
|
+
return "source-directory";
|
|
6990
|
+
}
|
|
6991
|
+
if (linkPointsTo(sourceDir, destinationDir)) return "linked";
|
|
6992
|
+
if (!pathExists(destinationDir)) return "missing";
|
|
6993
|
+
if (isSymbolicLink(destinationDir)) return "broken";
|
|
6994
|
+
if (isZamSkillCopy(destinationDir)) return "stale-copy";
|
|
6995
|
+
return "unmanaged";
|
|
6996
|
+
}
|
|
6750
6997
|
function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
|
|
6751
6998
|
const results = [];
|
|
6752
6999
|
const log = (message) => {
|
|
@@ -6756,7 +7003,9 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
|
|
|
6756
7003
|
if (!pairAgents.some((agent) => agents.has(agent))) continue;
|
|
6757
7004
|
const sourceDir = dirname5(from);
|
|
6758
7005
|
const destinationDir = dirname5(join15(cwd, to));
|
|
6759
|
-
|
|
7006
|
+
const label = dirname5(to);
|
|
7007
|
+
const state = classifySkillDestination(sourceDir, destinationDir);
|
|
7008
|
+
if (state === "source-missing") {
|
|
6760
7009
|
if (!opts.quiet) {
|
|
6761
7010
|
console.warn(` warn source not found, skipping: ${sourceDir}`);
|
|
6762
7011
|
}
|
|
@@ -6768,8 +7017,8 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
|
|
|
6768
7017
|
});
|
|
6769
7018
|
continue;
|
|
6770
7019
|
}
|
|
6771
|
-
if (
|
|
6772
|
-
log(` skip ${
|
|
7020
|
+
if (state === "source-directory") {
|
|
7021
|
+
log(` skip ${label} (package source)`);
|
|
6773
7022
|
results.push({
|
|
6774
7023
|
source: sourceDir,
|
|
6775
7024
|
destination: destinationDir,
|
|
@@ -6778,8 +7027,8 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
|
|
|
6778
7027
|
});
|
|
6779
7028
|
continue;
|
|
6780
7029
|
}
|
|
6781
|
-
if (
|
|
6782
|
-
log(` skip ${
|
|
7030
|
+
if (state === "linked") {
|
|
7031
|
+
log(` skip ${label} (already linked)`);
|
|
6783
7032
|
results.push({
|
|
6784
7033
|
source: sourceDir,
|
|
6785
7034
|
destination: destinationDir,
|
|
@@ -6788,12 +7037,12 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
|
|
|
6788
7037
|
});
|
|
6789
7038
|
continue;
|
|
6790
7039
|
}
|
|
6791
|
-
const destinationExists =
|
|
6792
|
-
const replaceExisting = destinationExists && (Boolean(opts.force) ||
|
|
7040
|
+
const destinationExists = state !== "missing";
|
|
7041
|
+
const replaceExisting = destinationExists && (Boolean(opts.force) || state === "broken" || state === "stale-copy");
|
|
6793
7042
|
if (destinationExists && !replaceExisting) {
|
|
6794
7043
|
if (!opts.quiet) {
|
|
6795
7044
|
console.warn(
|
|
6796
|
-
` warn ${
|
|
7045
|
+
` warn ${label} already exists and is not managed by ZAM; use --force to replace it`
|
|
6797
7046
|
);
|
|
6798
7047
|
}
|
|
6799
7048
|
results.push({
|
|
@@ -6806,7 +7055,7 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
|
|
|
6806
7055
|
}
|
|
6807
7056
|
const action = destinationExists ? "relinked" : "linked";
|
|
6808
7057
|
if (opts.dryRun) {
|
|
6809
|
-
log(` would ${action === "linked" ? "link" : "relink"} ${
|
|
7058
|
+
log(` would ${action === "linked" ? "link" : "relink"} ${label}`);
|
|
6810
7059
|
} else {
|
|
6811
7060
|
if (destinationExists) {
|
|
6812
7061
|
rmSync2(destinationDir, { recursive: true, force: true });
|
|
@@ -6817,39 +7066,36 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
|
|
|
6817
7066
|
destinationDir,
|
|
6818
7067
|
process.platform === "win32" ? "junction" : "dir"
|
|
6819
7068
|
);
|
|
6820
|
-
log(` ${action === "linked" ? "link" : "relink"} ${
|
|
7069
|
+
log(` ${action === "linked" ? "link" : "relink"} ${label}`);
|
|
6821
7070
|
}
|
|
6822
7071
|
results.push({ source: sourceDir, destination: destinationDir, action });
|
|
6823
7072
|
}
|
|
6824
7073
|
return results;
|
|
6825
7074
|
}
|
|
6826
|
-
function
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
7075
|
+
function inspectSkillLinks(cwd = process.cwd(), agents = parseSetupAgents()) {
|
|
7076
|
+
const results = [];
|
|
7077
|
+
for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
|
|
7078
|
+
if (!pairAgents.some((agent) => agents.has(agent))) continue;
|
|
7079
|
+
const sourceDir = dirname5(from);
|
|
7080
|
+
const destinationDir = dirname5(join15(cwd, to));
|
|
7081
|
+
results.push({
|
|
7082
|
+
agents: pairAgents,
|
|
7083
|
+
source: sourceDir,
|
|
7084
|
+
destination: destinationDir,
|
|
7085
|
+
state: classifySkillDestination(sourceDir, destinationDir)
|
|
7086
|
+
});
|
|
6836
7087
|
}
|
|
7088
|
+
return results;
|
|
6837
7089
|
}
|
|
6838
|
-
|
|
6839
|
-
if (
|
|
6840
|
-
|
|
6841
|
-
const target = getDatabaseTargetInfo();
|
|
6842
|
-
const db = await openDatabaseWithSync({ initialize: true });
|
|
6843
|
-
await db.close();
|
|
6844
|
-
console.log(` init ${formatDatabaseInitTarget(target)}`);
|
|
6845
|
-
} catch (err) {
|
|
6846
|
-
const msg = err.message;
|
|
6847
|
-
if (!msg.includes("already")) {
|
|
6848
|
-
console.warn(` warn database init: ${msg}`);
|
|
6849
|
-
} else {
|
|
6850
|
-
console.log(` skip database already initialized`);
|
|
6851
|
-
}
|
|
7090
|
+
function summarizeSkillLinkHealth(inspections) {
|
|
7091
|
+
if (inspections.some((item) => item.state === "unmanaged")) {
|
|
7092
|
+
return "unmanaged";
|
|
6852
7093
|
}
|
|
7094
|
+
const repairable = ["missing", "broken", "stale-copy"];
|
|
7095
|
+
if (inspections.some((item) => repairable.includes(item.state))) {
|
|
7096
|
+
return "needs-repair";
|
|
7097
|
+
}
|
|
7098
|
+
return "healthy";
|
|
6853
7099
|
}
|
|
6854
7100
|
var ZAM_BLOCK_START = "<!-- ZAM:START -->";
|
|
6855
7101
|
var ZAM_BLOCK_END = "<!-- ZAM:END -->";
|
|
@@ -6906,10 +7152,10 @@ ZAM is available in this repository. Use the \`zam\` skill in Claude Code to tur
|
|
|
6906
7152
|
return;
|
|
6907
7153
|
}
|
|
6908
7154
|
const name = basename4(cwd);
|
|
6909
|
-
const content = `# ZAM Personal Kernel \
|
|
7155
|
+
const content = `# ZAM Personal Kernel \u2014 ${name}
|
|
6910
7156
|
|
|
6911
7157
|
This is a ZAM personal instance. ZAM builds lasting skills through spaced
|
|
6912
|
-
repetition during real work \
|
|
7158
|
+
repetition during real work \u2014 not separate study sessions.
|
|
6913
7159
|
|
|
6914
7160
|
## First time here?
|
|
6915
7161
|
Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
|
|
@@ -6918,8 +7164,8 @@ Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
|
|
|
6918
7164
|
Run \`/zam\` to start a learning session on whatever you are working on.
|
|
6919
7165
|
|
|
6920
7166
|
## What lives here
|
|
6921
|
-
- \`beliefs/\` \
|
|
6922
|
-
- \`goals/\` \
|
|
7167
|
+
- \`beliefs/\` \u2014 your worldview, approved by git commit
|
|
7168
|
+
- \`goals/\` \u2014 your objectives, decomposed into tasks and learning tokens
|
|
6923
7169
|
|
|
6924
7170
|
## Fast-changing data
|
|
6925
7171
|
Learning tokens, cards, and review history live in local SQLite by default.
|
|
@@ -7008,605 +7254,356 @@ ZAM is available in this repository through \`.claude/skills/zam/\`. Use the \`z
|
|
|
7008
7254
|
Boolean(opts.dryRun)
|
|
7009
7255
|
);
|
|
7010
7256
|
}
|
|
7011
|
-
var setupCommand = new Command2("setup").description(
|
|
7012
|
-
"Link ZAM skill directories into this workspace and initialize the database"
|
|
7013
|
-
).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
|
|
7014
|
-
"--agents <list>",
|
|
7015
|
-
"comma-separated agents to wire: all, claude, copilot, codex, agent"
|
|
7016
|
-
).option(
|
|
7017
|
-
"--dry-run",
|
|
7018
|
-
"show what would be written without changing files",
|
|
7019
|
-
false
|
|
7020
|
-
).action(
|
|
7021
|
-
async (opts) => {
|
|
7022
|
-
let agents;
|
|
7023
|
-
try {
|
|
7024
|
-
agents = parseSetupAgents(opts.agents);
|
|
7025
|
-
} catch (err) {
|
|
7026
|
-
console.error(`Error: ${err.message}`);
|
|
7027
|
-
process.exit(1);
|
|
7028
|
-
}
|
|
7029
|
-
const target = resolve3(opts.target ?? process.cwd());
|
|
7030
|
-
const updateExistingInstructions = Boolean(opts.target) || opts.force;
|
|
7031
|
-
console.log(
|
|
7032
|
-
`Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
|
|
7033
|
-
`
|
|
7034
|
-
);
|
|
7035
|
-
wireSkills(target, agents, {
|
|
7036
|
-
force: opts.force,
|
|
7037
|
-
dryRun: opts.dryRun
|
|
7038
|
-
});
|
|
7039
|
-
await initDatabase(opts.skipInit || opts.dryRun);
|
|
7040
|
-
if (agents.has("claude")) {
|
|
7041
|
-
writeClaudeMd(opts.skipClaudeMd, target, {
|
|
7042
|
-
dryRun: opts.dryRun,
|
|
7043
|
-
updateExisting: updateExistingInstructions
|
|
7044
|
-
});
|
|
7045
|
-
}
|
|
7046
|
-
if (agents.has("codex") || agents.has("agent")) {
|
|
7047
|
-
writeAgentsMd(opts.skipAgentsMd, target, {
|
|
7048
|
-
dryRun: opts.dryRun,
|
|
7049
|
-
updateExisting: updateExistingInstructions
|
|
7050
|
-
});
|
|
7051
|
-
}
|
|
7052
|
-
if (agents.has("copilot") && (opts.target || opts.agents)) {
|
|
7053
|
-
writeCopilotInstructions(target, {
|
|
7054
|
-
dryRun: opts.dryRun,
|
|
7055
|
-
updateExisting: updateExistingInstructions
|
|
7056
|
-
});
|
|
7057
|
-
}
|
|
7058
|
-
console.log(
|
|
7059
|
-
"\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."
|
|
7060
|
-
);
|
|
7061
|
-
}
|
|
7062
|
-
);
|
|
7063
7257
|
|
|
7064
|
-
// src/cli/
|
|
7065
|
-
function
|
|
7066
|
-
|
|
7258
|
+
// src/cli/users/identity.ts
|
|
7259
|
+
async function ensureDefaultUser(db, preferredUserId) {
|
|
7260
|
+
const stored = await getSetting(db, "user.id");
|
|
7261
|
+
if (stored) return stored;
|
|
7262
|
+
const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
|
|
7263
|
+
await setSetting(db, "user.id", userId);
|
|
7264
|
+
return userId;
|
|
7265
|
+
}
|
|
7266
|
+
async function resolveUser(opts, db, resolveOpts) {
|
|
7267
|
+
if (opts.user) return opts.user;
|
|
7268
|
+
const stored = await getSetting(db, "user.id");
|
|
7269
|
+
if (stored) return stored;
|
|
7270
|
+
const message = "No user specified. Set a default with: zam whoami --set <id>";
|
|
7271
|
+
if (resolveOpts?.json) {
|
|
7272
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
7273
|
+
} else {
|
|
7274
|
+
console.error(message);
|
|
7275
|
+
}
|
|
7067
7276
|
process.exit(1);
|
|
7068
7277
|
}
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7278
|
+
|
|
7279
|
+
// src/cli/workspaces/active.ts
|
|
7280
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync9 } from "fs";
|
|
7281
|
+
import { homedir as homedir9 } from "os";
|
|
7282
|
+
import { basename as basename5, join as join16, resolve as resolve3 } from "path";
|
|
7283
|
+
var LEGACY_WORKSPACE_DIR_KEY = "personal.workspace_dir";
|
|
7284
|
+
var DEFAULT_WORKSPACE_ID = "personal";
|
|
7285
|
+
function defaultWorkspaceDir() {
|
|
7286
|
+
return join16(homedir9(), "Documents", "zam");
|
|
7287
|
+
}
|
|
7288
|
+
function normalizeWorkspacePath(path) {
|
|
7289
|
+
const resolved = resolve3(path);
|
|
7290
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
7291
|
+
}
|
|
7292
|
+
function sameWorkspacePath(left, right) {
|
|
7293
|
+
return normalizeWorkspacePath(left) === normalizeWorkspacePath(right);
|
|
7294
|
+
}
|
|
7295
|
+
function labelFromPath(path) {
|
|
7296
|
+
return basename5(path) || "ZAM";
|
|
7297
|
+
}
|
|
7298
|
+
function workspaceIdFromPath(dir) {
|
|
7299
|
+
const base = basename5(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7300
|
+
const prefix = base || "workspace";
|
|
7301
|
+
const existing = new Set(getConfiguredWorkspaces().map((item) => item.id));
|
|
7302
|
+
if (!existing.has(prefix)) return prefix;
|
|
7303
|
+
let index = 2;
|
|
7304
|
+
while (existing.has(`${prefix}-${index}`)) index++;
|
|
7305
|
+
return `${prefix}-${index}`;
|
|
7306
|
+
}
|
|
7307
|
+
function findWorkspaceByPath(dir) {
|
|
7308
|
+
return getConfiguredWorkspaces().find(
|
|
7309
|
+
(workspace) => sameWorkspacePath(workspace.path, dir)
|
|
7310
|
+
);
|
|
7311
|
+
}
|
|
7312
|
+
async function clearLegacyWorkspaceDir(db) {
|
|
7313
|
+
await deleteSetting(db, LEGACY_WORKSPACE_DIR_KEY);
|
|
7314
|
+
}
|
|
7315
|
+
async function migrateLegacyWorkspaceDir(db) {
|
|
7316
|
+
const legacyDir = await getSetting(db, LEGACY_WORKSPACE_DIR_KEY);
|
|
7317
|
+
if (!legacyDir) return void 0;
|
|
7318
|
+
const path = resolve3(legacyDir);
|
|
7319
|
+
const existing = findWorkspaceByPath(path);
|
|
7320
|
+
const migrated = existing ?? {
|
|
7321
|
+
id: getConfiguredWorkspaces().some(
|
|
7322
|
+
(workspace) => workspace.id === DEFAULT_WORKSPACE_ID
|
|
7323
|
+
) ? workspaceIdFromPath(path) : DEFAULT_WORKSPACE_ID,
|
|
7324
|
+
label: labelFromPath(path),
|
|
7325
|
+
kind: "personal",
|
|
7326
|
+
path
|
|
7327
|
+
};
|
|
7328
|
+
if (!existing) {
|
|
7329
|
+
upsertConfiguredWorkspace(migrated);
|
|
7330
|
+
}
|
|
7331
|
+
if (!getActiveWorkspace()) {
|
|
7332
|
+
setActiveWorkspaceId(migrated.id);
|
|
7078
7333
|
}
|
|
7334
|
+
await clearLegacyWorkspaceDir(db);
|
|
7335
|
+
return migrated;
|
|
7079
7336
|
}
|
|
7080
|
-
function
|
|
7081
|
-
|
|
7337
|
+
async function ensureActiveWorkspace(db) {
|
|
7338
|
+
await migrateLegacyWorkspaceDir(db);
|
|
7339
|
+
const active = getActiveWorkspace();
|
|
7340
|
+
if (active) {
|
|
7341
|
+
mkdirSync9(active.path, { recursive: true });
|
|
7342
|
+
return active;
|
|
7343
|
+
}
|
|
7344
|
+
const configured = getConfiguredWorkspaces()[0];
|
|
7345
|
+
if (configured) {
|
|
7346
|
+
setActiveWorkspaceId(configured.id);
|
|
7347
|
+
mkdirSync9(configured.path, { recursive: true });
|
|
7348
|
+
return configured;
|
|
7349
|
+
}
|
|
7350
|
+
const workspace = {
|
|
7351
|
+
id: DEFAULT_WORKSPACE_ID,
|
|
7352
|
+
label: "Personal",
|
|
7353
|
+
kind: "personal",
|
|
7354
|
+
path: defaultWorkspaceDir()
|
|
7355
|
+
};
|
|
7356
|
+
mkdirSync9(workspace.path, { recursive: true });
|
|
7357
|
+
upsertConfiguredWorkspace(workspace);
|
|
7358
|
+
setActiveWorkspaceId(workspace.id);
|
|
7359
|
+
await clearLegacyWorkspaceDir(db);
|
|
7360
|
+
return workspace;
|
|
7082
7361
|
}
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
}).trim();
|
|
7098
|
-
} catch (err) {
|
|
7099
|
-
throw new Error(`Git command failed: ${err.message}`);
|
|
7362
|
+
async function activateWorkspace(db, workspace) {
|
|
7363
|
+
upsertConfiguredWorkspace(workspace);
|
|
7364
|
+
setActiveWorkspaceId(workspace.id);
|
|
7365
|
+
await clearLegacyWorkspaceDir(db);
|
|
7366
|
+
return workspace;
|
|
7367
|
+
}
|
|
7368
|
+
async function activateWorkspacePath(db, dir, opts = {}) {
|
|
7369
|
+
await migrateLegacyWorkspaceDir(db);
|
|
7370
|
+
const path = resolve3(dir);
|
|
7371
|
+
const existing = opts.id ? void 0 : findWorkspaceByPath(path);
|
|
7372
|
+
if (existing) {
|
|
7373
|
+
setActiveWorkspaceId(existing.id);
|
|
7374
|
+
await clearLegacyWorkspaceDir(db);
|
|
7375
|
+
return existing;
|
|
7100
7376
|
}
|
|
7377
|
+
const workspace = {
|
|
7378
|
+
id: opts.id || workspaceIdFromPath(path),
|
|
7379
|
+
label: opts.label || labelFromPath(path),
|
|
7380
|
+
kind: opts.kind || "personal",
|
|
7381
|
+
path
|
|
7382
|
+
};
|
|
7383
|
+
return activateWorkspace(db, workspace);
|
|
7101
7384
|
}
|
|
7102
|
-
function
|
|
7103
|
-
|
|
7385
|
+
async function removeWorkspaceAndResolveActive(db, id) {
|
|
7386
|
+
removeConfiguredWorkspace(id);
|
|
7387
|
+
await clearLegacyWorkspaceDir(db);
|
|
7388
|
+
const activeWorkspace = await ensureActiveWorkspace(db);
|
|
7389
|
+
return {
|
|
7390
|
+
activeWorkspace,
|
|
7391
|
+
workspaces: getConfiguredWorkspaces()
|
|
7392
|
+
};
|
|
7104
7393
|
}
|
|
7105
|
-
function
|
|
7106
|
-
return
|
|
7394
|
+
function existingWorkspaceDirOrHome(workspace) {
|
|
7395
|
+
return existsSync16(workspace.path) ? workspace.path : homedir9();
|
|
7107
7396
|
}
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
"
|
|
7114
|
-
|
|
7115
|
-
"
|
|
7116
|
-
|
|
7117
|
-
"
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
if (
|
|
7128
|
-
|
|
7397
|
+
|
|
7398
|
+
// src/cli/workspaces/backup.ts
|
|
7399
|
+
import { mkdirSync as mkdirSync10 } from "fs";
|
|
7400
|
+
import { join as join17 } from "path";
|
|
7401
|
+
async function backupDatabaseTo(db, targetDir) {
|
|
7402
|
+
const backupDir = join17(targetDir, "zam-backups");
|
|
7403
|
+
mkdirSync10(backupDir, { recursive: true });
|
|
7404
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7405
|
+
const dest = join17(backupDir, `zam-${stamp}.db`);
|
|
7406
|
+
await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
7407
|
+
return dest;
|
|
7408
|
+
}
|
|
7409
|
+
|
|
7410
|
+
// src/cli/commands/bridge.ts
|
|
7411
|
+
var isServeMode = false;
|
|
7412
|
+
function jsonOut2(data) {
|
|
7413
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7414
|
+
}
|
|
7415
|
+
function jsonError(message) {
|
|
7416
|
+
if (isServeMode) {
|
|
7417
|
+
throw new Error(JSON.stringify({ error: message }));
|
|
7129
7418
|
}
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
);
|
|
7419
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
7420
|
+
process.exit(1);
|
|
7133
7421
|
}
|
|
7134
|
-
function
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
return source;
|
|
7422
|
+
function parseNonNegativeIntegerOption(name, value) {
|
|
7423
|
+
const parsed = Number(value);
|
|
7424
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
7425
|
+
jsonError(`${name} must be a non-negative integer`);
|
|
7139
7426
|
}
|
|
7140
|
-
|
|
7141
|
-
`Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
|
|
7142
|
-
);
|
|
7427
|
+
return parsed;
|
|
7143
7428
|
}
|
|
7144
|
-
function
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7429
|
+
function parseProviderScope(scope) {
|
|
7430
|
+
const value = scope ?? "machine";
|
|
7431
|
+
if (value === "machine") return true;
|
|
7432
|
+
if (value === "shared") return false;
|
|
7433
|
+
jsonError(`Invalid --scope: ${value}. Use machine or shared.`);
|
|
7148
7434
|
}
|
|
7149
|
-
function
|
|
7150
|
-
|
|
7151
|
-
if (!workspace) {
|
|
7152
|
-
throw new Error(
|
|
7153
|
-
`Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
|
|
7154
|
-
);
|
|
7155
|
-
}
|
|
7156
|
-
return workspace;
|
|
7435
|
+
async function withDb2(fn) {
|
|
7436
|
+
await withDb(fn, jsonError);
|
|
7157
7437
|
}
|
|
7158
|
-
|
|
7159
|
-
const
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7438
|
+
async function getReviewTarget2(db, cardId, userId) {
|
|
7439
|
+
const target = await db.prepare(
|
|
7440
|
+
`SELECT c.id AS card_id, c.token_id, c.user_id, t.slug
|
|
7441
|
+
FROM cards c
|
|
7442
|
+
JOIN tokens t ON t.id = c.token_id
|
|
7443
|
+
WHERE c.id = ?`
|
|
7444
|
+
).get(cardId);
|
|
7445
|
+
if (!target) {
|
|
7446
|
+
jsonError(`Card not found: ${cardId}`);
|
|
7163
7447
|
}
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
console.log(
|
|
7167
|
-
" (none) \u2014 add one: zam workspace add personal --path <dir>"
|
|
7168
|
-
);
|
|
7169
|
-
return;
|
|
7448
|
+
if (target.user_id !== userId) {
|
|
7449
|
+
jsonError(`Card ${cardId} does not belong to user ${userId}`);
|
|
7170
7450
|
}
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7451
|
+
return target;
|
|
7452
|
+
}
|
|
7453
|
+
function parseTokenUpdates(opts) {
|
|
7454
|
+
const updates = {};
|
|
7455
|
+
if (opts.concept !== void 0) updates.concept = opts.concept;
|
|
7456
|
+
if (opts.domain !== void 0) updates.domain = opts.domain;
|
|
7457
|
+
if (opts.bloom !== void 0)
|
|
7458
|
+
updates.bloom_level = Number(opts.bloom);
|
|
7459
|
+
if (opts.context !== void 0) updates.context = opts.context;
|
|
7460
|
+
if (opts.sourceLink !== void 0) {
|
|
7461
|
+
updates.source_link = opts.sourceLink === "" ? null : opts.sourceLink;
|
|
7182
7462
|
}
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
"custom"
|
|
7188
|
-
).option("--label <label>", "Human-readable label").option(
|
|
7189
|
-
"--source-control <provider>",
|
|
7190
|
-
`Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
|
|
7191
|
-
).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
|
|
7192
|
-
try {
|
|
7193
|
-
const path = resolve4(String(opts.path));
|
|
7194
|
-
if (!existsSync16(path)) {
|
|
7195
|
-
console.error(`Workspace path does not exist: ${path}`);
|
|
7196
|
-
process.exit(1);
|
|
7463
|
+
if (opts.mode !== void 0) {
|
|
7464
|
+
const validModes = ["shadowing", "copilot", "autonomy", "none"];
|
|
7465
|
+
if (!validModes.includes(opts.mode)) {
|
|
7466
|
+
jsonError(`Invalid mode: ${opts.mode}`);
|
|
7197
7467
|
}
|
|
7198
|
-
|
|
7199
|
-
id,
|
|
7200
|
-
kind: parseWorkspaceKind(opts.kind),
|
|
7201
|
-
path,
|
|
7202
|
-
...opts.label ? { label: opts.label } : {},
|
|
7203
|
-
...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
|
|
7204
|
-
...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
|
|
7205
|
-
...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
|
|
7206
|
-
};
|
|
7207
|
-
wireSkills(path, parseSetupAgents());
|
|
7208
|
-
upsertConfiguredWorkspace(workspace);
|
|
7209
|
-
console.log(`Registered and linked workspace "${id}" at ${path}.`);
|
|
7210
|
-
} catch (err) {
|
|
7211
|
-
console.error(`Error: ${err.message}`);
|
|
7212
|
-
process.exit(1);
|
|
7468
|
+
updates.symbiosis_mode = opts.mode === "none" ? null : opts.mode;
|
|
7213
7469
|
}
|
|
7470
|
+
return updates;
|
|
7471
|
+
}
|
|
7472
|
+
var bridgeCommand = new Command2("bridge").description(
|
|
7473
|
+
"Machine-readable JSON protocol for AI integration"
|
|
7474
|
+
);
|
|
7475
|
+
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) => {
|
|
7476
|
+
await withDb2(async (db) => {
|
|
7477
|
+
const userId = await resolveUser(opts, db, { json: true });
|
|
7478
|
+
const dueCards = await getDueCards(db, userId, void 0, opts.domain);
|
|
7479
|
+
const domains = [
|
|
7480
|
+
...new Set(dueCards.map((c) => c.domain).filter(Boolean))
|
|
7481
|
+
].sort();
|
|
7482
|
+
jsonOut2({
|
|
7483
|
+
userId,
|
|
7484
|
+
domain: opts.domain ?? null,
|
|
7485
|
+
dueCount: dueCards.length,
|
|
7486
|
+
domains,
|
|
7487
|
+
cards: dueCards.map((c) => ({
|
|
7488
|
+
cardId: c.id,
|
|
7489
|
+
tokenId: c.token_id,
|
|
7490
|
+
slug: c.slug,
|
|
7491
|
+
concept: c.concept,
|
|
7492
|
+
domain: c.domain,
|
|
7493
|
+
bloomLevel: c.bloom_level,
|
|
7494
|
+
state: c.state,
|
|
7495
|
+
dueAt: c.due_at
|
|
7496
|
+
}))
|
|
7497
|
+
});
|
|
7498
|
+
});
|
|
7214
7499
|
});
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7500
|
+
bridgeCommand.command("backup-db").description("Back up the local database into the workspace (JSON)").option(
|
|
7501
|
+
"--dir <path>",
|
|
7502
|
+
"Target directory (default: workspace dir, else ~/Documents/zam)"
|
|
7503
|
+
).action(async (opts) => {
|
|
7504
|
+
const target = getDatabaseTargetInfo();
|
|
7505
|
+
if (target.kind !== "local") {
|
|
7506
|
+
jsonOut2({
|
|
7507
|
+
ok: false,
|
|
7508
|
+
reason: "remote",
|
|
7509
|
+
target: target.kind,
|
|
7510
|
+
location: target.location
|
|
7511
|
+
});
|
|
7512
|
+
return;
|
|
7220
7513
|
}
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7514
|
+
await withDb2(async (db) => {
|
|
7515
|
+
const workspaceDir = opts.dir || (await ensureActiveWorkspace(db)).path;
|
|
7516
|
+
const path = await backupDatabaseTo(db, workspaceDir);
|
|
7517
|
+
jsonOut2({ ok: true, path });
|
|
7518
|
+
});
|
|
7225
7519
|
});
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
)
|
|
7234
|
-
|
|
7235
|
-
"show what would be written without changing files",
|
|
7236
|
-
false
|
|
7237
|
-
).action((id, opts) => {
|
|
7238
|
-
try {
|
|
7239
|
-
const workspace = requireWorkspace(id);
|
|
7240
|
-
const agents = parseSetupAgents(opts.agents);
|
|
7241
|
-
console.log(
|
|
7242
|
-
`Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
|
|
7243
|
-
`
|
|
7244
|
-
);
|
|
7245
|
-
wireSkills(workspace.path, agents, {
|
|
7246
|
-
force: Boolean(opts.force),
|
|
7247
|
-
dryRun: Boolean(opts.dryRun)
|
|
7248
|
-
});
|
|
7249
|
-
if (agents.has("claude")) {
|
|
7250
|
-
writeClaudeMd(false, workspace.path, {
|
|
7251
|
-
dryRun: Boolean(opts.dryRun),
|
|
7252
|
-
updateExisting: true
|
|
7253
|
-
});
|
|
7254
|
-
}
|
|
7255
|
-
if (agents.has("codex") || agents.has("agent")) {
|
|
7256
|
-
writeAgentsMd(false, workspace.path, {
|
|
7257
|
-
dryRun: Boolean(opts.dryRun),
|
|
7258
|
-
updateExisting: true
|
|
7259
|
-
});
|
|
7260
|
-
}
|
|
7261
|
-
if (agents.has("copilot")) {
|
|
7262
|
-
writeCopilotInstructions(workspace.path, {
|
|
7263
|
-
dryRun: Boolean(opts.dryRun),
|
|
7264
|
-
updateExisting: true
|
|
7265
|
-
});
|
|
7266
|
-
}
|
|
7267
|
-
} catch (err) {
|
|
7268
|
-
console.error(`Error: ${err.message}`);
|
|
7269
|
-
process.exit(1);
|
|
7270
|
-
}
|
|
7271
|
-
});
|
|
7272
|
-
workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
|
|
7273
|
-
let db;
|
|
7274
|
-
let workspaceDir = "";
|
|
7275
|
-
try {
|
|
7276
|
-
db = await openDatabase();
|
|
7277
|
-
workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
|
|
7278
|
-
await db.close();
|
|
7279
|
-
} catch {
|
|
7280
|
-
await db?.close();
|
|
7281
|
-
}
|
|
7282
|
-
if (!workspaceDir) {
|
|
7283
|
-
console.error(
|
|
7284
|
-
"\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
|
|
7285
|
-
);
|
|
7286
|
-
process.exit(1);
|
|
7287
|
-
}
|
|
7288
|
-
if (!existsSync16(workspaceDir)) {
|
|
7289
|
-
console.error(
|
|
7290
|
-
`\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
|
|
7291
|
-
);
|
|
7292
|
-
process.exit(1);
|
|
7293
|
-
}
|
|
7294
|
-
console.log(`
|
|
7295
|
-
Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
|
|
7296
|
-
if (!hasCommand("git")) {
|
|
7297
|
-
console.error(
|
|
7298
|
-
"\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
|
|
7299
|
-
);
|
|
7300
|
-
process.exit(1);
|
|
7301
|
-
}
|
|
7302
|
-
const gitignorePath = join16(workspaceDir, ".gitignore");
|
|
7303
|
-
if (!existsSync16(gitignorePath)) {
|
|
7304
|
-
writeFileSync8(
|
|
7305
|
-
gitignorePath,
|
|
7306
|
-
"node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
|
|
7307
|
-
"utf8"
|
|
7308
|
-
);
|
|
7309
|
-
}
|
|
7310
|
-
const hasGitRepo = existsSync16(join16(workspaceDir, ".git"));
|
|
7311
|
-
if (!hasGitRepo) {
|
|
7312
|
-
console.log("Initializing local Git repository...");
|
|
7313
|
-
runGit(workspaceDir, ["init", "-b", "main"]);
|
|
7314
|
-
runGit(workspaceDir, ["add", "."]);
|
|
7315
|
-
runGit(workspaceDir, [
|
|
7316
|
-
"commit",
|
|
7317
|
-
"-m",
|
|
7318
|
-
"chore: initial workspace sandbox bootstrap"
|
|
7319
|
-
]);
|
|
7320
|
-
console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
|
|
7321
|
-
} else {
|
|
7322
|
-
console.log("Git repository is already initialized.");
|
|
7323
|
-
}
|
|
7324
|
-
const repoName = await input({
|
|
7325
|
-
message: "Choose a name for your GitHub repository:",
|
|
7326
|
-
default: "zam-personal"
|
|
7327
|
-
});
|
|
7328
|
-
const isPrivate = await confirm({
|
|
7329
|
-
message: "Should the repository be private?",
|
|
7330
|
-
default: true
|
|
7331
|
-
});
|
|
7332
|
-
const repoVisibility = isPrivate ? "--private" : "--public";
|
|
7333
|
-
if (hasCommand("gh")) {
|
|
7334
|
-
console.log("GitHub CLI detected! Automating repository creation...");
|
|
7335
|
-
const proceedGh = await confirm({
|
|
7336
|
-
message: "Would you like ZAM to create the repository using the GitHub CLI?",
|
|
7337
|
-
default: true
|
|
7520
|
+
bridgeCommand.command("workspace-info").description("Report the workspace dir, its default, and the data dir (JSON)").action(async () => {
|
|
7521
|
+
await withDb2(async (db) => {
|
|
7522
|
+
const activeWorkspace = await ensureActiveWorkspace(db);
|
|
7523
|
+
jsonOut2({
|
|
7524
|
+
activeWorkspaceId: activeWorkspace.id,
|
|
7525
|
+
activeWorkspace,
|
|
7526
|
+
workspaceDir: activeWorkspace.path,
|
|
7527
|
+
defaultWorkspaceDir: defaultWorkspaceDir(),
|
|
7528
|
+
dataDir: join18(homedir10(), ".zam")
|
|
7338
7529
|
});
|
|
7339
|
-
if (proceedGh) {
|
|
7340
|
-
try {
|
|
7341
|
-
console.log(`Creating GitHub repository ${repoName}...`);
|
|
7342
|
-
execFileSync3("gh", ghRepoCreateArgs(repoName, repoVisibility), {
|
|
7343
|
-
cwd: workspaceDir,
|
|
7344
|
-
stdio: "inherit"
|
|
7345
|
-
});
|
|
7346
|
-
console.log(
|
|
7347
|
-
"\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
|
|
7348
|
-
);
|
|
7349
|
-
process.exit(0);
|
|
7350
|
-
} catch (err) {
|
|
7351
|
-
console.warn(
|
|
7352
|
-
`\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
|
|
7353
|
-
);
|
|
7354
|
-
}
|
|
7355
|
-
}
|
|
7356
|
-
}
|
|
7357
|
-
console.log(
|
|
7358
|
-
"\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
|
|
7359
|
-
);
|
|
7360
|
-
console.log(" 1. Go to https://github.com/new");
|
|
7361
|
-
console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
|
|
7362
|
-
console.log(
|
|
7363
|
-
` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
|
|
7364
|
-
);
|
|
7365
|
-
console.log(
|
|
7366
|
-
" 4. Do NOT initialize it with README, .gitignore, or license"
|
|
7367
|
-
);
|
|
7368
|
-
console.log(" 5. Click 'Create repository'\n");
|
|
7369
|
-
const githubUrl = await input({
|
|
7370
|
-
message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
|
|
7371
7530
|
});
|
|
7372
|
-
if (githubUrl) {
|
|
7373
|
-
try {
|
|
7374
|
-
console.log("Linking remote repository and pushing...");
|
|
7375
|
-
let hasOrigin = false;
|
|
7376
|
-
try {
|
|
7377
|
-
runGit(workspaceDir, ["remote", "get-url", "origin"]);
|
|
7378
|
-
hasOrigin = true;
|
|
7379
|
-
} catch {
|
|
7380
|
-
}
|
|
7381
|
-
runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
|
|
7382
|
-
runGit(workspaceDir, ["push", "-u", "origin", "main"]);
|
|
7383
|
-
console.log(
|
|
7384
|
-
"\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
|
|
7385
|
-
);
|
|
7386
|
-
} catch (err) {
|
|
7387
|
-
console.error(
|
|
7388
|
-
`\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
|
|
7389
|
-
);
|
|
7390
|
-
console.log(
|
|
7391
|
-
"You can push manually later using: git push -u origin main"
|
|
7392
|
-
);
|
|
7393
|
-
}
|
|
7394
|
-
}
|
|
7395
7531
|
});
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
const dest = join16(backupDir, `zam-${stamp}.db`);
|
|
7401
|
-
await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
7402
|
-
return dest;
|
|
7403
|
-
}
|
|
7404
|
-
workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
|
|
7405
|
-
const dir = join16(homedir9(), ".zam");
|
|
7406
|
-
console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
|
|
7407
|
-
});
|
|
7408
|
-
workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
|
|
7409
|
-
"--dir <path>",
|
|
7410
|
-
"Target directory (default: workspace dir, else ~/Documents/zam)"
|
|
7411
|
-
).option("--json", "Output as JSON").action(async (opts) => {
|
|
7412
|
-
const target = getDatabaseTargetInfo();
|
|
7413
|
-
if (target.kind !== "local") {
|
|
7414
|
-
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.`;
|
|
7415
|
-
if (opts.json) {
|
|
7416
|
-
console.log(JSON.stringify({ ok: false, reason }));
|
|
7417
|
-
return;
|
|
7418
|
-
}
|
|
7419
|
-
console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
|
|
7420
|
-
process.exit(1);
|
|
7421
|
-
}
|
|
7422
|
-
let db;
|
|
7423
|
-
try {
|
|
7424
|
-
db = await openDatabase();
|
|
7425
|
-
const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir9(), "Documents", "zam");
|
|
7426
|
-
const dest = await backupDatabaseTo(db, workspaceDir);
|
|
7427
|
-
if (opts.json) {
|
|
7428
|
-
console.log(JSON.stringify({ ok: true, path: dest }));
|
|
7429
|
-
} else {
|
|
7430
|
-
console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
|
|
7431
|
-
}
|
|
7432
|
-
} catch (err) {
|
|
7433
|
-
const reason = err.message;
|
|
7434
|
-
if (opts.json) {
|
|
7435
|
-
console.log(JSON.stringify({ ok: false, reason }));
|
|
7436
|
-
} else {
|
|
7437
|
-
console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
|
|
7438
|
-
}
|
|
7439
|
-
process.exit(1);
|
|
7440
|
-
} finally {
|
|
7441
|
-
await db?.close();
|
|
7442
|
-
}
|
|
7443
|
-
});
|
|
7444
|
-
|
|
7445
|
-
// src/cli/commands/bridge.ts
|
|
7446
|
-
var isServeMode = false;
|
|
7447
|
-
function jsonOut2(data) {
|
|
7448
|
-
console.log(JSON.stringify(data, null, 2));
|
|
7449
|
-
}
|
|
7450
|
-
function jsonError(message) {
|
|
7451
|
-
if (isServeMode) {
|
|
7452
|
-
throw new Error(JSON.stringify({ error: message }));
|
|
7453
|
-
}
|
|
7454
|
-
console.log(JSON.stringify({ error: message }, null, 2));
|
|
7455
|
-
process.exit(1);
|
|
7456
|
-
}
|
|
7457
|
-
function parseNonNegativeIntegerOption(name, value) {
|
|
7458
|
-
const parsed = Number(value);
|
|
7459
|
-
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
7460
|
-
jsonError(`${name} must be a non-negative integer`);
|
|
7461
|
-
}
|
|
7462
|
-
return parsed;
|
|
7463
|
-
}
|
|
7464
|
-
async function withDb2(fn) {
|
|
7465
|
-
await withDb(fn, jsonError);
|
|
7532
|
+
function provisionConfiguredWorkspaces() {
|
|
7533
|
+
return getConfiguredWorkspaces().flatMap(
|
|
7534
|
+
(workspace) => existsSync17(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
|
|
7535
|
+
);
|
|
7466
7536
|
}
|
|
7467
|
-
|
|
7468
|
-
const
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7537
|
+
function buildWorkspaceLinkHealth(workspaces) {
|
|
7538
|
+
const agents = parseSetupAgents();
|
|
7539
|
+
const map = {};
|
|
7540
|
+
for (const workspace of workspaces) {
|
|
7541
|
+
if (!existsSync17(workspace.path)) continue;
|
|
7542
|
+
const links = inspectSkillLinks(workspace.path, agents);
|
|
7543
|
+
map[workspace.id] = {
|
|
7544
|
+
health: summarizeSkillLinkHealth(links),
|
|
7545
|
+
states: Object.fromEntries(
|
|
7546
|
+
links.map((link) => [link.agents.join("+"), link.state])
|
|
7547
|
+
)
|
|
7548
|
+
};
|
|
7479
7549
|
}
|
|
7480
|
-
return
|
|
7550
|
+
return map;
|
|
7481
7551
|
}
|
|
7482
|
-
function
|
|
7483
|
-
const
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
updates.source_link = opts.sourceLink === "" ? null : opts.sourceLink;
|
|
7491
|
-
}
|
|
7492
|
-
if (opts.mode !== void 0) {
|
|
7493
|
-
const validModes = ["shadowing", "copilot", "autonomy", "none"];
|
|
7494
|
-
if (!validModes.includes(opts.mode)) {
|
|
7495
|
-
jsonError(`Invalid mode: ${opts.mode}`);
|
|
7496
|
-
}
|
|
7497
|
-
updates.symbiosis_mode = opts.mode === "none" ? null : opts.mode;
|
|
7498
|
-
}
|
|
7499
|
-
return updates;
|
|
7552
|
+
async function ensureDesktopWorkspace(db) {
|
|
7553
|
+
const activeWorkspace = await ensureActiveWorkspace(db);
|
|
7554
|
+
const skillLinks = provisionConfiguredWorkspaces();
|
|
7555
|
+
return {
|
|
7556
|
+
workspaceDir: activeWorkspace.path,
|
|
7557
|
+
activeWorkspaceId: activeWorkspace.id,
|
|
7558
|
+
skillLinks
|
|
7559
|
+
};
|
|
7500
7560
|
}
|
|
7501
|
-
|
|
7502
|
-
"Machine-readable JSON protocol for AI integration"
|
|
7503
|
-
);
|
|
7504
|
-
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) => {
|
|
7561
|
+
bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
|
|
7505
7562
|
await withDb2(async (db) => {
|
|
7506
|
-
const
|
|
7507
|
-
const
|
|
7508
|
-
const domains = [
|
|
7509
|
-
...new Set(dueCards.map((c) => c.domain).filter(Boolean))
|
|
7510
|
-
].sort();
|
|
7563
|
+
const activeWorkspace = await ensureActiveWorkspace(db);
|
|
7564
|
+
const workspaces = getConfiguredWorkspaces();
|
|
7511
7565
|
jsonOut2({
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
slug: c.slug,
|
|
7520
|
-
concept: c.concept,
|
|
7521
|
-
domain: c.domain,
|
|
7522
|
-
bloomLevel: c.bloom_level,
|
|
7523
|
-
state: c.state,
|
|
7524
|
-
dueAt: c.due_at
|
|
7525
|
-
}))
|
|
7566
|
+
workspaces,
|
|
7567
|
+
activeWorkspaceId: activeWorkspace.id,
|
|
7568
|
+
activeWorkspace,
|
|
7569
|
+
workspaceDir: activeWorkspace.path,
|
|
7570
|
+
defaultWorkspaceDir: defaultWorkspaceDir(),
|
|
7571
|
+
dataDir: join18(homedir10(), ".zam"),
|
|
7572
|
+
linkHealth: buildWorkspaceLinkHealth(workspaces)
|
|
7526
7573
|
});
|
|
7527
7574
|
});
|
|
7528
7575
|
});
|
|
7529
|
-
bridgeCommand.command("
|
|
7530
|
-
"
|
|
7531
|
-
|
|
7576
|
+
bridgeCommand.command("workspace-repair-links").description(
|
|
7577
|
+
"Relink ZAM skill junctions for a configured workspace, replacing broken links and outdated copies (JSON)"
|
|
7578
|
+
).requiredOption("--id <id>", "Workspace id").option(
|
|
7579
|
+
"--agents <list>",
|
|
7580
|
+
"comma-separated agents to wire: all, claude, copilot, codex, agent"
|
|
7532
7581
|
).action(async (opts) => {
|
|
7533
|
-
const
|
|
7534
|
-
if (
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
location: target.location
|
|
7540
|
-
});
|
|
7541
|
-
return;
|
|
7582
|
+
const id = String(opts.id ?? "").trim();
|
|
7583
|
+
if (!id) jsonError("A non-empty --id is required");
|
|
7584
|
+
const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
|
|
7585
|
+
if (!workspace) jsonError(`Workspace "${id}" is not configured`);
|
|
7586
|
+
if (!existsSync17(workspace.path)) {
|
|
7587
|
+
jsonError(`Workspace path does not exist: ${workspace.path}`);
|
|
7542
7588
|
}
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
});
|
|
7548
|
-
});
|
|
7549
|
-
bridgeCommand.command("workspace-info").description("Report the workspace dir, its default, and the data dir (JSON)").action(async () => {
|
|
7550
|
-
await withDb2(async (db) => {
|
|
7551
|
-
const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
|
|
7552
|
-
jsonOut2({
|
|
7553
|
-
workspaceDir,
|
|
7554
|
-
defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
|
|
7555
|
-
dataDir: join17(homedir10(), ".zam")
|
|
7556
|
-
});
|
|
7589
|
+
const agents = parseSetupAgents(opts.agents);
|
|
7590
|
+
const skillLinks = wireSkills(workspace.path, agents, {
|
|
7591
|
+
force: true,
|
|
7592
|
+
quiet: true
|
|
7557
7593
|
});
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
const workspaceDir = savedWorkspaceDir || configured.find((workspace) => workspace.kind === "personal")?.path || join17(homedir10(), "Documents", "zam");
|
|
7570
|
-
mkdirSync10(workspaceDir, { recursive: true });
|
|
7571
|
-
if (!savedWorkspaceDir) {
|
|
7572
|
-
await setSetting(db, "personal.workspace_dir", workspaceDir);
|
|
7573
|
-
}
|
|
7574
|
-
if (!configured.some(
|
|
7575
|
-
(workspace) => sameWorkspacePath(workspace.path, workspaceDir)
|
|
7576
|
-
)) {
|
|
7577
|
-
const id = configured.some((workspace) => workspace.id === "personal") ? workspaceIdFromPath(workspaceDir) : "personal";
|
|
7578
|
-
upsertConfiguredWorkspace({
|
|
7579
|
-
id,
|
|
7580
|
-
label: basename5(workspaceDir) || "ZAM",
|
|
7581
|
-
kind: "personal",
|
|
7582
|
-
path: workspaceDir
|
|
7583
|
-
});
|
|
7584
|
-
}
|
|
7585
|
-
const skillLinks = getConfiguredWorkspaces().flatMap(
|
|
7586
|
-
(workspace) => existsSync17(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
|
|
7587
|
-
);
|
|
7588
|
-
return { workspaceDir, skillLinks };
|
|
7589
|
-
}
|
|
7590
|
-
bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
|
|
7591
|
-
await withDb2(async (db) => {
|
|
7592
|
-
const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
|
|
7593
|
-
jsonOut2({
|
|
7594
|
-
workspaces: getConfiguredWorkspaces(),
|
|
7595
|
-
workspaceDir,
|
|
7596
|
-
defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
|
|
7597
|
-
dataDir: join17(homedir10(), ".zam")
|
|
7598
|
-
});
|
|
7594
|
+
const links = inspectSkillLinks(workspace.path, agents);
|
|
7595
|
+
jsonOut2({
|
|
7596
|
+
ok: true,
|
|
7597
|
+
workspace,
|
|
7598
|
+
skillLinks,
|
|
7599
|
+
linkHealth: {
|
|
7600
|
+
health: summarizeSkillLinkHealth(links),
|
|
7601
|
+
states: Object.fromEntries(
|
|
7602
|
+
links.map((link) => [link.agents.join("+"), link.state])
|
|
7603
|
+
)
|
|
7604
|
+
}
|
|
7599
7605
|
});
|
|
7600
7606
|
});
|
|
7601
|
-
function workspaceIdFromPath(dir) {
|
|
7602
|
-
const base = basename5(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7603
|
-
const prefix = base || "workspace";
|
|
7604
|
-
const existing = new Set(getConfiguredWorkspaces().map((item) => item.id));
|
|
7605
|
-
if (!existing.has(prefix)) return prefix;
|
|
7606
|
-
let index = 2;
|
|
7607
|
-
while (existing.has(`${prefix}-${index}`)) index++;
|
|
7608
|
-
return `${prefix}-${index}`;
|
|
7609
|
-
}
|
|
7610
7607
|
function parseBridgeWorkspaceKind(value) {
|
|
7611
7608
|
const kind = (value || "custom").toLowerCase();
|
|
7612
7609
|
if (kind === "personal" || kind === "team" || kind === "family" || kind === "community" || kind === "organization" || kind === "custom") {
|
|
@@ -7617,25 +7614,25 @@ function parseBridgeWorkspaceKind(value) {
|
|
|
7617
7614
|
bridgeCommand.command("workspace-add").description("Register an existing directory as a ZAM workspace (JSON)").requiredOption("--path <dir>", "Existing workspace/repository directory").option("--id <id>", "Workspace id").option("--label <label>", "Human-readable label").option("--kind <kind>", "Workspace kind", "custom").action(async (opts) => {
|
|
7618
7615
|
const raw = String(opts.path ?? "").trim();
|
|
7619
7616
|
if (!raw) jsonError("A non-empty --path is required");
|
|
7620
|
-
const path =
|
|
7617
|
+
const path = resolve4(raw);
|
|
7621
7618
|
if (!existsSync17(path)) jsonError(`Workspace path does not exist: ${path}`);
|
|
7622
|
-
const id =
|
|
7623
|
-
if (!id) jsonError("A non-empty --id is required");
|
|
7624
|
-
const
|
|
7625
|
-
id,
|
|
7626
|
-
label: opts.label || basename5(path) || id,
|
|
7627
|
-
kind: parseBridgeWorkspaceKind(opts.kind),
|
|
7628
|
-
path
|
|
7629
|
-
};
|
|
7619
|
+
const id = opts.id ? String(opts.id).trim() : void 0;
|
|
7620
|
+
if (opts.id && !id) jsonError("A non-empty --id is required");
|
|
7621
|
+
const kind = parseBridgeWorkspaceKind(opts.kind);
|
|
7630
7622
|
const skillLinks = wireSkills(path, parseSetupAgents(), { quiet: true });
|
|
7631
7623
|
await withDb2(async (db) => {
|
|
7632
|
-
await
|
|
7633
|
-
|
|
7624
|
+
const workspace = await activateWorkspacePath(db, path, {
|
|
7625
|
+
...id ? { id } : {},
|
|
7626
|
+
...opts.label ? { label: opts.label } : {},
|
|
7627
|
+
kind
|
|
7628
|
+
});
|
|
7634
7629
|
jsonOut2({
|
|
7635
7630
|
ok: true,
|
|
7636
7631
|
workspace,
|
|
7637
7632
|
workspaces: getConfiguredWorkspaces(),
|
|
7638
|
-
|
|
7633
|
+
activeWorkspaceId: workspace.id,
|
|
7634
|
+
activeWorkspace: workspace,
|
|
7635
|
+
workspaceDir: workspace.path,
|
|
7639
7636
|
skillLinks
|
|
7640
7637
|
});
|
|
7641
7638
|
});
|
|
@@ -7646,27 +7643,15 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
|
|
|
7646
7643
|
const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
|
|
7647
7644
|
if (!workspace) jsonError(`Workspace "${id}" is not configured`);
|
|
7648
7645
|
await withDb2(async (db) => {
|
|
7649
|
-
const
|
|
7650
|
-
const
|
|
7651
|
-
let workspaceDir = activeWorkspaceDir;
|
|
7652
|
-
let skillLinks = [];
|
|
7653
|
-
if (activeWorkspaceDir && sameWorkspacePath(activeWorkspaceDir, workspace.path)) {
|
|
7654
|
-
if (remaining.length > 0) {
|
|
7655
|
-
workspaceDir = remaining[0].path;
|
|
7656
|
-
await setSetting(db, "personal.workspace_dir", workspaceDir);
|
|
7657
|
-
} else {
|
|
7658
|
-
workspaceDir = join17(homedir10(), "Documents", "zam");
|
|
7659
|
-
await setSetting(db, "personal.workspace_dir", workspaceDir);
|
|
7660
|
-
const ensured = await ensureDesktopWorkspace(db);
|
|
7661
|
-
workspaceDir = ensured.workspaceDir;
|
|
7662
|
-
skillLinks = ensured.skillLinks;
|
|
7663
|
-
}
|
|
7664
|
-
}
|
|
7646
|
+
const { activeWorkspace, workspaces } = await removeWorkspaceAndResolveActive(db, id);
|
|
7647
|
+
const skillLinks = provisionConfiguredWorkspaces();
|
|
7665
7648
|
jsonOut2({
|
|
7666
7649
|
ok: true,
|
|
7667
7650
|
removed: workspace,
|
|
7668
|
-
workspaces
|
|
7669
|
-
|
|
7651
|
+
workspaces,
|
|
7652
|
+
activeWorkspaceId: activeWorkspace.id,
|
|
7653
|
+
activeWorkspace,
|
|
7654
|
+
workspaceDir: activeWorkspace.path,
|
|
7670
7655
|
skillLinks
|
|
7671
7656
|
});
|
|
7672
7657
|
});
|
|
@@ -7674,12 +7659,20 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
|
|
|
7674
7659
|
bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
|
|
7675
7660
|
const raw = String(opts.dir ?? "").trim();
|
|
7676
7661
|
if (!raw) jsonError("A non-empty --dir is required");
|
|
7677
|
-
const dir =
|
|
7662
|
+
const dir = resolve4(raw);
|
|
7678
7663
|
if (!existsSync17(dir)) jsonError(`Workspace path does not exist: ${dir}`);
|
|
7679
7664
|
const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
|
|
7680
7665
|
await withDb2(async (db) => {
|
|
7681
|
-
await
|
|
7682
|
-
jsonOut2({
|
|
7666
|
+
const workspace = await activateWorkspacePath(db, dir);
|
|
7667
|
+
jsonOut2({
|
|
7668
|
+
ok: true,
|
|
7669
|
+
workspace,
|
|
7670
|
+
workspaces: getConfiguredWorkspaces(),
|
|
7671
|
+
activeWorkspaceId: workspace.id,
|
|
7672
|
+
activeWorkspace: workspace,
|
|
7673
|
+
workspaceDir: workspace.path,
|
|
7674
|
+
skillLinks
|
|
7675
|
+
});
|
|
7683
7676
|
});
|
|
7684
7677
|
});
|
|
7685
7678
|
bridgeCommand.command("agent-list").description("List agent harnesses with detection state + the default (JSON)").action(async () => {
|
|
@@ -7729,8 +7722,8 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
|
|
|
7729
7722
|
if (opts.workspace && !configuredWorkspace) {
|
|
7730
7723
|
jsonError(`Workspace is not configured: ${opts.workspace}`);
|
|
7731
7724
|
}
|
|
7732
|
-
|
|
7733
|
-
|
|
7725
|
+
const activeWorkspace = await ensureActiveWorkspace(db);
|
|
7726
|
+
const workspace = opts.dir ? existsSync17(opts.dir) ? opts.dir : homedir10() : existingWorkspaceDirOrHome(configuredWorkspace ?? activeWorkspace);
|
|
7734
7727
|
launchHarness(harness, {
|
|
7735
7728
|
executable,
|
|
7736
7729
|
workspace,
|
|
@@ -7768,6 +7761,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
7768
7761
|
const item = queue.items[0];
|
|
7769
7762
|
const isLlmEnabled = await getSetting(db, "llm.enabled") === "true";
|
|
7770
7763
|
let resolvedQuestion = item.question;
|
|
7764
|
+
let questionSource = "original";
|
|
7765
|
+
let questionModel;
|
|
7771
7766
|
if (isLlmEnabled) {
|
|
7772
7767
|
try {
|
|
7773
7768
|
const healed = await ensureHighQualityQuestion(db, {
|
|
@@ -7780,7 +7775,9 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
7780
7775
|
question: item.question
|
|
7781
7776
|
});
|
|
7782
7777
|
if (healed) {
|
|
7783
|
-
resolvedQuestion = healed;
|
|
7778
|
+
resolvedQuestion = healed.question;
|
|
7779
|
+
questionSource = healed.source;
|
|
7780
|
+
questionModel = healed.model;
|
|
7784
7781
|
}
|
|
7785
7782
|
} catch {
|
|
7786
7783
|
}
|
|
@@ -7809,6 +7806,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
|
|
|
7809
7806
|
hasReview: true,
|
|
7810
7807
|
card: item,
|
|
7811
7808
|
prompt,
|
|
7809
|
+
questionSource,
|
|
7810
|
+
questionModel: questionModel ?? null,
|
|
7812
7811
|
resolvedContext,
|
|
7813
7812
|
queueSize: fullQueue.items.length
|
|
7814
7813
|
});
|
|
@@ -8104,10 +8103,10 @@ bridgeCommand.command("discover-skills").description(
|
|
|
8104
8103
|
"20"
|
|
8105
8104
|
).action(async (opts) => {
|
|
8106
8105
|
try {
|
|
8107
|
-
const monitorDir =
|
|
8106
|
+
const monitorDir = join18(homedir10(), ".zam", "monitor");
|
|
8108
8107
|
let files;
|
|
8109
8108
|
try {
|
|
8110
|
-
files =
|
|
8109
|
+
files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
|
|
8111
8110
|
} catch {
|
|
8112
8111
|
jsonOut2({ proposals: [], message: "No monitor logs found." });
|
|
8113
8112
|
return;
|
|
@@ -8117,7 +8116,7 @@ bridgeCommand.command("discover-skills").description(
|
|
|
8117
8116
|
return;
|
|
8118
8117
|
}
|
|
8119
8118
|
const limit = Number(opts.limit);
|
|
8120
|
-
const sorted = files.map((f) => ({ name: f, path:
|
|
8119
|
+
const sorted = files.map((f) => ({ name: f, path: join18(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
|
|
8121
8120
|
const sessionCommands = /* @__PURE__ */ new Map();
|
|
8122
8121
|
for (const file of sorted) {
|
|
8123
8122
|
const sessionId = file.name.replace(".jsonl", "");
|
|
@@ -8229,7 +8228,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
|
|
|
8229
8228
|
});
|
|
8230
8229
|
function resolveWindowsPowerShell() {
|
|
8231
8230
|
try {
|
|
8232
|
-
|
|
8231
|
+
execFileSync3("where.exe", ["pwsh.exe"], { stdio: "ignore" });
|
|
8233
8232
|
return "pwsh";
|
|
8234
8233
|
} catch {
|
|
8235
8234
|
return "powershell";
|
|
@@ -8244,7 +8243,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
|
|
|
8244
8243
|
throw new Error(`Invalid process name format: ${processName}`);
|
|
8245
8244
|
}
|
|
8246
8245
|
if (platform === "win32") {
|
|
8247
|
-
const stdout =
|
|
8246
|
+
const stdout = execFileSync3(
|
|
8248
8247
|
resolveWindowsPowerShell(),
|
|
8249
8248
|
[
|
|
8250
8249
|
"-NoProfile",
|
|
@@ -8557,13 +8556,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
|
|
|
8557
8556
|
} else if (platform === "darwin") {
|
|
8558
8557
|
if (hwnd) {
|
|
8559
8558
|
const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
|
|
8560
|
-
|
|
8559
|
+
execFileSync3("screencapture", ["-l", String(parsedHwnd), outputPath], {
|
|
8561
8560
|
stdio: "pipe"
|
|
8562
8561
|
});
|
|
8563
8562
|
return { method: "screencapture-window", target: null };
|
|
8564
8563
|
} else if (processName) {
|
|
8565
8564
|
try {
|
|
8566
|
-
const windowId =
|
|
8565
|
+
const windowId = execFileSync3(
|
|
8567
8566
|
"osascript",
|
|
8568
8567
|
[
|
|
8569
8568
|
"-e",
|
|
@@ -8572,17 +8571,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
|
|
|
8572
8571
|
{ encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
|
|
8573
8572
|
).trim();
|
|
8574
8573
|
if (windowId && /^\d+$/.test(windowId)) {
|
|
8575
|
-
|
|
8574
|
+
execFileSync3("screencapture", ["-l", windowId, outputPath], {
|
|
8576
8575
|
stdio: "pipe"
|
|
8577
8576
|
});
|
|
8578
8577
|
return { method: "screencapture-window", target: null };
|
|
8579
8578
|
}
|
|
8580
8579
|
} catch {
|
|
8581
8580
|
}
|
|
8582
|
-
|
|
8581
|
+
execFileSync3("screencapture", ["-x", outputPath], { stdio: "pipe" });
|
|
8583
8582
|
return { method: "screencapture-full", target: null };
|
|
8584
8583
|
} else {
|
|
8585
|
-
|
|
8584
|
+
execFileSync3("screencapture", ["-x", outputPath], { stdio: "pipe" });
|
|
8586
8585
|
return { method: "screencapture-full", target: null };
|
|
8587
8586
|
}
|
|
8588
8587
|
} else {
|
|
@@ -8619,7 +8618,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
|
|
|
8619
8618
|
return;
|
|
8620
8619
|
}
|
|
8621
8620
|
}
|
|
8622
|
-
const outputPath = opts.image ?? opts.output ??
|
|
8621
|
+
const outputPath = opts.image ?? opts.output ?? join18(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
|
|
8623
8622
|
const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
|
|
8624
8623
|
if (!isProvided) {
|
|
8625
8624
|
const post = decidePostCapture(policy, {
|
|
@@ -8674,11 +8673,11 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
8674
8673
|
return;
|
|
8675
8674
|
}
|
|
8676
8675
|
const sessionId = opts.session;
|
|
8677
|
-
const statePath =
|
|
8676
|
+
const statePath = join18(tmpdir3(), `zam-recording-${sessionId}.json`);
|
|
8678
8677
|
const defaultExt = platform === "win32" ? ".mkv" : ".mov";
|
|
8679
|
-
const outputPath = opts.output ??
|
|
8680
|
-
const { existsSync:
|
|
8681
|
-
if (
|
|
8678
|
+
const outputPath = opts.output ?? join18(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
|
|
8679
|
+
const { existsSync: existsSync26, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
|
|
8680
|
+
if (existsSync26(statePath)) {
|
|
8682
8681
|
jsonOut2({
|
|
8683
8682
|
sessionId,
|
|
8684
8683
|
started: false,
|
|
@@ -8686,7 +8685,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
8686
8685
|
});
|
|
8687
8686
|
return;
|
|
8688
8687
|
}
|
|
8689
|
-
const logPath =
|
|
8688
|
+
const logPath = join18(tmpdir3(), `zam-recording-${sessionId}.log`);
|
|
8690
8689
|
let logFd;
|
|
8691
8690
|
try {
|
|
8692
8691
|
logFd = openSync(logPath, "w");
|
|
@@ -8768,9 +8767,9 @@ bridgeCommand.command("stop-recording").description(
|
|
|
8768
8767
|
return;
|
|
8769
8768
|
}
|
|
8770
8769
|
const sessionId = opts.session;
|
|
8771
|
-
const statePath =
|
|
8772
|
-
const { existsSync:
|
|
8773
|
-
if (!
|
|
8770
|
+
const statePath = join18(tmpdir3(), `zam-recording-${sessionId}.json`);
|
|
8771
|
+
const { existsSync: existsSync26, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
|
|
8772
|
+
if (!existsSync26(statePath)) {
|
|
8774
8773
|
jsonOut2({
|
|
8775
8774
|
sessionId,
|
|
8776
8775
|
stopped: false,
|
|
@@ -8794,7 +8793,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
8794
8793
|
};
|
|
8795
8794
|
let attempts = 0;
|
|
8796
8795
|
while (isProcessRunning(pid) && attempts < 20) {
|
|
8797
|
-
await new Promise((
|
|
8796
|
+
await new Promise((resolve10) => setTimeout(resolve10, 250));
|
|
8798
8797
|
attempts++;
|
|
8799
8798
|
}
|
|
8800
8799
|
if (isProcessRunning(pid)) {
|
|
@@ -8807,7 +8806,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
8807
8806
|
rmSync4(statePath, { force: true });
|
|
8808
8807
|
} catch {
|
|
8809
8808
|
}
|
|
8810
|
-
if (!
|
|
8809
|
+
if (!existsSync26(outputPath)) {
|
|
8811
8810
|
jsonOut2({
|
|
8812
8811
|
sessionId,
|
|
8813
8812
|
stopped: false,
|
|
@@ -8887,25 +8886,207 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
|
|
|
8887
8886
|
}
|
|
8888
8887
|
}
|
|
8889
8888
|
jsonOut2({
|
|
8890
|
-
enabled,
|
|
8891
|
-
online,
|
|
8892
|
-
url,
|
|
8893
|
-
model,
|
|
8894
|
-
modelAvailable,
|
|
8895
|
-
availableModels,
|
|
8896
|
-
apiFlavor: provider.apiFlavor,
|
|
8897
|
-
unsupportedProvider
|
|
8889
|
+
enabled,
|
|
8890
|
+
online,
|
|
8891
|
+
url,
|
|
8892
|
+
model,
|
|
8893
|
+
modelAvailable,
|
|
8894
|
+
availableModels,
|
|
8895
|
+
apiFlavor: provider.apiFlavor,
|
|
8896
|
+
unsupportedProvider
|
|
8897
|
+
});
|
|
8898
|
+
});
|
|
8899
|
+
});
|
|
8900
|
+
bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
|
|
8901
|
+
await withDb2(async (db) => {
|
|
8902
|
+
const [recall, vision, text] = await Promise.all([
|
|
8903
|
+
getProviderRoleStatus(db, "recall"),
|
|
8904
|
+
getProviderRoleStatus(db, "vision"),
|
|
8905
|
+
getProviderRoleStatus(db, "text")
|
|
8906
|
+
]);
|
|
8907
|
+
jsonOut2({ roles: { recall, vision, text } });
|
|
8908
|
+
});
|
|
8909
|
+
});
|
|
8910
|
+
bridgeCommand.command("provider-config-list").description("List provider records and role bindings (JSON)").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts) => {
|
|
8911
|
+
const machine = parseProviderScope(opts.scope);
|
|
8912
|
+
await withProviderScope(machine, async (db) => {
|
|
8913
|
+
const providers = await readScopedProviders(db, machine);
|
|
8914
|
+
const roles = await readScopedRoles(db, machine);
|
|
8915
|
+
const rows = buildProviderListing(
|
|
8916
|
+
providers,
|
|
8917
|
+
(ref) => getProviderApiKey(ref) !== null
|
|
8918
|
+
);
|
|
8919
|
+
jsonOut2({
|
|
8920
|
+
scope: machine ? "machine" : "shared",
|
|
8921
|
+
providers: rows,
|
|
8922
|
+
roles,
|
|
8923
|
+
orphans: findOrphanKeyRefs(listProviderApiKeyRefs(), providers)
|
|
8924
|
+
});
|
|
8925
|
+
});
|
|
8926
|
+
});
|
|
8927
|
+
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(
|
|
8928
|
+
"--flavor <flavor>",
|
|
8929
|
+
`Wire protocol: ${VALID_API_FLAVORS.join(" | ")}`
|
|
8930
|
+
).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) => {
|
|
8931
|
+
const machine = parseProviderScope(opts.scope);
|
|
8932
|
+
let apiFlavor;
|
|
8933
|
+
if (opts.flavor) {
|
|
8934
|
+
if (!VALID_API_FLAVORS.includes(opts.flavor)) {
|
|
8935
|
+
jsonError(
|
|
8936
|
+
`Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
|
|
8937
|
+
);
|
|
8938
|
+
}
|
|
8939
|
+
apiFlavor = opts.flavor;
|
|
8940
|
+
}
|
|
8941
|
+
let local;
|
|
8942
|
+
if (command.getOptionValueSource("local") === "cli") {
|
|
8943
|
+
local = opts.local === true;
|
|
8944
|
+
}
|
|
8945
|
+
const patch = {};
|
|
8946
|
+
if (opts.label !== void 0) patch.label = opts.label;
|
|
8947
|
+
if (opts.url !== void 0) patch.url = opts.url;
|
|
8948
|
+
if (opts.model !== void 0) patch.model = opts.model;
|
|
8949
|
+
if (apiFlavor !== void 0) patch.apiFlavor = apiFlavor;
|
|
8950
|
+
if (opts.keyRef !== void 0) patch.apiKeyRef = opts.keyRef;
|
|
8951
|
+
if (local !== void 0) patch.local = local;
|
|
8952
|
+
if (opts.runner !== void 0) patch.runner = opts.runner;
|
|
8953
|
+
await withProviderScope(machine, async (db) => {
|
|
8954
|
+
const providers = await readScopedProviders(db, machine);
|
|
8955
|
+
const next = upsertProviderRecord(providers, opts.name, patch);
|
|
8956
|
+
await writeScopedProviders(db, machine, next);
|
|
8957
|
+
const rows = buildProviderListing(
|
|
8958
|
+
next,
|
|
8959
|
+
(ref) => getProviderApiKey(ref) !== null
|
|
8960
|
+
);
|
|
8961
|
+
const row = rows.find((entry) => entry.name === opts.name);
|
|
8962
|
+
jsonOut2({
|
|
8963
|
+
ok: true,
|
|
8964
|
+
scope: machine ? "machine" : "shared",
|
|
8965
|
+
name: opts.name,
|
|
8966
|
+
provider: row,
|
|
8967
|
+
cloudModelHint: opts.url ? getCloudModelRecommendation(opts.url) : null
|
|
8968
|
+
});
|
|
8969
|
+
});
|
|
8970
|
+
});
|
|
8971
|
+
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) => {
|
|
8972
|
+
const machine = parseProviderScope(opts.scope);
|
|
8973
|
+
await withProviderScope(machine, async (db) => {
|
|
8974
|
+
const providers = await readScopedProviders(db, machine);
|
|
8975
|
+
const { providers: next, removed } = removeProviderRecord(
|
|
8976
|
+
providers,
|
|
8977
|
+
opts.name
|
|
8978
|
+
);
|
|
8979
|
+
if (!removed) {
|
|
8980
|
+
jsonError(`No such provider: ${opts.name}`);
|
|
8981
|
+
}
|
|
8982
|
+
await writeScopedProviders(db, machine, next);
|
|
8983
|
+
jsonOut2({
|
|
8984
|
+
ok: true,
|
|
8985
|
+
scope: machine ? "machine" : "shared",
|
|
8986
|
+
name: opts.name,
|
|
8987
|
+
removed: true,
|
|
8988
|
+
referencingRoles: rolesReferencing(
|
|
8989
|
+
await readScopedRoles(db, machine),
|
|
8990
|
+
opts.name
|
|
8991
|
+
)
|
|
8992
|
+
});
|
|
8993
|
+
});
|
|
8994
|
+
});
|
|
8995
|
+
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) => {
|
|
8996
|
+
if (!VALID_ROLES.includes(opts.role)) {
|
|
8997
|
+
jsonError(`Invalid --role: ${opts.role}. Use ${VALID_ROLES.join(", ")}.`);
|
|
8998
|
+
}
|
|
8999
|
+
const machine = parseProviderScope(opts.scope);
|
|
9000
|
+
await withProviderScope(machine, async (db) => {
|
|
9001
|
+
const providers = await readScopedProviders(db, machine);
|
|
9002
|
+
const roles = await readScopedRoles(db, machine);
|
|
9003
|
+
const nextRoles = bindRoleProviders(
|
|
9004
|
+
roles,
|
|
9005
|
+
opts.role,
|
|
9006
|
+
opts.primary,
|
|
9007
|
+
opts.fallback
|
|
9008
|
+
);
|
|
9009
|
+
await writeScopedRoles(db, machine, nextRoles);
|
|
9010
|
+
const binding = nextRoles[opts.role];
|
|
9011
|
+
const primary = providers[opts.primary];
|
|
9012
|
+
jsonOut2({
|
|
9013
|
+
ok: true,
|
|
9014
|
+
scope: machine ? "machine" : "shared",
|
|
9015
|
+
role: opts.role,
|
|
9016
|
+
binding,
|
|
9017
|
+
warnings: [
|
|
9018
|
+
...primary && primary.apiFlavor === "anthropic-messages" && (opts.role === "recall" || opts.role === "text") ? ["unsupported-provider-for-role"] : [],
|
|
9019
|
+
...opts.primary && !(opts.primary in providers) ? ["primary-provider-undefined"] : [],
|
|
9020
|
+
...opts.fallback && !(opts.fallback in providers) ? ["fallback-provider-undefined"] : []
|
|
9021
|
+
]
|
|
8898
9022
|
});
|
|
8899
9023
|
});
|
|
8900
9024
|
});
|
|
8901
|
-
bridgeCommand.command("provider-
|
|
9025
|
+
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) => {
|
|
9026
|
+
const key = opts.key.trim();
|
|
9027
|
+
if (!key) jsonError("No key provided.");
|
|
9028
|
+
setProviderApiKey(opts.ref, key);
|
|
9029
|
+
jsonOut2({ ok: true, ref: opts.ref, masked: maskSecret(key) });
|
|
9030
|
+
});
|
|
9031
|
+
bridgeCommand.command("provider-clear-key").description("Remove a stored provider API key (JSON)").requiredOption("--ref <ref>", "Credential reference name").action((opts) => {
|
|
9032
|
+
clearProviderApiKey(opts.ref);
|
|
9033
|
+
jsonOut2({ ok: true, ref: opts.ref });
|
|
9034
|
+
});
|
|
9035
|
+
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) => {
|
|
9036
|
+
const apiKey = opts.keyRef ? getProviderApiKey(opts.keyRef) ?? void 0 : void 0;
|
|
9037
|
+
const models = await getAvailableModels(opts.url, apiKey);
|
|
9038
|
+
jsonOut2({ models });
|
|
9039
|
+
});
|
|
9040
|
+
bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for an endpoint URL (JSON)").requiredOption("--url <url>", "Endpoint base URL").action((opts) => {
|
|
9041
|
+
jsonOut2({ recommendation: getCloudModelRecommendation(opts.url) });
|
|
9042
|
+
});
|
|
9043
|
+
bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
|
|
9044
|
+
const profile = getSystemProfile();
|
|
9045
|
+
const flmInstalled = hasCommand("flm") || existsSync17("C:\\Program Files\\flm\\flm.exe");
|
|
9046
|
+
const ollamaInstalled = hasCommand("ollama");
|
|
9047
|
+
const runners = [
|
|
9048
|
+
{ id: "flm", label: "FastFlowLM", installed: flmInstalled },
|
|
9049
|
+
{ id: "ollama", label: "Ollama", installed: ollamaInstalled },
|
|
9050
|
+
{
|
|
9051
|
+
id: "foundry-local",
|
|
9052
|
+
label: "Foundry Local",
|
|
9053
|
+
installed: false
|
|
9054
|
+
}
|
|
9055
|
+
];
|
|
9056
|
+
let recommended = "ollama";
|
|
9057
|
+
if (profile.recommendedRunner === "fastflowlm" && flmInstalled) {
|
|
9058
|
+
recommended = "flm";
|
|
9059
|
+
} else if (profile.recommendedRunner === "ollama" && ollamaInstalled) {
|
|
9060
|
+
recommended = "ollama";
|
|
9061
|
+
} else if (flmInstalled) {
|
|
9062
|
+
recommended = "flm";
|
|
9063
|
+
} else if (ollamaInstalled) {
|
|
9064
|
+
recommended = "ollama";
|
|
9065
|
+
} else if (profile.recommendedRunner === "fastflowlm") {
|
|
9066
|
+
recommended = "flm";
|
|
9067
|
+
}
|
|
9068
|
+
const defaultUrl = recommended === "ollama" ? "http://localhost:11434/v1" : DEFAULT_LLM_URL;
|
|
9069
|
+
jsonOut2({
|
|
9070
|
+
runners,
|
|
9071
|
+
recommended,
|
|
9072
|
+
defaultUrl,
|
|
9073
|
+
defaultModel: profile.recommendedModel || DEFAULT_LLM_MODEL
|
|
9074
|
+
});
|
|
9075
|
+
});
|
|
9076
|
+
var UI_WRITABLE_SETTINGS = /* @__PURE__ */ new Set([
|
|
9077
|
+
"llm.enabled",
|
|
9078
|
+
"llm.vision.enabled",
|
|
9079
|
+
"system.locale"
|
|
9080
|
+
]);
|
|
9081
|
+
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) => {
|
|
9082
|
+
if (!UI_WRITABLE_SETTINGS.has(opts.key)) {
|
|
9083
|
+
jsonError(
|
|
9084
|
+
`Setting "${opts.key}" is not writable via setting-set. Allowed: ${[...UI_WRITABLE_SETTINGS].join(", ")}.`
|
|
9085
|
+
);
|
|
9086
|
+
}
|
|
8902
9087
|
await withDb2(async (db) => {
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
getProviderRoleStatus(db, "vision"),
|
|
8906
|
-
getProviderRoleStatus(db, "text")
|
|
8907
|
-
]);
|
|
8908
|
-
jsonOut2({ roles: { recall, vision, text } });
|
|
9088
|
+
await setSetting(db, opts.key, opts.value);
|
|
9089
|
+
jsonOut2({ ok: true, key: opts.key, value: opts.value });
|
|
8909
9090
|
});
|
|
8910
9091
|
});
|
|
8911
9092
|
bridgeCommand.command("check-vision").description(
|
|
@@ -8954,7 +9135,10 @@ bridgeCommand.command("translate-question").description("Translate a question dy
|
|
|
8954
9135
|
});
|
|
8955
9136
|
bridgeCommand.command("evaluate-answer").description(
|
|
8956
9137
|
"Evaluate the learner's active-recall answer using the local LLM (JSON)"
|
|
8957
|
-
).requiredOption("--slug <slug>", "Token slug").requiredOption("--concept <concept>", "Target concept text").requiredOption("--domain <domain>", "Token domain").requiredOption("--bloom-level <level>", "Bloom taxonomy level").requiredOption("--question <question>", "Question prompt presented").requiredOption("--user-answer <answer>", "User's typed answer").option("--context <context>", "Optional token context details").option("--source-link <link>", "Optional source link").
|
|
9138
|
+
).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(
|
|
9139
|
+
"--source-content <content>",
|
|
9140
|
+
"Pre-resolved source reference content (skips re-fetch when set)"
|
|
9141
|
+
).action(async (opts) => {
|
|
8958
9142
|
await withDb2(async (db) => {
|
|
8959
9143
|
const isEnabled = await getSetting(db, "llm.enabled") === "true";
|
|
8960
9144
|
if (!isEnabled) {
|
|
@@ -8965,8 +9149,8 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
8965
9149
|
});
|
|
8966
9150
|
return;
|
|
8967
9151
|
}
|
|
8968
|
-
let resolvedContextContent = null;
|
|
8969
|
-
if (opts.sourceLink) {
|
|
9152
|
+
let resolvedContextContent = opts.sourceContent ?? null;
|
|
9153
|
+
if (resolvedContextContent == null && opts.sourceLink) {
|
|
8970
9154
|
try {
|
|
8971
9155
|
const resolved = await resolveReviewContext(opts.sourceLink);
|
|
8972
9156
|
resolvedContextContent = resolved?.content ?? null;
|
|
@@ -8974,7 +9158,7 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
8974
9158
|
}
|
|
8975
9159
|
}
|
|
8976
9160
|
try {
|
|
8977
|
-
const
|
|
9161
|
+
const result = await evaluateAnswerViaLLM(db, {
|
|
8978
9162
|
slug: opts.slug,
|
|
8979
9163
|
concept: opts.concept,
|
|
8980
9164
|
domain: opts.domain,
|
|
@@ -8984,7 +9168,11 @@ bridgeCommand.command("evaluate-answer").description(
|
|
|
8984
9168
|
userAnswer: opts.userAnswer,
|
|
8985
9169
|
sourceLinkContent: resolvedContextContent
|
|
8986
9170
|
});
|
|
8987
|
-
jsonOut2({
|
|
9171
|
+
jsonOut2({
|
|
9172
|
+
success: true,
|
|
9173
|
+
evaluation: result.text,
|
|
9174
|
+
evaluationModel: result.model
|
|
9175
|
+
});
|
|
8988
9176
|
} catch (err) {
|
|
8989
9177
|
jsonOut2({
|
|
8990
9178
|
success: false,
|
|
@@ -8998,11 +9186,12 @@ bridgeCommand.command("desktop-bootstrap").description("Initialize first-run des
|
|
|
8998
9186
|
await withDb2(async (db) => {
|
|
8999
9187
|
const userId = await ensureDefaultUser(db, opts.user);
|
|
9000
9188
|
const { enabled, url, model, locale } = await getLlmConfig(db);
|
|
9001
|
-
const { workspaceDir, skillLinks } = await ensureDesktopWorkspace(db);
|
|
9189
|
+
const { workspaceDir, activeWorkspaceId, skillLinks } = await ensureDesktopWorkspace(db);
|
|
9002
9190
|
jsonOut2({
|
|
9003
9191
|
userId,
|
|
9004
9192
|
locale,
|
|
9005
9193
|
llm: { enabled, url, model },
|
|
9194
|
+
activeWorkspaceId,
|
|
9006
9195
|
workspaceDir,
|
|
9007
9196
|
skillLinks
|
|
9008
9197
|
});
|
|
@@ -9223,8 +9412,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
9223
9412
|
});
|
|
9224
9413
|
|
|
9225
9414
|
// src/cli/commands/card.ts
|
|
9226
|
-
import { Command as
|
|
9227
|
-
var cardCommand = new
|
|
9415
|
+
import { Command as Command3 } from "commander";
|
|
9416
|
+
var cardCommand = new Command3("card").description(
|
|
9228
9417
|
"Manage spaced-repetition cards"
|
|
9229
9418
|
);
|
|
9230
9419
|
cardCommand.command("due").description("Show due tokens for a user").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").option("--json", "Output as JSON").option("--summary", "Show only counts per domain (no slugs or concepts)").action(async (opts) => {
|
|
@@ -9422,9 +9611,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
|
|
|
9422
9611
|
});
|
|
9423
9612
|
|
|
9424
9613
|
// src/cli/commands/connector.ts
|
|
9425
|
-
import { input
|
|
9426
|
-
import { Command as
|
|
9427
|
-
var connectorCommand = new
|
|
9614
|
+
import { input, password } from "@inquirer/prompts";
|
|
9615
|
+
import { Command as Command4 } from "commander";
|
|
9616
|
+
var connectorCommand = new Command4("connector").description(
|
|
9428
9617
|
"Manage external service connectors"
|
|
9429
9618
|
);
|
|
9430
9619
|
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(
|
|
@@ -9443,10 +9632,10 @@ connectorCommand.command("setup").description("Configure a connector").argument(
|
|
|
9443
9632
|
process.exit(1);
|
|
9444
9633
|
}
|
|
9445
9634
|
try {
|
|
9446
|
-
const orgUrl = await
|
|
9635
|
+
const orgUrl = await input({
|
|
9447
9636
|
message: "Organization URL (e.g. https://dev.azure.com/myorg):"
|
|
9448
9637
|
});
|
|
9449
|
-
const project = await
|
|
9638
|
+
const project = await input({
|
|
9450
9639
|
message: "Project name:"
|
|
9451
9640
|
});
|
|
9452
9641
|
const pat = await password({
|
|
@@ -9536,8 +9725,16 @@ connectorCommand.command("sync").description("Verify the Turso cloud database co
|
|
|
9536
9725
|
});
|
|
9537
9726
|
async function setupTurso(urlArg, tokenArg, mode) {
|
|
9538
9727
|
let db;
|
|
9728
|
+
const arch = getSystemProfile().arch;
|
|
9729
|
+
const isWindowsArm64 = process.platform === "win32" && arch === "arm64";
|
|
9730
|
+
const effectiveMode = mode ?? (isWindowsArm64 ? "remote" : void 0);
|
|
9731
|
+
if (!mode && isWindowsArm64) {
|
|
9732
|
+
console.log(
|
|
9733
|
+
"Detected Windows ARM64 \u2014 defaulting to remote (HTTP) mode.\n The native libsql driver is not available on this architecture.\n Remote mode uses the Turso HTTP API and works everywhere.\n"
|
|
9734
|
+
);
|
|
9735
|
+
}
|
|
9539
9736
|
try {
|
|
9540
|
-
const url = urlArg ?? await
|
|
9737
|
+
const url = urlArg ?? await input({
|
|
9541
9738
|
message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
|
|
9542
9739
|
});
|
|
9543
9740
|
const token = tokenArg ?? await password({
|
|
@@ -9547,12 +9744,12 @@ async function setupTurso(urlArg, tokenArg, mode) {
|
|
|
9547
9744
|
console.error("Both URL and token are required.");
|
|
9548
9745
|
process.exit(1);
|
|
9549
9746
|
}
|
|
9550
|
-
setTursoCredentials(url, token, void 0,
|
|
9747
|
+
setTursoCredentials(url, token, void 0, effectiveMode);
|
|
9551
9748
|
db = await openDatabaseWithSync({ initialize: true });
|
|
9552
9749
|
await db.prepare("SELECT 1").get();
|
|
9553
9750
|
await db.close();
|
|
9554
9751
|
console.log(
|
|
9555
|
-
`Turso cloud database configured and verified: ${url}` + (
|
|
9752
|
+
`Turso cloud database configured and verified: ${url}` + (effectiveMode ? ` (mode: ${effectiveMode})` : "")
|
|
9556
9753
|
);
|
|
9557
9754
|
} catch (err) {
|
|
9558
9755
|
await db?.close();
|
|
@@ -9567,26 +9764,26 @@ async function setupTurso(urlArg, tokenArg, mode) {
|
|
|
9567
9764
|
|
|
9568
9765
|
// src/cli/commands/git-sync.ts
|
|
9569
9766
|
import { execSync as execSync5 } from "child_process";
|
|
9570
|
-
import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as
|
|
9571
|
-
import { join as
|
|
9572
|
-
import { Command as
|
|
9767
|
+
import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync8 } from "fs";
|
|
9768
|
+
import { join as join19 } from "path";
|
|
9769
|
+
import { Command as Command5 } from "commander";
|
|
9573
9770
|
function installHook2() {
|
|
9574
|
-
const gitDir =
|
|
9771
|
+
const gitDir = join19(process.cwd(), ".git");
|
|
9575
9772
|
if (!existsSync18(gitDir)) {
|
|
9576
9773
|
console.error(
|
|
9577
9774
|
"Error: Current directory is not the root of a Git repository."
|
|
9578
9775
|
);
|
|
9579
9776
|
process.exit(1);
|
|
9580
9777
|
}
|
|
9581
|
-
const hooksDir =
|
|
9582
|
-
const hookPath =
|
|
9778
|
+
const hooksDir = join19(gitDir, "hooks");
|
|
9779
|
+
const hookPath = join19(hooksDir, "post-commit");
|
|
9583
9780
|
const hookContent = `#!/bin/sh
|
|
9584
9781
|
# ZAM Spaced Repetition Auto-Stale Hook
|
|
9585
9782
|
# Triggered automatically on git commits to decay modified concept cards.
|
|
9586
9783
|
zam git-sync --commit HEAD --quiet
|
|
9587
9784
|
`;
|
|
9588
9785
|
try {
|
|
9589
|
-
|
|
9786
|
+
writeFileSync8(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
|
|
9590
9787
|
try {
|
|
9591
9788
|
chmodSync2(hookPath, "755");
|
|
9592
9789
|
} catch (_e) {
|
|
@@ -9599,7 +9796,7 @@ zam git-sync --commit HEAD --quiet
|
|
|
9599
9796
|
process.exit(1);
|
|
9600
9797
|
}
|
|
9601
9798
|
}
|
|
9602
|
-
var gitSyncCommand = new
|
|
9799
|
+
var gitSyncCommand = new Command5("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
|
|
9603
9800
|
if (opts.install) {
|
|
9604
9801
|
installHook2();
|
|
9605
9802
|
return;
|
|
@@ -9689,9 +9886,9 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
|
|
|
9689
9886
|
|
|
9690
9887
|
// src/cli/commands/goal.ts
|
|
9691
9888
|
import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
|
|
9692
|
-
import { resolve as
|
|
9693
|
-
import { input as
|
|
9694
|
-
import { Command as
|
|
9889
|
+
import { resolve as resolve5 } from "path";
|
|
9890
|
+
import { input as input2 } from "@inquirer/prompts";
|
|
9891
|
+
import { Command as Command6 } from "commander";
|
|
9695
9892
|
async function resolveGoalsDir() {
|
|
9696
9893
|
let goalsDir;
|
|
9697
9894
|
let db;
|
|
@@ -9702,9 +9899,9 @@ async function resolveGoalsDir() {
|
|
|
9702
9899
|
} finally {
|
|
9703
9900
|
await db?.close();
|
|
9704
9901
|
}
|
|
9705
|
-
return goalsDir ?
|
|
9902
|
+
return goalsDir ? resolve5(goalsDir) : resolve5("goals");
|
|
9706
9903
|
}
|
|
9707
|
-
var goalCommand = new
|
|
9904
|
+
var goalCommand = new Command6("goal").description(
|
|
9708
9905
|
"Manage learning goals (markdown files)"
|
|
9709
9906
|
);
|
|
9710
9907
|
goalCommand.command("list").description("List all goals").option(
|
|
@@ -9823,11 +10020,11 @@ goalCommand.command("create").description("Create a new goal").option("--slug <s
|
|
|
9823
10020
|
if (!slug || !title) {
|
|
9824
10021
|
try {
|
|
9825
10022
|
if (!title) {
|
|
9826
|
-
title = await
|
|
10023
|
+
title = await input2({ message: "Goal title:" });
|
|
9827
10024
|
}
|
|
9828
10025
|
if (!slug) {
|
|
9829
10026
|
const suggested = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
9830
|
-
slug = await
|
|
10027
|
+
slug = await input2({
|
|
9831
10028
|
message: "Goal slug (filename):",
|
|
9832
10029
|
default: suggested
|
|
9833
10030
|
});
|
|
@@ -9873,22 +10070,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
|
|
|
9873
10070
|
});
|
|
9874
10071
|
|
|
9875
10072
|
// src/cli/commands/init.ts
|
|
9876
|
-
import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as
|
|
10073
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
9877
10074
|
import { homedir as homedir11 } from "os";
|
|
9878
|
-
import { join as
|
|
9879
|
-
import { confirm
|
|
9880
|
-
import { Command as
|
|
10075
|
+
import { join as join20, resolve as resolve6 } from "path";
|
|
10076
|
+
import { confirm, input as input3 } from "@inquirer/prompts";
|
|
10077
|
+
import { Command as Command7 } from "commander";
|
|
9881
10078
|
var HOME2 = homedir11();
|
|
9882
10079
|
function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
|
|
9883
10080
|
console.log(`${color}${char.repeat(len)}\x1B[0m`);
|
|
9884
10081
|
}
|
|
9885
10082
|
function bootstrapSandboxWorkspace(workspaceDir) {
|
|
9886
|
-
mkdirSync12(
|
|
9887
|
-
mkdirSync12(
|
|
9888
|
-
mkdirSync12(
|
|
9889
|
-
const worldviewFile =
|
|
10083
|
+
mkdirSync12(join20(workspaceDir, "beliefs"), { recursive: true });
|
|
10084
|
+
mkdirSync12(join20(workspaceDir, "goals"), { recursive: true });
|
|
10085
|
+
mkdirSync12(join20(workspaceDir, "skills"), { recursive: true });
|
|
10086
|
+
const worldviewFile = join20(workspaceDir, "beliefs", "worldview.md");
|
|
9890
10087
|
if (!existsSync20(worldviewFile)) {
|
|
9891
|
-
|
|
10088
|
+
writeFileSync9(
|
|
9892
10089
|
worldviewFile,
|
|
9893
10090
|
`# Personal Worldview
|
|
9894
10091
|
|
|
@@ -9900,9 +10097,9 @@ Here, I declare the core concepts and principles I want to master.
|
|
|
9900
10097
|
"utf8"
|
|
9901
10098
|
);
|
|
9902
10099
|
}
|
|
9903
|
-
const goalsFile =
|
|
10100
|
+
const goalsFile = join20(workspaceDir, "goals", "goals.md");
|
|
9904
10101
|
if (!existsSync20(goalsFile)) {
|
|
9905
|
-
|
|
10102
|
+
writeFileSync9(
|
|
9906
10103
|
goalsFile,
|
|
9907
10104
|
`# Personal Goals
|
|
9908
10105
|
|
|
@@ -9914,7 +10111,7 @@ Here, I declare the core concepts and principles I want to master.
|
|
|
9914
10111
|
);
|
|
9915
10112
|
}
|
|
9916
10113
|
}
|
|
9917
|
-
var initCommand = new
|
|
10114
|
+
var initCommand = new Command7("init").description("Launch the guided interactive onboarding wizard").action(async () => {
|
|
9918
10115
|
printLine();
|
|
9919
10116
|
console.log(
|
|
9920
10117
|
"\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
|
|
@@ -9924,9 +10121,9 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
|
|
|
9924
10121
|
);
|
|
9925
10122
|
printLine();
|
|
9926
10123
|
console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
|
|
9927
|
-
const defaultWorkspace =
|
|
9928
|
-
const workspacePath =
|
|
9929
|
-
await
|
|
10124
|
+
const defaultWorkspace = join20(HOME2, "Documents", "zam");
|
|
10125
|
+
const workspacePath = resolve6(
|
|
10126
|
+
await input3({
|
|
9930
10127
|
message: "Choose your ZAM workspace directory:",
|
|
9931
10128
|
default: defaultWorkspace
|
|
9932
10129
|
})
|
|
@@ -9940,6 +10137,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
|
|
|
9940
10137
|
kind: "personal",
|
|
9941
10138
|
path: workspacePath
|
|
9942
10139
|
});
|
|
10140
|
+
setActiveWorkspaceId("personal");
|
|
9943
10141
|
console.log(
|
|
9944
10142
|
`\x1B[32m\u2713 Local Sandbox created at: ${workspacePath}\x1B[0m`
|
|
9945
10143
|
);
|
|
@@ -9970,7 +10168,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
|
|
|
9970
10168
|
\x1B[1mRecommendation:\x1B[0m ZAM suggests installing \x1B[32m${runnerLabel}\x1B[0m with \x1B[36m${profile.recommendedModel}\x1B[0m.`
|
|
9971
10169
|
);
|
|
9972
10170
|
console.log("\n\x1B[1m[3/5] Setting up Local LLM Runner\x1B[0m");
|
|
9973
|
-
const proceedInstall = await
|
|
10171
|
+
const proceedInstall = await confirm({
|
|
9974
10172
|
message: `Would you like ZAM to install and configure ${runnerLabel} automatically?`,
|
|
9975
10173
|
default: true
|
|
9976
10174
|
});
|
|
@@ -10007,7 +10205,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
|
|
|
10007
10205
|
let db;
|
|
10008
10206
|
try {
|
|
10009
10207
|
db = await openDatabaseWithSync({ initialize: true });
|
|
10010
|
-
await
|
|
10208
|
+
await deleteSetting(db, "personal.workspace_dir");
|
|
10011
10209
|
const detectedLocale = detectSystemLocale();
|
|
10012
10210
|
await setSetting(db, "system.locale", detectedLocale);
|
|
10013
10211
|
console.log(
|
|
@@ -10038,7 +10236,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
|
|
|
10038
10236
|
console.log(
|
|
10039
10237
|
"\n\x1B[1m[5/5] Wiring Developer Agents & Terminal Helpers\x1B[0m"
|
|
10040
10238
|
);
|
|
10041
|
-
const proceedHooks = await
|
|
10239
|
+
const proceedHooks = await confirm({
|
|
10042
10240
|
message: "Distribute ZAM skills and install optional monitored-session shell helpers?",
|
|
10043
10241
|
default: true
|
|
10044
10242
|
});
|
|
@@ -10089,8 +10287,8 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
|
|
|
10089
10287
|
});
|
|
10090
10288
|
|
|
10091
10289
|
// src/cli/commands/learn.ts
|
|
10092
|
-
import { input as
|
|
10093
|
-
import { Command as
|
|
10290
|
+
import { input as input5 } from "@inquirer/prompts";
|
|
10291
|
+
import { Command as Command8 } from "commander";
|
|
10094
10292
|
|
|
10095
10293
|
// src/cli/learn-format.ts
|
|
10096
10294
|
var BLOOM_VERBS3 = {
|
|
@@ -10139,7 +10337,7 @@ function formatReveal(input8) {
|
|
|
10139
10337
|
}
|
|
10140
10338
|
|
|
10141
10339
|
// src/cli/review-actions.ts
|
|
10142
|
-
import { confirm as
|
|
10340
|
+
import { confirm as confirm2, input as input4, select } from "@inquirer/prompts";
|
|
10143
10341
|
async function runInteractiveReviewAction(inputData) {
|
|
10144
10342
|
let currentItem = { ...inputData.item };
|
|
10145
10343
|
while (true) {
|
|
@@ -10254,7 +10452,7 @@ async function runInteractiveReviewAction(inputData) {
|
|
|
10254
10452
|
continue;
|
|
10255
10453
|
}
|
|
10256
10454
|
if (choice === "deprecate-token") {
|
|
10257
|
-
const approved = await
|
|
10455
|
+
const approved = await confirm2({
|
|
10258
10456
|
message: `Deprecate ${currentItem.slug} so it stops appearing in review queues?`,
|
|
10259
10457
|
default: false
|
|
10260
10458
|
});
|
|
@@ -10285,7 +10483,7 @@ async function runInteractiveReviewAction(inputData) {
|
|
|
10285
10483
|
console.log(` Session steps: ${impact.session_steps}`);
|
|
10286
10484
|
console.log(` Sessions touched: ${impact.sessions_touched}`);
|
|
10287
10485
|
console.log(` Agent skills updated: ${impact.agent_skills}`);
|
|
10288
|
-
const approved = await
|
|
10486
|
+
const approved = await confirm2({
|
|
10289
10487
|
message: "Permanently delete this token and its dependent learning data?",
|
|
10290
10488
|
default: false
|
|
10291
10489
|
});
|
|
@@ -10310,7 +10508,7 @@ async function runInteractiveReviewAction(inputData) {
|
|
|
10310
10508
|
);
|
|
10311
10509
|
console.log(`Delete your card for ${currentItem.slug}?`);
|
|
10312
10510
|
console.log(` Review logs removed: ${impact.review_logs}`);
|
|
10313
|
-
const approved = await
|
|
10511
|
+
const approved = await confirm2({
|
|
10314
10512
|
message: "Delete only your card for this token?",
|
|
10315
10513
|
default: false
|
|
10316
10514
|
});
|
|
@@ -10348,21 +10546,21 @@ async function promptTokenEdit(token) {
|
|
|
10348
10546
|
}
|
|
10349
10547
|
switch (field) {
|
|
10350
10548
|
case "concept": {
|
|
10351
|
-
const concept = await
|
|
10549
|
+
const concept = await input4({
|
|
10352
10550
|
message: "Concept:",
|
|
10353
10551
|
default: token.concept
|
|
10354
10552
|
});
|
|
10355
10553
|
return concept === token.concept ? null : { concept };
|
|
10356
10554
|
}
|
|
10357
10555
|
case "domain": {
|
|
10358
|
-
const domain = await
|
|
10556
|
+
const domain = await input4({
|
|
10359
10557
|
message: "Domain (blank allowed):",
|
|
10360
10558
|
default: token.domain
|
|
10361
10559
|
});
|
|
10362
10560
|
return domain === token.domain ? null : { domain };
|
|
10363
10561
|
}
|
|
10364
10562
|
case "context": {
|
|
10365
|
-
const context = await
|
|
10563
|
+
const context = await input4({
|
|
10366
10564
|
message: "Context (blank allowed):",
|
|
10367
10565
|
default: token.context
|
|
10368
10566
|
});
|
|
@@ -10403,14 +10601,14 @@ async function promptTokenEdit(token) {
|
|
|
10403
10601
|
return mode === token.symbiosis_mode ? null : { symbiosis_mode: mode };
|
|
10404
10602
|
}
|
|
10405
10603
|
case "source_link": {
|
|
10406
|
-
const link = await
|
|
10604
|
+
const link = await input4({
|
|
10407
10605
|
message: "Source link (blank allowed):",
|
|
10408
10606
|
default: token.source_link ?? ""
|
|
10409
10607
|
});
|
|
10410
10608
|
return link === token.source_link ? null : { source_link: link || null };
|
|
10411
10609
|
}
|
|
10412
10610
|
case "question": {
|
|
10413
|
-
const question = await
|
|
10611
|
+
const question = await input4({
|
|
10414
10612
|
message: "Recall question (blank allowed):",
|
|
10415
10613
|
default: token.question ?? ""
|
|
10416
10614
|
});
|
|
@@ -10428,7 +10626,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
|
|
|
10428
10626
|
function isExitPrompt(err) {
|
|
10429
10627
|
return err instanceof Error && err.name === "ExitPromptError";
|
|
10430
10628
|
}
|
|
10431
|
-
var learnCommand = new
|
|
10629
|
+
var learnCommand = new Command8("learn").description(
|
|
10432
10630
|
"Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
|
|
10433
10631
|
).option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into the revealed answer").action(async (opts) => {
|
|
10434
10632
|
let db;
|
|
@@ -10484,7 +10682,7 @@ ${t(locale, "welcome", { count: queue.items.length })}`);
|
|
|
10484
10682
|
question: item.question
|
|
10485
10683
|
});
|
|
10486
10684
|
if (healed) {
|
|
10487
|
-
resolvedQuestion = healed;
|
|
10685
|
+
resolvedQuestion = healed.question;
|
|
10488
10686
|
}
|
|
10489
10687
|
} catch {
|
|
10490
10688
|
}
|
|
@@ -10508,7 +10706,7 @@ ${"\u2500".repeat(50)}`);
|
|
|
10508
10706
|
${prompt.question}`);
|
|
10509
10707
|
let answer;
|
|
10510
10708
|
try {
|
|
10511
|
-
answer = await
|
|
10709
|
+
answer = await input5({
|
|
10512
10710
|
message: t(locale, "prompt_answer")
|
|
10513
10711
|
});
|
|
10514
10712
|
} catch (err) {
|
|
@@ -10549,7 +10747,8 @@ ${"\u2500".repeat(50)}`);
|
|
|
10549
10747
|
`
|
|
10550
10748
|
${t(locale, "feedback_title", { line: "\u2500".repeat(34) })}`
|
|
10551
10749
|
);
|
|
10552
|
-
|
|
10750
|
+
console.log(` \x1B[2m[${evaluation.model}]\x1B[0m`);
|
|
10751
|
+
for (const line of evaluation.text.split("\n")) {
|
|
10553
10752
|
console.log(` ${line}`);
|
|
10554
10753
|
}
|
|
10555
10754
|
} catch (err) {
|
|
@@ -10672,8 +10871,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
|
|
|
10672
10871
|
});
|
|
10673
10872
|
|
|
10674
10873
|
// src/cli/commands/monitor.ts
|
|
10675
|
-
import { Command as
|
|
10676
|
-
var monitorCommand = new
|
|
10874
|
+
import { Command as Command9 } from "commander";
|
|
10875
|
+
var monitorCommand = new Command9("monitor").description(
|
|
10677
10876
|
"Shell observation for real-time task monitoring"
|
|
10678
10877
|
);
|
|
10679
10878
|
monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
|
|
@@ -10855,8 +11054,8 @@ monitorCommand.command("open").description("Open a new monitored terminal window
|
|
|
10855
11054
|
});
|
|
10856
11055
|
|
|
10857
11056
|
// src/cli/commands/observer.ts
|
|
10858
|
-
import { Command as
|
|
10859
|
-
var observerCommand = new
|
|
11057
|
+
import { Command as Command10 } from "commander";
|
|
11058
|
+
var observerCommand = new Command10("observer").description(
|
|
10860
11059
|
"Configure what the UI observer may capture (Layer 2 policy)"
|
|
10861
11060
|
);
|
|
10862
11061
|
function applyObserverListChange(current, entry, op) {
|
|
@@ -10918,9 +11117,8 @@ observerCommand.command("revoke <process>").description("Remove a process from o
|
|
|
10918
11117
|
});
|
|
10919
11118
|
|
|
10920
11119
|
// src/cli/commands/profile.ts
|
|
10921
|
-
import {
|
|
10922
|
-
import {
|
|
10923
|
-
import { Command as Command13 } from "commander";
|
|
11120
|
+
import { dirname as dirname6, resolve as resolve7 } from "path";
|
|
11121
|
+
import { Command as Command11 } from "commander";
|
|
10924
11122
|
var C2 = {
|
|
10925
11123
|
reset: "\x1B[0m",
|
|
10926
11124
|
bold: "\x1B[1m",
|
|
@@ -10928,9 +11126,6 @@ var C2 = {
|
|
|
10928
11126
|
dim: "\x1B[2m",
|
|
10929
11127
|
green: "\x1B[32m"
|
|
10930
11128
|
};
|
|
10931
|
-
function defaultPersonalDir() {
|
|
10932
|
-
return join20(homedir12(), "Documents", "zam");
|
|
10933
|
-
}
|
|
10934
11129
|
function render(profile) {
|
|
10935
11130
|
const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
|
|
10936
11131
|
console.log(`${C2.bold}ZAM install profile${C2.reset}`);
|
|
@@ -10940,7 +11135,7 @@ function render(profile) {
|
|
|
10940
11135
|
console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
|
|
10941
11136
|
console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
|
|
10942
11137
|
}
|
|
10943
|
-
var profileCommand = new
|
|
11138
|
+
var profileCommand = new Command11("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
|
|
10944
11139
|
if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
|
|
10945
11140
|
console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
|
|
10946
11141
|
process.exit(1);
|
|
@@ -10950,9 +11145,12 @@ var profileCommand = new Command13("profile").description("Show or change this m
|
|
|
10950
11145
|
if (opts.mode) setInstallMode(opts.mode);
|
|
10951
11146
|
db = await openDatabaseWithSync({ initialize: true });
|
|
10952
11147
|
if (opts.dir) {
|
|
10953
|
-
await
|
|
11148
|
+
await activateWorkspacePath(db, resolve7(opts.dir), {
|
|
11149
|
+
kind: "personal",
|
|
11150
|
+
label: "Personal"
|
|
11151
|
+
});
|
|
10954
11152
|
}
|
|
10955
|
-
const personalDir = await
|
|
11153
|
+
const personalDir = (await ensureActiveWorkspace(db)).path;
|
|
10956
11154
|
await db.close();
|
|
10957
11155
|
db = void 0;
|
|
10958
11156
|
const dbPath = getDefaultDbPath();
|
|
@@ -10977,120 +11175,8 @@ var profileCommand = new Command13("profile").description("Show or change this m
|
|
|
10977
11175
|
|
|
10978
11176
|
// src/cli/commands/provider.ts
|
|
10979
11177
|
import { password as password2 } from "@inquirer/prompts";
|
|
10980
|
-
import { Command as
|
|
10981
|
-
var
|
|
10982
|
-
"chat-completions",
|
|
10983
|
-
"anthropic-messages"
|
|
10984
|
-
];
|
|
10985
|
-
var VALID_ROLES = ["vision", "recall", "text"];
|
|
10986
|
-
function upsertProviderRecord(providers, name, patch) {
|
|
10987
|
-
const merged = { ...providers[name] ?? {} };
|
|
10988
|
-
if (patch.url !== void 0) merged.url = patch.url;
|
|
10989
|
-
if (patch.model !== void 0) merged.model = patch.model;
|
|
10990
|
-
if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
|
|
10991
|
-
if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
|
|
10992
|
-
if (patch.label !== void 0) merged.label = patch.label;
|
|
10993
|
-
if (patch.local !== void 0) merged.local = patch.local;
|
|
10994
|
-
if (patch.runner !== void 0) merged.runner = patch.runner;
|
|
10995
|
-
return { ...providers, [name]: merged };
|
|
10996
|
-
}
|
|
10997
|
-
function removeProviderRecord(providers, name) {
|
|
10998
|
-
if (!(name in providers)) return { providers, removed: false };
|
|
10999
|
-
const next = { ...providers };
|
|
11000
|
-
delete next[name];
|
|
11001
|
-
return { providers: next, removed: true };
|
|
11002
|
-
}
|
|
11003
|
-
function rolesReferencing(roles, name) {
|
|
11004
|
-
return VALID_ROLES.filter((role) => {
|
|
11005
|
-
const binding = roles[role];
|
|
11006
|
-
return binding?.primary === name || binding?.fallback === name;
|
|
11007
|
-
});
|
|
11008
|
-
}
|
|
11009
|
-
function bindRoleProviders(roles, role, primary, fallback) {
|
|
11010
|
-
const binding = { primary };
|
|
11011
|
-
if (fallback) binding.fallback = fallback;
|
|
11012
|
-
return { ...roles, [role]: binding };
|
|
11013
|
-
}
|
|
11014
|
-
function maskSecret(key) {
|
|
11015
|
-
return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
|
|
11016
|
-
}
|
|
11017
|
-
function buildProviderListing(providers, hasKey) {
|
|
11018
|
-
return Object.entries(providers).map(([name, rec]) => {
|
|
11019
|
-
const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
|
|
11020
|
-
let keyState;
|
|
11021
|
-
if (!rec.apiKeyRef) keyState = "none";
|
|
11022
|
-
else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
|
|
11023
|
-
const row = {
|
|
11024
|
-
name,
|
|
11025
|
-
url: rec.url,
|
|
11026
|
-
model: rec.model,
|
|
11027
|
-
apiFlavor,
|
|
11028
|
-
apiKeyRef: rec.apiKeyRef,
|
|
11029
|
-
keyState
|
|
11030
|
-
};
|
|
11031
|
-
if (rec.label !== void 0) row.label = rec.label;
|
|
11032
|
-
if (rec.local !== void 0) row.local = rec.local;
|
|
11033
|
-
if (rec.runner !== void 0) row.runner = rec.runner;
|
|
11034
|
-
return row;
|
|
11035
|
-
});
|
|
11036
|
-
}
|
|
11037
|
-
function findOrphanKeyRefs(storedRefs, providers) {
|
|
11038
|
-
const used = new Set(
|
|
11039
|
-
Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
|
|
11040
|
-
);
|
|
11041
|
-
return storedRefs.filter((ref) => !used.has(ref));
|
|
11042
|
-
}
|
|
11043
|
-
async function readJson(db, key, fallback) {
|
|
11044
|
-
const raw = await getSetting(db, key);
|
|
11045
|
-
if (!raw) return fallback;
|
|
11046
|
-
try {
|
|
11047
|
-
return JSON.parse(raw);
|
|
11048
|
-
} catch {
|
|
11049
|
-
return fallback;
|
|
11050
|
-
}
|
|
11051
|
-
}
|
|
11052
|
-
var readProviders = (db) => readJson(db, "llm.providers", {});
|
|
11053
|
-
var readRoles = (db) => readJson(db, "llm.roles", {});
|
|
11054
|
-
async function readScopedProviders(db, machine) {
|
|
11055
|
-
if (machine) return getMachineAiConfig().providers ?? {};
|
|
11056
|
-
if (!db)
|
|
11057
|
-
throw new Error("Database is required for shared provider settings.");
|
|
11058
|
-
return readProviders(db);
|
|
11059
|
-
}
|
|
11060
|
-
async function readScopedRoles(db, machine) {
|
|
11061
|
-
if (machine) return getMachineAiConfig().roles ?? {};
|
|
11062
|
-
if (!db)
|
|
11063
|
-
throw new Error("Database is required for shared provider settings.");
|
|
11064
|
-
return readRoles(db);
|
|
11065
|
-
}
|
|
11066
|
-
async function writeScopedProviders(db, machine, p) {
|
|
11067
|
-
if (machine) {
|
|
11068
|
-
const config = getMachineAiConfig();
|
|
11069
|
-
saveMachineAiConfig({ ...config, providers: p });
|
|
11070
|
-
return;
|
|
11071
|
-
}
|
|
11072
|
-
if (!db)
|
|
11073
|
-
throw new Error("Database is required for shared provider settings.");
|
|
11074
|
-
await setSetting(db, "llm.providers", JSON.stringify(p));
|
|
11075
|
-
}
|
|
11076
|
-
async function writeScopedRoles(db, machine, r) {
|
|
11077
|
-
if (machine) {
|
|
11078
|
-
const config = getMachineAiConfig();
|
|
11079
|
-
saveMachineAiConfig({ ...config, roles: r });
|
|
11080
|
-
return;
|
|
11081
|
-
}
|
|
11082
|
-
if (!db)
|
|
11083
|
-
throw new Error("Database is required for shared provider settings.");
|
|
11084
|
-
await setSetting(db, "llm.roles", JSON.stringify(r));
|
|
11085
|
-
}
|
|
11086
|
-
async function withProviderScope(machine, action) {
|
|
11087
|
-
if (machine) {
|
|
11088
|
-
await action(void 0);
|
|
11089
|
-
return;
|
|
11090
|
-
}
|
|
11091
|
-
await withDb(action);
|
|
11092
|
-
}
|
|
11093
|
-
var providerCommand = new Command14("provider").description(
|
|
11178
|
+
import { Command as Command12 } from "commander";
|
|
11179
|
+
var providerCommand = new Command12("provider").description(
|
|
11094
11180
|
"Configure role-based AI providers (url/model/flavor/key, per role)"
|
|
11095
11181
|
);
|
|
11096
11182
|
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) => {
|
|
@@ -11325,8 +11411,8 @@ providerCommand.command("clear-key <ref>").description("Remove a stored API key"
|
|
|
11325
11411
|
});
|
|
11326
11412
|
|
|
11327
11413
|
// src/cli/commands/review.ts
|
|
11328
|
-
import { Command as
|
|
11329
|
-
var reviewCommand = new
|
|
11414
|
+
import { Command as Command13 } from "commander";
|
|
11415
|
+
var reviewCommand = new Command13("review").description("Start an interactive review session").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 inline context").action(async (opts) => {
|
|
11330
11416
|
let db;
|
|
11331
11417
|
try {
|
|
11332
11418
|
db = await openDatabase();
|
|
@@ -11440,9 +11526,9 @@ ${"\u2550".repeat(50)}`);
|
|
|
11440
11526
|
|
|
11441
11527
|
// src/cli/commands/session.ts
|
|
11442
11528
|
import { readFileSync as readFileSync12 } from "fs";
|
|
11443
|
-
import { input as
|
|
11444
|
-
import { Command as
|
|
11445
|
-
var sessionCommand = new
|
|
11529
|
+
import { input as input6, select as select2 } from "@inquirer/prompts";
|
|
11530
|
+
import { Command as Command14 } from "commander";
|
|
11531
|
+
var sessionCommand = new Command14("session").description(
|
|
11446
11532
|
"Manage learning sessions"
|
|
11447
11533
|
);
|
|
11448
11534
|
sessionCommand.command("start").description("Start a new learning session (review \u2192 task)").option("--user <id>", "User ID (default: whoami)").option("--task <description>", "Task description (interactive if omitted)").option(
|
|
@@ -11619,7 +11705,7 @@ async function selectTask() {
|
|
|
11619
11705
|
console.log("No active work items found in Azure DevOps.");
|
|
11620
11706
|
}
|
|
11621
11707
|
}
|
|
11622
|
-
return
|
|
11708
|
+
return input6({ message: "Task description:" });
|
|
11623
11709
|
}
|
|
11624
11710
|
var RATING_LABELS = {
|
|
11625
11711
|
1: "Again",
|
|
@@ -11822,8 +11908,8 @@ sessionCommand.command("end").description("End a session and show summary").requ
|
|
|
11822
11908
|
|
|
11823
11909
|
// src/cli/commands/settings.ts
|
|
11824
11910
|
import { existsSync as existsSync21 } from "fs";
|
|
11825
|
-
import { Command as
|
|
11826
|
-
var settingsCommand = new
|
|
11911
|
+
import { Command as Command15 } from "commander";
|
|
11912
|
+
var settingsCommand = new Command15("settings").description(
|
|
11827
11913
|
"Manage user settings"
|
|
11828
11914
|
);
|
|
11829
11915
|
var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
|
|
@@ -11994,12 +12080,108 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
|
|
|
11994
12080
|
console.log(` ${name.padEnd(9)}: \x1B[33m- Not Set\x1B[0m`);
|
|
11995
12081
|
}
|
|
11996
12082
|
}
|
|
11997
|
-
});
|
|
11998
|
-
});
|
|
12083
|
+
});
|
|
12084
|
+
});
|
|
12085
|
+
|
|
12086
|
+
// src/cli/commands/setup.ts
|
|
12087
|
+
import { resolve as resolve8 } from "path";
|
|
12088
|
+
import { Command as Command16 } from "commander";
|
|
12089
|
+
function formatDatabaseInitTarget(target) {
|
|
12090
|
+
switch (target.kind) {
|
|
12091
|
+
case "local":
|
|
12092
|
+
return `ZAM database at ${target.location} (local SQLite)`;
|
|
12093
|
+
case "turso-remote":
|
|
12094
|
+
return `ZAM database via Turso remote at ${target.location}`;
|
|
12095
|
+
case "turso-native":
|
|
12096
|
+
return `ZAM database via Turso native driver at ${target.location}`;
|
|
12097
|
+
case "turso-replica":
|
|
12098
|
+
return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
|
|
12099
|
+
}
|
|
12100
|
+
}
|
|
12101
|
+
async function initDatabase(skipInit) {
|
|
12102
|
+
if (skipInit) return;
|
|
12103
|
+
try {
|
|
12104
|
+
const target = getDatabaseTargetInfo();
|
|
12105
|
+
const db = await openDatabaseWithSync({ initialize: true });
|
|
12106
|
+
await activateMachineProviderConfig(db);
|
|
12107
|
+
await db.close();
|
|
12108
|
+
console.log(` init ${formatDatabaseInitTarget(target)}`);
|
|
12109
|
+
} catch (err) {
|
|
12110
|
+
const msg = err.message;
|
|
12111
|
+
if (!msg.includes("already")) {
|
|
12112
|
+
console.warn(` warn database init: ${msg}`);
|
|
12113
|
+
} else {
|
|
12114
|
+
console.log(` skip database already initialized`);
|
|
12115
|
+
}
|
|
12116
|
+
}
|
|
12117
|
+
}
|
|
12118
|
+
async function activateMachineProviderConfig(db) {
|
|
12119
|
+
const machineAi = getMachineAiConfig();
|
|
12120
|
+
const providerCount = Object.keys(machineAi.providers ?? {}).length;
|
|
12121
|
+
const roleCount = Object.keys(machineAi.roles ?? {}).length;
|
|
12122
|
+
if (providerCount === 0 && roleCount === 0) return;
|
|
12123
|
+
if (await getSetting(db, "llm.enabled") !== void 0) return;
|
|
12124
|
+
await setSetting(db, "llm.enabled", "true");
|
|
12125
|
+
console.log(
|
|
12126
|
+
` activate ${providerCount} machine-local provider(s) from ~/.zam/config.json`
|
|
12127
|
+
);
|
|
12128
|
+
}
|
|
12129
|
+
var setupCommand = new Command16("setup").description(
|
|
12130
|
+
"Link ZAM skill directories into this workspace and initialize the database"
|
|
12131
|
+
).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(
|
|
12132
|
+
"--agents <list>",
|
|
12133
|
+
"comma-separated agents to wire: all, claude, copilot, codex, agent"
|
|
12134
|
+
).option(
|
|
12135
|
+
"--dry-run",
|
|
12136
|
+
"show what would be written without changing files",
|
|
12137
|
+
false
|
|
12138
|
+
).action(
|
|
12139
|
+
async (opts) => {
|
|
12140
|
+
let agents;
|
|
12141
|
+
try {
|
|
12142
|
+
agents = parseSetupAgents(opts.agents);
|
|
12143
|
+
} catch (err) {
|
|
12144
|
+
console.error(`Error: ${err.message}`);
|
|
12145
|
+
process.exit(1);
|
|
12146
|
+
}
|
|
12147
|
+
const target = resolve8(opts.target ?? process.cwd());
|
|
12148
|
+
const updateExistingInstructions = Boolean(opts.target) || opts.force;
|
|
12149
|
+
console.log(
|
|
12150
|
+
`Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
|
|
12151
|
+
`
|
|
12152
|
+
);
|
|
12153
|
+
wireSkills(target, agents, {
|
|
12154
|
+
force: opts.force,
|
|
12155
|
+
dryRun: opts.dryRun
|
|
12156
|
+
});
|
|
12157
|
+
await initDatabase(opts.skipInit || opts.dryRun);
|
|
12158
|
+
if (agents.has("claude")) {
|
|
12159
|
+
writeClaudeMd(opts.skipClaudeMd, target, {
|
|
12160
|
+
dryRun: opts.dryRun,
|
|
12161
|
+
updateExisting: updateExistingInstructions
|
|
12162
|
+
});
|
|
12163
|
+
}
|
|
12164
|
+
if (agents.has("codex") || agents.has("agent")) {
|
|
12165
|
+
writeAgentsMd(opts.skipAgentsMd, target, {
|
|
12166
|
+
dryRun: opts.dryRun,
|
|
12167
|
+
updateExisting: updateExistingInstructions
|
|
12168
|
+
});
|
|
12169
|
+
}
|
|
12170
|
+
if (agents.has("copilot") && (opts.target || opts.agents)) {
|
|
12171
|
+
writeCopilotInstructions(target, {
|
|
12172
|
+
dryRun: opts.dryRun,
|
|
12173
|
+
updateExisting: updateExistingInstructions
|
|
12174
|
+
});
|
|
12175
|
+
}
|
|
12176
|
+
console.log(
|
|
12177
|
+
"\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."
|
|
12178
|
+
);
|
|
12179
|
+
}
|
|
12180
|
+
);
|
|
11999
12181
|
|
|
12000
12182
|
// src/cli/commands/skill.ts
|
|
12001
|
-
import { Command as
|
|
12002
|
-
var skillCommand = new
|
|
12183
|
+
import { Command as Command17 } from "commander";
|
|
12184
|
+
var skillCommand = new Command17("skill").description(
|
|
12003
12185
|
"Manage agent skill entries (task recipes)"
|
|
12004
12186
|
);
|
|
12005
12187
|
skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
|
|
@@ -12084,10 +12266,9 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
|
|
|
12084
12266
|
});
|
|
12085
12267
|
|
|
12086
12268
|
// src/cli/commands/snapshot.ts
|
|
12087
|
-
import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as
|
|
12088
|
-
import { homedir as homedir13 } from "os";
|
|
12269
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
|
|
12089
12270
|
import { dirname as dirname7, join as join21 } from "path";
|
|
12090
|
-
import { Command as
|
|
12271
|
+
import { Command as Command18 } from "commander";
|
|
12091
12272
|
function defaultOutName() {
|
|
12092
12273
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
|
|
12093
12274
|
return `zam-snapshot-${stamp}.sql`;
|
|
@@ -12101,12 +12282,12 @@ function summarize(tables) {
|
|
|
12101
12282
|
}
|
|
12102
12283
|
return { total, nonEmpty };
|
|
12103
12284
|
}
|
|
12104
|
-
var exportCmd = new
|
|
12285
|
+
var exportCmd = new Command18("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
|
|
12105
12286
|
let db;
|
|
12106
12287
|
try {
|
|
12107
12288
|
db = await openDatabaseWithSync({ initialize: true });
|
|
12108
12289
|
const snapshot = await exportSnapshot(db);
|
|
12109
|
-
const personalDir = await
|
|
12290
|
+
const personalDir = (await ensureActiveWorkspace(db)).path;
|
|
12110
12291
|
await db.close();
|
|
12111
12292
|
db = void 0;
|
|
12112
12293
|
const manifest = verifySnapshot(snapshot);
|
|
@@ -12120,7 +12301,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
|
|
|
12120
12301
|
if (dir && dir !== "." && !existsSync22(dir)) {
|
|
12121
12302
|
mkdirSync13(dir, { recursive: true });
|
|
12122
12303
|
}
|
|
12123
|
-
|
|
12304
|
+
writeFileSync10(out, snapshot, "utf-8");
|
|
12124
12305
|
console.log(`Snapshot written: ${out}`);
|
|
12125
12306
|
console.log(
|
|
12126
12307
|
` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
|
|
@@ -12131,7 +12312,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
|
|
|
12131
12312
|
process.exit(1);
|
|
12132
12313
|
}
|
|
12133
12314
|
});
|
|
12134
|
-
var importCmd = new
|
|
12315
|
+
var importCmd = new Command18("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
|
|
12135
12316
|
let db;
|
|
12136
12317
|
try {
|
|
12137
12318
|
if (!existsSync22(file)) {
|
|
@@ -12154,7 +12335,7 @@ var importCmd = new Command19("import").description("Restore a snapshot into the
|
|
|
12154
12335
|
process.exit(1);
|
|
12155
12336
|
}
|
|
12156
12337
|
});
|
|
12157
|
-
var verifyCmd = new
|
|
12338
|
+
var verifyCmd = new Command18("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
|
|
12158
12339
|
try {
|
|
12159
12340
|
if (!existsSync22(file)) {
|
|
12160
12341
|
console.error(`Error: Snapshot file not found: ${file}`);
|
|
@@ -12172,11 +12353,11 @@ var verifyCmd = new Command19("verify").description("Check a snapshot's manifest
|
|
|
12172
12353
|
process.exit(1);
|
|
12173
12354
|
}
|
|
12174
12355
|
});
|
|
12175
|
-
var snapshotCommand = new
|
|
12356
|
+
var snapshotCommand = new Command18("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
|
|
12176
12357
|
|
|
12177
12358
|
// src/cli/commands/stats.ts
|
|
12178
|
-
import { Command as
|
|
12179
|
-
var statsCommand = new
|
|
12359
|
+
import { Command as Command19 } from "commander";
|
|
12360
|
+
var statsCommand = new Command19("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
|
|
12180
12361
|
await withDb(async (db) => {
|
|
12181
12362
|
const userId = await resolveUser(opts, db);
|
|
12182
12363
|
const stats = await getUserStats(db, userId);
|
|
@@ -12212,8 +12393,8 @@ var statsCommand = new Command20("stats").description("Show learning dashboard f
|
|
|
12212
12393
|
});
|
|
12213
12394
|
|
|
12214
12395
|
// src/cli/commands/token.ts
|
|
12215
|
-
import { Command as
|
|
12216
|
-
var tokenCommand = new
|
|
12396
|
+
import { Command as Command20 } from "commander";
|
|
12397
|
+
var tokenCommand = new Command20("token").description(
|
|
12217
12398
|
"Manage knowledge tokens"
|
|
12218
12399
|
);
|
|
12219
12400
|
tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
|
|
@@ -12501,10 +12682,10 @@ tokenCommand.command("status").description("Show full status of a token for a us
|
|
|
12501
12682
|
// src/cli/commands/ui.ts
|
|
12502
12683
|
import { spawn as spawn3, spawnSync } from "child_process";
|
|
12503
12684
|
import { existsSync as existsSync23 } from "fs";
|
|
12504
|
-
import { homedir as
|
|
12685
|
+
import { homedir as homedir12 } from "os";
|
|
12505
12686
|
import { dirname as dirname8, join as join22 } from "path";
|
|
12506
12687
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
12507
|
-
import { Command as
|
|
12688
|
+
import { Command as Command21 } from "commander";
|
|
12508
12689
|
var C3 = {
|
|
12509
12690
|
reset: "\x1B[0m",
|
|
12510
12691
|
red: "\x1B[31m",
|
|
@@ -12551,7 +12732,7 @@ function findInstalledApp() {
|
|
|
12551
12732
|
process.env.LOCALAPPDATA && join22(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
|
|
12552
12733
|
process.env.ProgramFiles && join22(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
|
|
12553
12734
|
process.env["ProgramFiles(x86)"] && join22(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
|
|
12554
|
-
] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(
|
|
12735
|
+
] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(homedir12(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
|
|
12555
12736
|
return candidates.find((candidate) => candidate && existsSync23(candidate)) || null;
|
|
12556
12737
|
}
|
|
12557
12738
|
function runNpm(args, opts) {
|
|
@@ -12684,13 +12865,13 @@ function createShortcuts(appPath, repoRoot) {
|
|
|
12684
12865
|
console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
|
|
12685
12866
|
}
|
|
12686
12867
|
}
|
|
12687
|
-
var uiCommand = new
|
|
12868
|
+
var uiCommand = new Command21("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
|
|
12688
12869
|
"--build",
|
|
12689
12870
|
"Build the native installer for your OS (needs Rust, one-time)"
|
|
12690
12871
|
).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
|
|
12691
12872
|
const installedApp = findInstalledApp();
|
|
12692
12873
|
if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
|
|
12693
|
-
launchApp(installedApp,
|
|
12874
|
+
launchApp(installedApp, homedir12());
|
|
12694
12875
|
return;
|
|
12695
12876
|
}
|
|
12696
12877
|
const desktopDir = findDesktopDir();
|
|
@@ -12791,8 +12972,8 @@ import { spawnSync as spawnSync2 } from "child_process";
|
|
|
12791
12972
|
import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "fs";
|
|
12792
12973
|
import { dirname as dirname9, join as join23 } from "path";
|
|
12793
12974
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
12794
|
-
import { confirm as
|
|
12795
|
-
import { Command as
|
|
12975
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
12976
|
+
import { Command as Command22 } from "commander";
|
|
12796
12977
|
var GITHUB_REPO = "zam-os/zam";
|
|
12797
12978
|
var CHANNELS = [
|
|
12798
12979
|
"developer",
|
|
@@ -12820,273 +13001,647 @@ function currentVersion() {
|
|
|
12820
13001
|
} catch {
|
|
12821
13002
|
}
|
|
12822
13003
|
}
|
|
12823
|
-
return "0.0.0";
|
|
13004
|
+
return "0.0.0";
|
|
13005
|
+
}
|
|
13006
|
+
function versionAt(dir) {
|
|
13007
|
+
try {
|
|
13008
|
+
const pkg2 = JSON.parse(
|
|
13009
|
+
readFileSync14(join23(dir, "package.json"), "utf-8")
|
|
13010
|
+
);
|
|
13011
|
+
return pkg2.version ?? "unknown";
|
|
13012
|
+
} catch {
|
|
13013
|
+
return "unknown";
|
|
13014
|
+
}
|
|
13015
|
+
}
|
|
13016
|
+
async function fetchLatestVersion(repo) {
|
|
13017
|
+
const res = await fetch(
|
|
13018
|
+
`https://api.github.com/repos/${repo}/releases/latest`,
|
|
13019
|
+
{
|
|
13020
|
+
headers: {
|
|
13021
|
+
Accept: "application/vnd.github+json",
|
|
13022
|
+
"User-Agent": "zam-cli"
|
|
13023
|
+
}
|
|
13024
|
+
}
|
|
13025
|
+
);
|
|
13026
|
+
if (!res.ok) {
|
|
13027
|
+
throw new Error(
|
|
13028
|
+
`Could not reach the release server (HTTP ${res.status}). Pass --latest <version> to check offline.`
|
|
13029
|
+
);
|
|
13030
|
+
}
|
|
13031
|
+
const data = await res.json();
|
|
13032
|
+
if (!data.tag_name) throw new Error("No published release found yet.");
|
|
13033
|
+
return data.tag_name;
|
|
13034
|
+
}
|
|
13035
|
+
function render2(decision) {
|
|
13036
|
+
if (!decision.updateAvailable) {
|
|
13037
|
+
console.log(
|
|
13038
|
+
`${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${decision.currentVersion}${C4.reset}).`
|
|
13039
|
+
);
|
|
13040
|
+
return;
|
|
13041
|
+
}
|
|
13042
|
+
console.log(
|
|
13043
|
+
`${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${decision.currentVersion}${C4.reset} \u2192 ${C4.cyan}${decision.latestVersion}${C4.reset}`
|
|
13044
|
+
);
|
|
13045
|
+
console.log(` ${decision.reason}`);
|
|
13046
|
+
if (decision.action === "run-command" || decision.action === "inform") {
|
|
13047
|
+
console.log(` Run: ${C4.cyan}${decision.command}${C4.reset}`);
|
|
13048
|
+
} else if (decision.action === "self-update") {
|
|
13049
|
+
console.log(
|
|
13050
|
+
` ${C4.dim}The desktop app can apply this update for you.${C4.reset}`
|
|
13051
|
+
);
|
|
13052
|
+
}
|
|
13053
|
+
}
|
|
13054
|
+
var checkCmd = new Command22("check").description("Check whether a newer ZAM has been released").option(
|
|
13055
|
+
"--latest <version>",
|
|
13056
|
+
"Compare against this version instead of fetching"
|
|
13057
|
+
).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
|
|
13058
|
+
async (opts) => {
|
|
13059
|
+
try {
|
|
13060
|
+
if (opts.channel && !CHANNELS.includes(opts.channel)) {
|
|
13061
|
+
console.error(
|
|
13062
|
+
`Invalid --channel: ${opts.channel}. Use ${CHANNELS.join(", ")}.`
|
|
13063
|
+
);
|
|
13064
|
+
process.exit(1);
|
|
13065
|
+
}
|
|
13066
|
+
const current = currentVersion();
|
|
13067
|
+
const latest = opts.latest ?? await fetchLatestVersion(GITHUB_REPO);
|
|
13068
|
+
const channel = opts.channel ?? getInstallChannel();
|
|
13069
|
+
const decision = decideUpdate({
|
|
13070
|
+
currentVersion: current,
|
|
13071
|
+
latestVersion: latest,
|
|
13072
|
+
channel
|
|
13073
|
+
});
|
|
13074
|
+
if (opts.json) {
|
|
13075
|
+
console.log(JSON.stringify(decision, null, 2));
|
|
13076
|
+
return;
|
|
13077
|
+
}
|
|
13078
|
+
render2(decision);
|
|
13079
|
+
} catch (err) {
|
|
13080
|
+
console.error("Error:", err.message);
|
|
13081
|
+
process.exit(1);
|
|
13082
|
+
}
|
|
13083
|
+
}
|
|
13084
|
+
);
|
|
13085
|
+
function findSourceRepo() {
|
|
13086
|
+
let dir = realpathSync2(dirname9(fileURLToPath4(import.meta.url)));
|
|
13087
|
+
let parent = dirname9(dir);
|
|
13088
|
+
while (parent !== dir) {
|
|
13089
|
+
if (existsSync24(join23(dir, ".git"))) return dir;
|
|
13090
|
+
dir = parent;
|
|
13091
|
+
parent = dirname9(dir);
|
|
13092
|
+
}
|
|
13093
|
+
return existsSync24(join23(dir, ".git")) ? dir : null;
|
|
13094
|
+
}
|
|
13095
|
+
function runGit(cwd, args, capture) {
|
|
13096
|
+
const res = spawnSync2("git", args, {
|
|
13097
|
+
cwd,
|
|
13098
|
+
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
13099
|
+
encoding: "utf8"
|
|
13100
|
+
});
|
|
13101
|
+
return { ok: res.status === 0, out: (res.stdout ?? "").trim() };
|
|
13102
|
+
}
|
|
13103
|
+
function runNpm2(args, cwd) {
|
|
13104
|
+
const res = spawnSync2("npm", args, {
|
|
13105
|
+
cwd,
|
|
13106
|
+
stdio: "inherit",
|
|
13107
|
+
shell: process.platform === "win32"
|
|
13108
|
+
});
|
|
13109
|
+
return res.status ?? 1;
|
|
13110
|
+
}
|
|
13111
|
+
function runShell(command) {
|
|
13112
|
+
const res = spawnSync2(command, { stdio: "inherit", shell: true });
|
|
13113
|
+
return res.status === 0;
|
|
13114
|
+
}
|
|
13115
|
+
function applyDeveloperUpdate(force) {
|
|
13116
|
+
if (!hasCommand("git")) {
|
|
13117
|
+
console.error(`${C4.red}\u2717${C4.reset} git was not found on PATH.`);
|
|
13118
|
+
process.exit(1);
|
|
13119
|
+
}
|
|
13120
|
+
const src = findSourceRepo();
|
|
13121
|
+
if (!src) {
|
|
13122
|
+
console.error(
|
|
13123
|
+
`${C4.red}\u2717${C4.reset} Could not locate the ZAM source checkout to update.`
|
|
13124
|
+
);
|
|
13125
|
+
process.exit(1);
|
|
13126
|
+
}
|
|
13127
|
+
const status = runGit(src, ["status", "--porcelain"], true);
|
|
13128
|
+
if (status.ok && status.out && !force) {
|
|
13129
|
+
console.error(
|
|
13130
|
+
`${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
|
|
13131
|
+
${status.out}
|
|
13132
|
+
Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
|
|
13133
|
+
);
|
|
13134
|
+
process.exit(1);
|
|
13135
|
+
}
|
|
13136
|
+
console.log(
|
|
13137
|
+
`${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
|
|
13138
|
+
);
|
|
13139
|
+
if (!runGit(src, ["pull", "--ff-only"], false).ok) {
|
|
13140
|
+
console.error(
|
|
13141
|
+
`${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
|
|
13142
|
+
);
|
|
13143
|
+
process.exit(1);
|
|
13144
|
+
}
|
|
13145
|
+
console.log(`${C4.dim}\u2192 npm install${C4.reset}`);
|
|
13146
|
+
if (runNpm2(["install"], src) !== 0) {
|
|
13147
|
+
console.error(`${C4.red}\u2717${C4.reset} npm install failed.`);
|
|
13148
|
+
process.exit(1);
|
|
13149
|
+
}
|
|
13150
|
+
console.log(`${C4.dim}\u2192 npm run build${C4.reset}`);
|
|
13151
|
+
if (runNpm2(["run", "build"], src) !== 0) {
|
|
13152
|
+
console.error(`${C4.red}\u2717${C4.reset} Build failed.`);
|
|
13153
|
+
process.exit(1);
|
|
13154
|
+
}
|
|
13155
|
+
console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
|
|
13156
|
+
const setup = spawnSync2(
|
|
13157
|
+
process.execPath,
|
|
13158
|
+
[join23(src, "dist", "cli", "index.js"), "setup", "--force"],
|
|
13159
|
+
{ cwd: process.cwd(), stdio: "inherit" }
|
|
13160
|
+
);
|
|
13161
|
+
if (setup.status !== 0) {
|
|
13162
|
+
console.warn(
|
|
13163
|
+
`${C4.yellow}\u26A0${C4.reset} Skill refresh reported a problem \u2014 run '${C4.cyan}zam setup --force${C4.reset}' here manually.`
|
|
13164
|
+
);
|
|
13165
|
+
}
|
|
13166
|
+
console.log(
|
|
13167
|
+
`
|
|
13168
|
+
${C4.green}\u2713${C4.reset} Updated to ${C4.cyan}${versionAt(src)}${C4.reset}. Restart your agent client (e.g. Claude Code) to load the refreshed ${C4.cyan}/zam${C4.reset} skill.`
|
|
13169
|
+
);
|
|
13170
|
+
}
|
|
13171
|
+
async function applyUpdate(opts) {
|
|
13172
|
+
try {
|
|
13173
|
+
const current = currentVersion();
|
|
13174
|
+
const latest = await fetchLatestVersion(GITHUB_REPO);
|
|
13175
|
+
const channel = getInstallChannel();
|
|
13176
|
+
const decision = decideUpdate({
|
|
13177
|
+
currentVersion: current,
|
|
13178
|
+
latestVersion: latest,
|
|
13179
|
+
channel
|
|
13180
|
+
});
|
|
13181
|
+
if (!decision.updateAvailable) {
|
|
13182
|
+
console.log(
|
|
13183
|
+
`${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${current}${C4.reset}).`
|
|
13184
|
+
);
|
|
13185
|
+
return;
|
|
13186
|
+
}
|
|
13187
|
+
console.log(
|
|
13188
|
+
`${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${current}${C4.reset} \u2192 ${C4.cyan}${latest}${C4.reset} ${C4.dim}(${channel} install)${C4.reset}`
|
|
13189
|
+
);
|
|
13190
|
+
for (const step of planUpdate(decision)) {
|
|
13191
|
+
console.log(` ${C4.dim}\u2022${C4.reset} ${step.label}`);
|
|
13192
|
+
}
|
|
13193
|
+
if (channel === "direct") {
|
|
13194
|
+
console.log(
|
|
13195
|
+
`
|
|
13196
|
+
${C4.dim}This install updates through the desktop app's signed updater \u2014 open ZAM Desktop to install ${latest}.${C4.reset}`
|
|
13197
|
+
);
|
|
13198
|
+
return;
|
|
13199
|
+
}
|
|
13200
|
+
if (!opts.yes) {
|
|
13201
|
+
const ok = await confirm3({
|
|
13202
|
+
message: "Apply this update?",
|
|
13203
|
+
default: true
|
|
13204
|
+
});
|
|
13205
|
+
if (!ok) {
|
|
13206
|
+
console.log("Aborted.");
|
|
13207
|
+
return;
|
|
13208
|
+
}
|
|
13209
|
+
}
|
|
13210
|
+
console.log();
|
|
13211
|
+
if (channel === "winget" || channel === "homebrew") {
|
|
13212
|
+
if (!decision.command || !runShell(decision.command)) process.exit(1);
|
|
13213
|
+
return;
|
|
13214
|
+
}
|
|
13215
|
+
applyDeveloperUpdate(opts.force ?? false);
|
|
13216
|
+
} catch (err) {
|
|
13217
|
+
console.error("Error:", err.message);
|
|
13218
|
+
process.exit(1);
|
|
13219
|
+
}
|
|
13220
|
+
}
|
|
13221
|
+
var updateCommand = new Command22("update").description(
|
|
13222
|
+
"Update ZAM to the latest release (use `update check` to only check)"
|
|
13223
|
+
).option("-y, --yes", "Apply without confirmation").option(
|
|
13224
|
+
"--force",
|
|
13225
|
+
"Update even if the source checkout has uncommitted changes"
|
|
13226
|
+
).action(async (opts) => {
|
|
13227
|
+
await applyUpdate(opts);
|
|
13228
|
+
}).addCommand(checkCmd);
|
|
13229
|
+
|
|
13230
|
+
// src/cli/commands/whoami.ts
|
|
13231
|
+
import { Command as Command23 } from "commander";
|
|
13232
|
+
var whoamiCommand = new Command23("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
|
|
13233
|
+
await withDb(async (db) => {
|
|
13234
|
+
if (opts.set) {
|
|
13235
|
+
await setSetting(db, "user.id", opts.set);
|
|
13236
|
+
if (opts.json) {
|
|
13237
|
+
console.log(JSON.stringify({ userId: opts.set }));
|
|
13238
|
+
} else {
|
|
13239
|
+
console.log(`Default user set to: ${opts.set}`);
|
|
13240
|
+
}
|
|
13241
|
+
return;
|
|
13242
|
+
}
|
|
13243
|
+
if (opts.clear) {
|
|
13244
|
+
const deleted = await deleteSetting(db, "user.id");
|
|
13245
|
+
if (opts.json) {
|
|
13246
|
+
console.log(JSON.stringify({ userId: null, cleared: deleted }));
|
|
13247
|
+
} else if (deleted) {
|
|
13248
|
+
console.log("Default user cleared.");
|
|
13249
|
+
} else {
|
|
13250
|
+
console.log("No default user was set.");
|
|
13251
|
+
}
|
|
13252
|
+
return;
|
|
13253
|
+
}
|
|
13254
|
+
const userId = await getSetting(db, "user.id");
|
|
13255
|
+
if (opts.json) {
|
|
13256
|
+
console.log(JSON.stringify({ userId: userId ?? null }));
|
|
13257
|
+
return;
|
|
13258
|
+
}
|
|
13259
|
+
if (userId) {
|
|
13260
|
+
console.log(userId);
|
|
13261
|
+
} else {
|
|
13262
|
+
console.log("No default user set. Use: zam whoami --set <id>");
|
|
13263
|
+
}
|
|
13264
|
+
});
|
|
13265
|
+
});
|
|
13266
|
+
|
|
13267
|
+
// src/cli/commands/workspace.ts
|
|
13268
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
13269
|
+
import { existsSync as existsSync25, writeFileSync as writeFileSync11 } from "fs";
|
|
13270
|
+
import { homedir as homedir13 } from "os";
|
|
13271
|
+
import { join as join24, resolve as resolve9 } from "path";
|
|
13272
|
+
import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
|
|
13273
|
+
import { Command as Command24 } from "commander";
|
|
13274
|
+
function runGit2(cwd, args) {
|
|
13275
|
+
try {
|
|
13276
|
+
return execFileSync4("git", args, {
|
|
13277
|
+
cwd,
|
|
13278
|
+
stdio: "pipe",
|
|
13279
|
+
encoding: "utf8"
|
|
13280
|
+
}).trim();
|
|
13281
|
+
} catch (err) {
|
|
13282
|
+
throw new Error(`Git command failed: ${err.message}`);
|
|
13283
|
+
}
|
|
12824
13284
|
}
|
|
12825
|
-
function
|
|
12826
|
-
|
|
12827
|
-
|
|
12828
|
-
|
|
12829
|
-
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
13285
|
+
function ghRepoCreateArgs(repoName, repoVisibility) {
|
|
13286
|
+
return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
|
|
13287
|
+
}
|
|
13288
|
+
function gitRemoteArgs(githubUrl, hasOrigin) {
|
|
13289
|
+
return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
|
|
13290
|
+
}
|
|
13291
|
+
var workspaceCommand = new Command24("workspace").description(
|
|
13292
|
+
"Manage your ZAM learning workspace"
|
|
13293
|
+
);
|
|
13294
|
+
var WORKSPACE_KINDS = [
|
|
13295
|
+
"personal",
|
|
13296
|
+
"team",
|
|
13297
|
+
"family",
|
|
13298
|
+
"community",
|
|
13299
|
+
"organization",
|
|
13300
|
+
"custom"
|
|
13301
|
+
];
|
|
13302
|
+
var WORKSPACE_SOURCE_CONTROLS = [
|
|
13303
|
+
"github",
|
|
13304
|
+
"azure-devops",
|
|
13305
|
+
"git",
|
|
13306
|
+
"none"
|
|
13307
|
+
];
|
|
13308
|
+
function parseWorkspaceKind(value) {
|
|
13309
|
+
const kind = (value ?? "custom").toLowerCase();
|
|
13310
|
+
if (WORKSPACE_KINDS.includes(kind)) {
|
|
13311
|
+
return kind;
|
|
12833
13312
|
}
|
|
13313
|
+
throw new Error(
|
|
13314
|
+
`Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
|
|
13315
|
+
);
|
|
12834
13316
|
}
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
}
|
|
13317
|
+
function parseWorkspaceSourceControl(value) {
|
|
13318
|
+
if (!value) return void 0;
|
|
13319
|
+
const source = value.toLowerCase();
|
|
13320
|
+
if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
|
|
13321
|
+
return source;
|
|
13322
|
+
}
|
|
13323
|
+
throw new Error(
|
|
13324
|
+
`Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
|
|
12844
13325
|
);
|
|
12845
|
-
|
|
13326
|
+
}
|
|
13327
|
+
function parseScopes(value) {
|
|
13328
|
+
if (!value) return void 0;
|
|
13329
|
+
const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
13330
|
+
return scopes.length > 0 ? scopes : void 0;
|
|
13331
|
+
}
|
|
13332
|
+
function requireWorkspace(id) {
|
|
13333
|
+
const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
|
|
13334
|
+
if (!workspace) {
|
|
12846
13335
|
throw new Error(
|
|
12847
|
-
`
|
|
13336
|
+
`Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
|
|
12848
13337
|
);
|
|
12849
13338
|
}
|
|
12850
|
-
|
|
12851
|
-
if (!data.tag_name) throw new Error("No published release found yet.");
|
|
12852
|
-
return data.tag_name;
|
|
13339
|
+
return workspace;
|
|
12853
13340
|
}
|
|
12854
|
-
function
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
13341
|
+
function formatLinkHealth(id, health) {
|
|
13342
|
+
switch (health) {
|
|
13343
|
+
case "healthy":
|
|
13344
|
+
return "ok";
|
|
13345
|
+
case "needs-repair":
|
|
13346
|
+
return `needs repair \u2014 run: zam workspace setup ${id}`;
|
|
13347
|
+
case "unmanaged":
|
|
13348
|
+
return `unmanaged skill dir \u2014 run: zam workspace setup ${id} --force`;
|
|
12860
13349
|
}
|
|
12861
|
-
|
|
12862
|
-
|
|
13350
|
+
}
|
|
13351
|
+
workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
|
|
13352
|
+
const workspaces = getConfiguredWorkspaces();
|
|
13353
|
+
const agents = parseSetupAgents();
|
|
13354
|
+
const linkHealth = Object.fromEntries(
|
|
13355
|
+
workspaces.filter((workspace) => existsSync25(workspace.path)).map((workspace) => [
|
|
13356
|
+
workspace.id,
|
|
13357
|
+
summarizeSkillLinkHealth(inspectSkillLinks(workspace.path, agents))
|
|
13358
|
+
])
|
|
12863
13359
|
);
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
}
|
|
13360
|
+
if (opts.json) {
|
|
13361
|
+
console.log(JSON.stringify({ workspaces, linkHealth }, null, 2));
|
|
13362
|
+
return;
|
|
13363
|
+
}
|
|
13364
|
+
console.log("Configured ZAM workspaces:\n");
|
|
13365
|
+
if (workspaces.length === 0) {
|
|
12868
13366
|
console.log(
|
|
12869
|
-
|
|
13367
|
+
" (none) \u2014 add one: zam workspace add personal --path <dir>"
|
|
12870
13368
|
);
|
|
13369
|
+
return;
|
|
12871
13370
|
}
|
|
12872
|
-
|
|
12873
|
-
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
13371
|
+
for (const workspace of workspaces) {
|
|
13372
|
+
const label = workspace.label ? ` (${workspace.label})` : "";
|
|
13373
|
+
console.log(` ${workspace.id}${label}`);
|
|
13374
|
+
console.log(` kind: ${workspace.kind}`);
|
|
13375
|
+
console.log(` path: ${workspace.path}`);
|
|
13376
|
+
if (workspace.sourceControl) {
|
|
13377
|
+
console.log(` source: ${workspace.sourceControl}`);
|
|
13378
|
+
}
|
|
13379
|
+
if (workspace.knowledgeScopes?.length) {
|
|
13380
|
+
console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
|
|
13381
|
+
}
|
|
13382
|
+
const health = linkHealth[workspace.id];
|
|
13383
|
+
if (health) {
|
|
13384
|
+
console.log(` links: ${formatLinkHealth(workspace.id, health)}`);
|
|
13385
|
+
}
|
|
13386
|
+
}
|
|
13387
|
+
});
|
|
13388
|
+
workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
|
|
13389
|
+
"--kind <kind>",
|
|
13390
|
+
`Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
|
|
13391
|
+
"custom"
|
|
13392
|
+
).option("--label <label>", "Human-readable label").option(
|
|
13393
|
+
"--source-control <provider>",
|
|
13394
|
+
`Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
|
|
13395
|
+
).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
|
|
13396
|
+
try {
|
|
13397
|
+
const path = resolve9(String(opts.path));
|
|
13398
|
+
if (!existsSync25(path)) {
|
|
13399
|
+
console.error(`Workspace path does not exist: ${path}`);
|
|
12900
13400
|
process.exit(1);
|
|
12901
13401
|
}
|
|
13402
|
+
const workspace = {
|
|
13403
|
+
id,
|
|
13404
|
+
kind: parseWorkspaceKind(opts.kind),
|
|
13405
|
+
path,
|
|
13406
|
+
...opts.label ? { label: opts.label } : {},
|
|
13407
|
+
...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
|
|
13408
|
+
...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
|
|
13409
|
+
...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
|
|
13410
|
+
};
|
|
13411
|
+
wireSkills(path, parseSetupAgents());
|
|
13412
|
+
upsertConfiguredWorkspace(workspace);
|
|
13413
|
+
console.log(`Registered and linked workspace "${id}" at ${path}.`);
|
|
13414
|
+
} catch (err) {
|
|
13415
|
+
console.error(`Error: ${err.message}`);
|
|
13416
|
+
process.exit(1);
|
|
12902
13417
|
}
|
|
12903
|
-
);
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
dir = parent;
|
|
12910
|
-
parent = dirname9(dir);
|
|
13418
|
+
});
|
|
13419
|
+
workspaceCommand.command("remove <id>").description("Unregister a ZAM workspace without deleting its files").action((id) => {
|
|
13420
|
+
const existing = getConfiguredWorkspaces().find((item) => item.id === id);
|
|
13421
|
+
if (!existing) {
|
|
13422
|
+
console.error(`Workspace "${id}" is not configured.`);
|
|
13423
|
+
process.exit(1);
|
|
12911
13424
|
}
|
|
12912
|
-
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
}
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
13425
|
+
removeConfiguredWorkspace(id);
|
|
13426
|
+
console.log(
|
|
13427
|
+
`Unregistered workspace "${id}". Files in ${existing.path} were not changed.`
|
|
13428
|
+
);
|
|
13429
|
+
});
|
|
13430
|
+
workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
|
|
13431
|
+
"--agents <list>",
|
|
13432
|
+
"comma-separated agents to wire: all, claude, copilot, codex, agent"
|
|
13433
|
+
).option(
|
|
13434
|
+
"--force",
|
|
13435
|
+
"overwrite existing skill files and refresh ZAM blocks",
|
|
13436
|
+
false
|
|
13437
|
+
).option(
|
|
13438
|
+
"--dry-run",
|
|
13439
|
+
"show what would be written without changing files",
|
|
13440
|
+
false
|
|
13441
|
+
).action((id, opts) => {
|
|
13442
|
+
try {
|
|
13443
|
+
const workspace = requireWorkspace(id);
|
|
13444
|
+
const agents = parseSetupAgents(opts.agents);
|
|
13445
|
+
console.log(
|
|
13446
|
+
`Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
|
|
13447
|
+
`
|
|
13448
|
+
);
|
|
13449
|
+
wireSkills(workspace.path, agents, {
|
|
13450
|
+
force: Boolean(opts.force),
|
|
13451
|
+
dryRun: Boolean(opts.dryRun)
|
|
13452
|
+
});
|
|
13453
|
+
if (agents.has("claude")) {
|
|
13454
|
+
writeClaudeMd(false, workspace.path, {
|
|
13455
|
+
dryRun: Boolean(opts.dryRun),
|
|
13456
|
+
updateExisting: true
|
|
13457
|
+
});
|
|
13458
|
+
}
|
|
13459
|
+
if (agents.has("codex") || agents.has("agent")) {
|
|
13460
|
+
writeAgentsMd(false, workspace.path, {
|
|
13461
|
+
dryRun: Boolean(opts.dryRun),
|
|
13462
|
+
updateExisting: true
|
|
13463
|
+
});
|
|
13464
|
+
}
|
|
13465
|
+
if (agents.has("copilot")) {
|
|
13466
|
+
writeCopilotInstructions(workspace.path, {
|
|
13467
|
+
dryRun: Boolean(opts.dryRun),
|
|
13468
|
+
updateExisting: true
|
|
13469
|
+
});
|
|
13470
|
+
}
|
|
13471
|
+
} catch (err) {
|
|
13472
|
+
console.error(`Error: ${err.message}`);
|
|
12937
13473
|
process.exit(1);
|
|
12938
13474
|
}
|
|
12939
|
-
|
|
12940
|
-
|
|
13475
|
+
});
|
|
13476
|
+
workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
|
|
13477
|
+
let db;
|
|
13478
|
+
let workspaceDir = "";
|
|
13479
|
+
try {
|
|
13480
|
+
db = await openDatabase();
|
|
13481
|
+
workspaceDir = (await ensureActiveWorkspace(db)).path;
|
|
13482
|
+
await db.close();
|
|
13483
|
+
} catch {
|
|
13484
|
+
await db?.close();
|
|
13485
|
+
}
|
|
13486
|
+
if (!workspaceDir) {
|
|
12941
13487
|
console.error(
|
|
12942
|
-
|
|
13488
|
+
"\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
|
|
12943
13489
|
);
|
|
12944
13490
|
process.exit(1);
|
|
12945
13491
|
}
|
|
12946
|
-
|
|
12947
|
-
if (status.ok && status.out && !force) {
|
|
13492
|
+
if (!existsSync25(workspaceDir)) {
|
|
12948
13493
|
console.error(
|
|
12949
|
-
|
|
12950
|
-
${status.out}
|
|
12951
|
-
Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
|
|
13494
|
+
`\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
|
|
12952
13495
|
);
|
|
12953
13496
|
process.exit(1);
|
|
12954
13497
|
}
|
|
12955
|
-
console.log(
|
|
12956
|
-
|
|
12957
|
-
)
|
|
12958
|
-
if (!runGit2(src, ["pull", "--ff-only"], false).ok) {
|
|
13498
|
+
console.log(`
|
|
13499
|
+
Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
|
|
13500
|
+
if (!hasCommand("git")) {
|
|
12959
13501
|
console.error(
|
|
12960
|
-
|
|
13502
|
+
"\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
|
|
12961
13503
|
);
|
|
12962
13504
|
process.exit(1);
|
|
12963
13505
|
}
|
|
12964
|
-
|
|
12965
|
-
if (
|
|
12966
|
-
|
|
12967
|
-
|
|
13506
|
+
const gitignorePath = join24(workspaceDir, ".gitignore");
|
|
13507
|
+
if (!existsSync25(gitignorePath)) {
|
|
13508
|
+
writeFileSync11(
|
|
13509
|
+
gitignorePath,
|
|
13510
|
+
"node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
|
|
13511
|
+
"utf8"
|
|
13512
|
+
);
|
|
12968
13513
|
}
|
|
12969
|
-
|
|
12970
|
-
if (
|
|
12971
|
-
console.
|
|
12972
|
-
|
|
13514
|
+
const hasGitRepo = existsSync25(join24(workspaceDir, ".git"));
|
|
13515
|
+
if (!hasGitRepo) {
|
|
13516
|
+
console.log("Initializing local Git repository...");
|
|
13517
|
+
runGit2(workspaceDir, ["init", "-b", "main"]);
|
|
13518
|
+
runGit2(workspaceDir, ["add", "."]);
|
|
13519
|
+
runGit2(workspaceDir, [
|
|
13520
|
+
"commit",
|
|
13521
|
+
"-m",
|
|
13522
|
+
"chore: initial workspace sandbox bootstrap"
|
|
13523
|
+
]);
|
|
13524
|
+
console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
|
|
13525
|
+
} else {
|
|
13526
|
+
console.log("Git repository is already initialized.");
|
|
12973
13527
|
}
|
|
12974
|
-
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
|
|
12978
|
-
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
13528
|
+
const repoName = await input7({
|
|
13529
|
+
message: "Choose a name for your GitHub repository:",
|
|
13530
|
+
default: "zam-personal"
|
|
13531
|
+
});
|
|
13532
|
+
const isPrivate = await confirm4({
|
|
13533
|
+
message: "Should the repository be private?",
|
|
13534
|
+
default: true
|
|
13535
|
+
});
|
|
13536
|
+
const repoVisibility = isPrivate ? "--private" : "--public";
|
|
13537
|
+
if (hasCommand("gh")) {
|
|
13538
|
+
console.log("GitHub CLI detected! Automating repository creation...");
|
|
13539
|
+
const proceedGh = await confirm4({
|
|
13540
|
+
message: "Would you like ZAM to create the repository using the GitHub CLI?",
|
|
13541
|
+
default: true
|
|
13542
|
+
});
|
|
13543
|
+
if (proceedGh) {
|
|
13544
|
+
try {
|
|
13545
|
+
console.log(`Creating GitHub repository ${repoName}...`);
|
|
13546
|
+
execFileSync4("gh", ghRepoCreateArgs(repoName, repoVisibility), {
|
|
13547
|
+
cwd: workspaceDir,
|
|
13548
|
+
stdio: "inherit"
|
|
13549
|
+
});
|
|
13550
|
+
console.log(
|
|
13551
|
+
"\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
|
|
13552
|
+
);
|
|
13553
|
+
process.exit(0);
|
|
13554
|
+
} catch (err) {
|
|
13555
|
+
console.warn(
|
|
13556
|
+
`\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
|
|
13557
|
+
);
|
|
13558
|
+
}
|
|
13559
|
+
}
|
|
12984
13560
|
}
|
|
12985
13561
|
console.log(
|
|
12986
|
-
|
|
12987
|
-
${C4.green}\u2713${C4.reset} Updated to ${C4.cyan}${versionAt(src)}${C4.reset}. Restart your agent client (e.g. Claude Code) to load the refreshed ${C4.cyan}/zam${C4.reset} skill.`
|
|
13562
|
+
"\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
|
|
12988
13563
|
);
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13564
|
+
console.log(" 1. Go to https://github.com/new");
|
|
13565
|
+
console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
|
|
13566
|
+
console.log(
|
|
13567
|
+
` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
|
|
13568
|
+
);
|
|
13569
|
+
console.log(
|
|
13570
|
+
" 4. Do NOT initialize it with README, .gitignore, or license"
|
|
13571
|
+
);
|
|
13572
|
+
console.log(" 5. Click 'Create repository'\n");
|
|
13573
|
+
const githubUrl = await input7({
|
|
13574
|
+
message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
|
|
13575
|
+
});
|
|
13576
|
+
if (githubUrl) {
|
|
13577
|
+
try {
|
|
13578
|
+
console.log("Linking remote repository and pushing...");
|
|
13579
|
+
let hasOrigin = false;
|
|
13580
|
+
try {
|
|
13581
|
+
runGit2(workspaceDir, ["remote", "get-url", "origin"]);
|
|
13582
|
+
hasOrigin = true;
|
|
13583
|
+
} catch {
|
|
13584
|
+
}
|
|
13585
|
+
runGit2(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
|
|
13586
|
+
runGit2(workspaceDir, ["push", "-u", "origin", "main"]);
|
|
13001
13587
|
console.log(
|
|
13002
|
-
|
|
13588
|
+
"\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
|
|
13589
|
+
);
|
|
13590
|
+
} catch (err) {
|
|
13591
|
+
console.error(
|
|
13592
|
+
`\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
|
|
13003
13593
|
);
|
|
13004
|
-
return;
|
|
13005
|
-
}
|
|
13006
|
-
console.log(
|
|
13007
|
-
`${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${current}${C4.reset} \u2192 ${C4.cyan}${latest}${C4.reset} ${C4.dim}(${channel} install)${C4.reset}`
|
|
13008
|
-
);
|
|
13009
|
-
for (const step of planUpdate(decision)) {
|
|
13010
|
-
console.log(` ${C4.dim}\u2022${C4.reset} ${step.label}`);
|
|
13011
|
-
}
|
|
13012
|
-
if (channel === "direct") {
|
|
13013
13594
|
console.log(
|
|
13014
|
-
|
|
13015
|
-
${C4.dim}This install updates through the desktop app's signed updater \u2014 open ZAM Desktop to install ${latest}.${C4.reset}`
|
|
13595
|
+
"You can push manually later using: git push -u origin main"
|
|
13016
13596
|
);
|
|
13017
|
-
return;
|
|
13018
|
-
}
|
|
13019
|
-
if (!opts.yes) {
|
|
13020
|
-
const ok = await confirm4({
|
|
13021
|
-
message: "Apply this update?",
|
|
13022
|
-
default: true
|
|
13023
|
-
});
|
|
13024
|
-
if (!ok) {
|
|
13025
|
-
console.log("Aborted.");
|
|
13026
|
-
return;
|
|
13027
|
-
}
|
|
13028
13597
|
}
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
13598
|
+
}
|
|
13599
|
+
});
|
|
13600
|
+
workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
|
|
13601
|
+
const dir = join24(homedir13(), ".zam");
|
|
13602
|
+
console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
|
|
13603
|
+
});
|
|
13604
|
+
workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
|
|
13605
|
+
"--dir <path>",
|
|
13606
|
+
"Target directory (default: workspace dir, else ~/Documents/zam)"
|
|
13607
|
+
).option("--json", "Output as JSON").action(async (opts) => {
|
|
13608
|
+
const target = getDatabaseTargetInfo();
|
|
13609
|
+
if (target.kind !== "local") {
|
|
13610
|
+
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.`;
|
|
13611
|
+
if (opts.json) {
|
|
13612
|
+
console.log(JSON.stringify({ ok: false, reason }));
|
|
13032
13613
|
return;
|
|
13033
13614
|
}
|
|
13034
|
-
|
|
13035
|
-
} catch (err) {
|
|
13036
|
-
console.error("Error:", err.message);
|
|
13615
|
+
console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
|
|
13037
13616
|
process.exit(1);
|
|
13038
13617
|
}
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
"Update even if the source checkout has uncommitted changes"
|
|
13045
|
-
).action(async (opts) => {
|
|
13046
|
-
await applyUpdate(opts);
|
|
13047
|
-
}).addCommand(checkCmd);
|
|
13048
|
-
|
|
13049
|
-
// src/cli/commands/whoami.ts
|
|
13050
|
-
import { Command as Command24 } from "commander";
|
|
13051
|
-
var whoamiCommand = new Command24("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
|
|
13052
|
-
await withDb(async (db) => {
|
|
13053
|
-
if (opts.set) {
|
|
13054
|
-
await setSetting(db, "user.id", opts.set);
|
|
13055
|
-
if (opts.json) {
|
|
13056
|
-
console.log(JSON.stringify({ userId: opts.set }));
|
|
13057
|
-
} else {
|
|
13058
|
-
console.log(`Default user set to: ${opts.set}`);
|
|
13059
|
-
}
|
|
13060
|
-
return;
|
|
13061
|
-
}
|
|
13062
|
-
if (opts.clear) {
|
|
13063
|
-
const deleted = await deleteSetting(db, "user.id");
|
|
13064
|
-
if (opts.json) {
|
|
13065
|
-
console.log(JSON.stringify({ userId: null, cleared: deleted }));
|
|
13066
|
-
} else if (deleted) {
|
|
13067
|
-
console.log("Default user cleared.");
|
|
13068
|
-
} else {
|
|
13069
|
-
console.log("No default user was set.");
|
|
13070
|
-
}
|
|
13071
|
-
return;
|
|
13072
|
-
}
|
|
13073
|
-
const userId = await getSetting(db, "user.id");
|
|
13618
|
+
let db;
|
|
13619
|
+
try {
|
|
13620
|
+
db = await openDatabase();
|
|
13621
|
+
const workspaceDir = opts.dir || (await ensureActiveWorkspace(db)).path;
|
|
13622
|
+
const dest = await backupDatabaseTo(db, workspaceDir);
|
|
13074
13623
|
if (opts.json) {
|
|
13075
|
-
console.log(JSON.stringify({
|
|
13076
|
-
|
|
13624
|
+
console.log(JSON.stringify({ ok: true, path: dest }));
|
|
13625
|
+
} else {
|
|
13626
|
+
console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
|
|
13077
13627
|
}
|
|
13078
|
-
|
|
13079
|
-
|
|
13628
|
+
} catch (err) {
|
|
13629
|
+
const reason = err.message;
|
|
13630
|
+
if (opts.json) {
|
|
13631
|
+
console.log(JSON.stringify({ ok: false, reason }));
|
|
13080
13632
|
} else {
|
|
13081
|
-
console.
|
|
13633
|
+
console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
|
|
13082
13634
|
}
|
|
13083
|
-
|
|
13635
|
+
process.exit(1);
|
|
13636
|
+
} finally {
|
|
13637
|
+
await db?.close();
|
|
13638
|
+
}
|
|
13084
13639
|
});
|
|
13085
13640
|
|
|
13086
13641
|
// src/cli/index.ts
|
|
13087
13642
|
var __dirname = dirname10(fileURLToPath5(import.meta.url));
|
|
13088
13643
|
var pkg = JSON.parse(
|
|
13089
|
-
readFileSync15(
|
|
13644
|
+
readFileSync15(join25(__dirname, "..", "..", "package.json"), "utf-8")
|
|
13090
13645
|
);
|
|
13091
13646
|
var program = new Command25();
|
|
13092
13647
|
program.name("zam").description(
|