zam-core 0.4.3 → 0.5.0
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/.agent/skills/zam/SKILL.md +9 -6
- package/.agents/skills/zam/SKILL.md +9 -6
- package/.claude/skills/zam/SKILL.md +9 -6
- package/dist/cli/index.js +2489 -909
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +56 -1
- package/dist/index.js +79 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { dirname as dirname10, join as
|
|
4
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
5
|
+
import { dirname as dirname10, join as join24 } from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
7
|
-
import { Command as
|
|
7
|
+
import { Command as Command25 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/cli/commands/agent.ts
|
|
10
|
-
import { existsSync as
|
|
11
|
-
import { join as
|
|
10
|
+
import { existsSync as existsSync13 } from "fs";
|
|
11
|
+
import { join as join13 } from "path";
|
|
12
12
|
import { Command } from "commander";
|
|
13
13
|
|
|
14
14
|
// src/kernel/analytics/stats.ts
|
|
@@ -203,6 +203,25 @@ function clearADOCredentials(path) {
|
|
|
203
203
|
delete creds.ado;
|
|
204
204
|
saveCredentials(creds, path);
|
|
205
205
|
}
|
|
206
|
+
function getProviderApiKey(name, path) {
|
|
207
|
+
const key = loadCredentials(path).llmProviders?.[name]?.apiKey;
|
|
208
|
+
return key && key.length > 0 ? key : null;
|
|
209
|
+
}
|
|
210
|
+
function setProviderApiKey(name, apiKey, path) {
|
|
211
|
+
const creds = loadCredentials(path);
|
|
212
|
+
creds.llmProviders = { ...creds.llmProviders, [name]: { apiKey } };
|
|
213
|
+
saveCredentials(creds, path);
|
|
214
|
+
}
|
|
215
|
+
function clearProviderApiKey(name, path) {
|
|
216
|
+
const creds = loadCredentials(path);
|
|
217
|
+
if (creds.llmProviders && name in creds.llmProviders) {
|
|
218
|
+
delete creds.llmProviders[name];
|
|
219
|
+
saveCredentials(creds, path);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function listProviderApiKeyRefs(path) {
|
|
223
|
+
return Object.keys(loadCredentials(path).llmProviders ?? {});
|
|
224
|
+
}
|
|
206
225
|
|
|
207
226
|
// src/kernel/connectors/azure-devops.ts
|
|
208
227
|
function loadADOConfig() {
|
|
@@ -4403,8 +4422,10 @@ function t(locale, key, params = {}) {
|
|
|
4403
4422
|
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
4404
4423
|
import { homedir as homedir6 } from "os";
|
|
4405
4424
|
import { dirname as dirname4, join as join9 } from "path";
|
|
4406
|
-
|
|
4407
|
-
|
|
4425
|
+
function defaultConfigPath() {
|
|
4426
|
+
return process.env.ZAM_CONFIG_PATH || join9(homedir6(), ".zam", "config.json");
|
|
4427
|
+
}
|
|
4428
|
+
function loadInstallConfig(path = defaultConfigPath()) {
|
|
4408
4429
|
if (!existsSync8(path)) return {};
|
|
4409
4430
|
try {
|
|
4410
4431
|
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
@@ -4412,25 +4433,50 @@ function loadInstallConfig(path = DEFAULT_CONFIG_PATH) {
|
|
|
4412
4433
|
return {};
|
|
4413
4434
|
}
|
|
4414
4435
|
}
|
|
4415
|
-
function saveInstallConfig(config, path =
|
|
4436
|
+
function saveInstallConfig(config, path = defaultConfigPath()) {
|
|
4416
4437
|
const dir = dirname4(path);
|
|
4417
4438
|
if (!existsSync8(dir)) mkdirSync7(dir, { recursive: true });
|
|
4418
4439
|
writeFileSync5(path, `${JSON.stringify(config, null, 2)}
|
|
4419
4440
|
`, "utf-8");
|
|
4420
4441
|
}
|
|
4421
|
-
function getInstallMode(path =
|
|
4442
|
+
function getInstallMode(path = defaultConfigPath()) {
|
|
4422
4443
|
return loadInstallConfig(path).mode ?? "developer";
|
|
4423
4444
|
}
|
|
4424
|
-
function setInstallMode(mode, path =
|
|
4445
|
+
function setInstallMode(mode, path = defaultConfigPath()) {
|
|
4425
4446
|
const config = loadInstallConfig(path);
|
|
4426
4447
|
config.mode = mode;
|
|
4427
4448
|
saveInstallConfig(config, path);
|
|
4428
4449
|
}
|
|
4429
|
-
function getInstallChannel(path =
|
|
4450
|
+
function getInstallChannel(path = defaultConfigPath()) {
|
|
4430
4451
|
const config = loadInstallConfig(path);
|
|
4431
4452
|
if (config.channel) return config.channel;
|
|
4432
4453
|
return (config.mode ?? "developer") === "developer" ? "developer" : "direct";
|
|
4433
4454
|
}
|
|
4455
|
+
function getMachineAiConfig(path = defaultConfigPath()) {
|
|
4456
|
+
return loadInstallConfig(path).ai ?? {};
|
|
4457
|
+
}
|
|
4458
|
+
function saveMachineAiConfig(ai, path = defaultConfigPath()) {
|
|
4459
|
+
const config = loadInstallConfig(path);
|
|
4460
|
+
config.ai = ai;
|
|
4461
|
+
saveInstallConfig(config, path);
|
|
4462
|
+
}
|
|
4463
|
+
function getConfiguredWorkspaces(path = defaultConfigPath()) {
|
|
4464
|
+
return loadInstallConfig(path).workspaces ?? [];
|
|
4465
|
+
}
|
|
4466
|
+
function saveConfiguredWorkspaces(workspaces, path = defaultConfigPath()) {
|
|
4467
|
+
const config = loadInstallConfig(path);
|
|
4468
|
+
config.workspaces = workspaces;
|
|
4469
|
+
saveInstallConfig(config, path);
|
|
4470
|
+
}
|
|
4471
|
+
function upsertConfiguredWorkspace(workspace, path = defaultConfigPath()) {
|
|
4472
|
+
const current = getConfiguredWorkspaces(path);
|
|
4473
|
+
const next = [
|
|
4474
|
+
...current.filter((candidate) => candidate.id !== workspace.id),
|
|
4475
|
+
workspace
|
|
4476
|
+
];
|
|
4477
|
+
saveConfiguredWorkspaces(next, path);
|
|
4478
|
+
return next;
|
|
4479
|
+
}
|
|
4434
4480
|
function detectSyncProvider(dir) {
|
|
4435
4481
|
const p = dir.toLowerCase();
|
|
4436
4482
|
if (p.includes("onedrive")) return "OneDrive";
|
|
@@ -4707,13 +4753,11 @@ function runCommand(cmd) {
|
|
|
4707
4753
|
return "";
|
|
4708
4754
|
}
|
|
4709
4755
|
}
|
|
4710
|
-
function
|
|
4756
|
+
function detectWindowsNPU() {
|
|
4711
4757
|
if (process.platform !== "win32") return false;
|
|
4712
|
-
const cmd = `powershell -NoProfile -Command "Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -like '*AMD IPU*' -or $_.Name -like '*AMD NPU*' -or $_.Name -like '*NPU
|
|
4758
|
+
const cmd = `powershell -NoProfile -Command "Get-CimInstance Win32_PnPEntity | Where-Object { $_.PNPClass -eq 'ComputeAccelerator' -or $_.Name -like '*AMD IPU*' -or $_.Name -like '*AMD NPU*' -or $_.Name -like '*Qualcomm*NPU*' -or $_.Name -like '*Hexagon*NPU*' -or $_.Name -like '*Intel*AI Boost*' -or $_.Name -like '*NPU Compute*' -or $_.Name -like '*Ryzen AI*' } | Select-Object -First 1 -ExpandProperty Name"`;
|
|
4713
4759
|
const output = runCommand(cmd);
|
|
4714
|
-
return Boolean(
|
|
4715
|
-
output && (output.toLowerCase().includes("amd") || output.toLowerCase().includes("ipu") || output.toLowerCase().includes("npu") || output.toLowerCase().includes("ryzen"))
|
|
4716
|
-
);
|
|
4760
|
+
return Boolean(output && output.length > 0);
|
|
4717
4761
|
}
|
|
4718
4762
|
function getSystemProfile() {
|
|
4719
4763
|
const platform = process.platform;
|
|
@@ -4725,13 +4769,21 @@ function getSystemProfile() {
|
|
|
4725
4769
|
let arch = "unknown";
|
|
4726
4770
|
if (archStr === "x64") arch = "x64";
|
|
4727
4771
|
else if (archStr === "arm64") arch = "arm64";
|
|
4728
|
-
const
|
|
4772
|
+
const hasNpu = os === "windows" && detectWindowsNPU();
|
|
4729
4773
|
const hasAppleSilicon = os === "macos" && arch === "arm64";
|
|
4730
4774
|
let recommendedRunner = "generic";
|
|
4731
4775
|
let recommendedModel = "qwen3.5:4b";
|
|
4732
|
-
if (
|
|
4733
|
-
|
|
4734
|
-
|
|
4776
|
+
if (hasNpu) {
|
|
4777
|
+
const isQualcomm = runCommand(
|
|
4778
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -like '*Qualcomm*' } | Select-Object -First 1"`
|
|
4779
|
+
).length > 0;
|
|
4780
|
+
if (isQualcomm) {
|
|
4781
|
+
recommendedRunner = "generic";
|
|
4782
|
+
recommendedModel = "qwen3.5-4b";
|
|
4783
|
+
} else {
|
|
4784
|
+
recommendedRunner = "fastflowlm";
|
|
4785
|
+
recommendedModel = "qwen3.5:4b";
|
|
4786
|
+
}
|
|
4735
4787
|
} else if (hasAppleSilicon) {
|
|
4736
4788
|
recommendedRunner = "ollama";
|
|
4737
4789
|
recommendedModel = "llama3.2:3b";
|
|
@@ -4742,7 +4794,8 @@ function getSystemProfile() {
|
|
|
4742
4794
|
return {
|
|
4743
4795
|
os,
|
|
4744
4796
|
arch,
|
|
4745
|
-
hasRyzenNPU,
|
|
4797
|
+
hasRyzenNPU: hasNpu && !archStr.includes("arm64"),
|
|
4798
|
+
// backwards compatibility flag
|
|
4746
4799
|
hasAppleSilicon,
|
|
4747
4800
|
recommendedRunner,
|
|
4748
4801
|
recommendedModel
|
|
@@ -4879,6 +4932,304 @@ function planUpdate(decision) {
|
|
|
4879
4932
|
}
|
|
4880
4933
|
}
|
|
4881
4934
|
|
|
4935
|
+
// src/cli/agent-harness.ts
|
|
4936
|
+
import { spawn } from "child_process";
|
|
4937
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4938
|
+
import { homedir as homedir8 } from "os";
|
|
4939
|
+
import { join as join12 } from "path";
|
|
4940
|
+
|
|
4941
|
+
// src/cli/terminal-open.ts
|
|
4942
|
+
import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
|
|
4943
|
+
import {
|
|
4944
|
+
accessSync,
|
|
4945
|
+
constants,
|
|
4946
|
+
existsSync as existsSync11,
|
|
4947
|
+
unlinkSync,
|
|
4948
|
+
writeFileSync as writeFileSync6
|
|
4949
|
+
} from "fs";
|
|
4950
|
+
import { tmpdir } from "os";
|
|
4951
|
+
import { basename as basename2, delimiter, extname, isAbsolute, join as join11 } from "path";
|
|
4952
|
+
function isPowerShellShell(shell) {
|
|
4953
|
+
return shell === "pwsh" || shell === "powershell";
|
|
4954
|
+
}
|
|
4955
|
+
function psSingleQuoted2(value) {
|
|
4956
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
4957
|
+
}
|
|
4958
|
+
function detectShell() {
|
|
4959
|
+
if (process.platform === "win32")
|
|
4960
|
+
return findExecutable("pwsh.exe") ? "pwsh" : "powershell";
|
|
4961
|
+
const shell = process.env.SHELL ?? "";
|
|
4962
|
+
return basename2(shell) === "bash" ? "bash" : "zsh";
|
|
4963
|
+
}
|
|
4964
|
+
function normalizeShell(shell) {
|
|
4965
|
+
if (!shell) return detectShell();
|
|
4966
|
+
const normalized = shell.toLowerCase();
|
|
4967
|
+
if (normalized === "zsh" || normalized === "bash" || normalized === "pwsh" || normalized === "powershell") {
|
|
4968
|
+
return normalized;
|
|
4969
|
+
}
|
|
4970
|
+
throw new Error(
|
|
4971
|
+
`Unsupported shell: ${shell}. Expected zsh, bash, pwsh, or powershell.`
|
|
4972
|
+
);
|
|
4973
|
+
}
|
|
4974
|
+
function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
|
|
4975
|
+
if (results.length === 0) return null;
|
|
4976
|
+
const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
|
|
4977
|
+
const runnable = results.find(
|
|
4978
|
+
(result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
|
|
4979
|
+
);
|
|
4980
|
+
return runnable ?? results[0];
|
|
4981
|
+
}
|
|
4982
|
+
function stripSurroundingQuotes(command) {
|
|
4983
|
+
const trimmed = command.trim();
|
|
4984
|
+
if (trimmed.length < 2) return trimmed;
|
|
4985
|
+
const first = trimmed[0];
|
|
4986
|
+
const last = trimmed[trimmed.length - 1];
|
|
4987
|
+
if (first === '"' && last === '"' || first === "'" && last === "'") {
|
|
4988
|
+
return trimmed.slice(1, -1);
|
|
4989
|
+
}
|
|
4990
|
+
return trimmed;
|
|
4991
|
+
}
|
|
4992
|
+
function executableExists(path) {
|
|
4993
|
+
if (!existsSync11(path)) return false;
|
|
4994
|
+
if (process.platform === "win32") return true;
|
|
4995
|
+
try {
|
|
4996
|
+
accessSync(path, constants.X_OK);
|
|
4997
|
+
return true;
|
|
4998
|
+
} catch {
|
|
4999
|
+
return false;
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
function windowsExecutableNames(command) {
|
|
5003
|
+
if (process.platform !== "win32") return [command];
|
|
5004
|
+
if (extname(command)) return [command];
|
|
5005
|
+
const extensions = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => ext.trim()).filter(Boolean);
|
|
5006
|
+
return extensions.map((ext) => `${command}${ext}`);
|
|
5007
|
+
}
|
|
5008
|
+
function hasDirectoryPart(command) {
|
|
5009
|
+
return isAbsolute(command) || command.includes("/") || command.includes("\\");
|
|
5010
|
+
}
|
|
5011
|
+
function findExecutable(command) {
|
|
5012
|
+
const normalized = stripSurroundingQuotes(command);
|
|
5013
|
+
if (!normalized) return null;
|
|
5014
|
+
const matches = [];
|
|
5015
|
+
if (hasDirectoryPart(normalized)) {
|
|
5016
|
+
for (const candidate of windowsExecutableNames(normalized)) {
|
|
5017
|
+
if (executableExists(candidate)) matches.push(candidate);
|
|
5018
|
+
}
|
|
5019
|
+
return process.platform === "win32" ? selectWindowsExecutable(matches) : matches[0] ?? null;
|
|
5020
|
+
}
|
|
5021
|
+
const pathEntries = (process.env.PATH ?? "").split(delimiter).map((entry) => entry.trim()).filter(Boolean);
|
|
5022
|
+
for (const entry of pathEntries) {
|
|
5023
|
+
for (const name of windowsExecutableNames(normalized)) {
|
|
5024
|
+
const candidate = join11(entry, name);
|
|
5025
|
+
if (executableExists(candidate)) matches.push(candidate);
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
return process.platform === "win32" ? selectWindowsExecutable(matches) : matches[0] ?? null;
|
|
5029
|
+
}
|
|
5030
|
+
function resolveZamInvocation(shell) {
|
|
5031
|
+
const installed = findExecutable("zam");
|
|
5032
|
+
if (installed) {
|
|
5033
|
+
return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
|
|
5034
|
+
}
|
|
5035
|
+
const projectRoot = join11(import.meta.dirname, "..", "..");
|
|
5036
|
+
const cliSource = join11(projectRoot, "src/cli/index.ts");
|
|
5037
|
+
if (isPowerShellShell(shell)) {
|
|
5038
|
+
return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
|
|
5039
|
+
}
|
|
5040
|
+
return `npx --prefix ${JSON.stringify(projectRoot)} tsx ${JSON.stringify(cliSource)}`;
|
|
5041
|
+
}
|
|
5042
|
+
function buildShellSetupCommand(dir, shell, command) {
|
|
5043
|
+
if (isPowerShellShell(shell)) {
|
|
5044
|
+
return [`Set-Location -LiteralPath ${psSingleQuoted2(dir)}`, command].join(
|
|
5045
|
+
"; "
|
|
5046
|
+
);
|
|
5047
|
+
}
|
|
5048
|
+
return `cd ${JSON.stringify(dir)} && ${command}`;
|
|
5049
|
+
}
|
|
5050
|
+
function isItermRunning() {
|
|
5051
|
+
try {
|
|
5052
|
+
const result = execSync4(
|
|
5053
|
+
`osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"' 2>/dev/null`,
|
|
5054
|
+
{ encoding: "utf-8" }
|
|
5055
|
+
).trim();
|
|
5056
|
+
return result === "true";
|
|
5057
|
+
} catch {
|
|
5058
|
+
return false;
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
function openMacTerminal(shellSetup, label, dir, silent) {
|
|
5062
|
+
const useIterm = isItermRunning();
|
|
5063
|
+
const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
5064
|
+
const appleScript = useIterm ? `tell application "iTerm2"
|
|
5065
|
+
activate
|
|
5066
|
+
set newWindow to (create window with default profile)
|
|
5067
|
+
tell current session of newWindow
|
|
5068
|
+
write text "${escaped}"
|
|
5069
|
+
end tell
|
|
5070
|
+
end tell` : `tell application "Terminal"
|
|
5071
|
+
activate
|
|
5072
|
+
do script "${escaped}"
|
|
5073
|
+
end tell`;
|
|
5074
|
+
const tmpFile = join11(tmpdir(), `zam-terminal-${label}.scpt`);
|
|
5075
|
+
try {
|
|
5076
|
+
writeFileSync6(tmpFile, appleScript);
|
|
5077
|
+
execSync4(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
|
|
5078
|
+
if (!silent) {
|
|
5079
|
+
console.log(
|
|
5080
|
+
`Opened ${useIterm ? "iTerm2" : "Terminal.app"} window (${label})`
|
|
5081
|
+
);
|
|
5082
|
+
console.log(` Directory: ${dir}`);
|
|
5083
|
+
}
|
|
5084
|
+
} catch (err) {
|
|
5085
|
+
if (!silent) {
|
|
5086
|
+
console.error(`Failed to open terminal: ${err.message}`);
|
|
5087
|
+
console.log(`
|
|
5088
|
+
Run this manually in a new terminal:
|
|
5089
|
+
`);
|
|
5090
|
+
console.log(` ${shellSetup}`);
|
|
5091
|
+
}
|
|
5092
|
+
} finally {
|
|
5093
|
+
try {
|
|
5094
|
+
unlinkSync(tmpFile);
|
|
5095
|
+
} catch {
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
function openWindowsPowerShell(shellSetup, label, dir, requestedShell, silent) {
|
|
5100
|
+
const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
|
|
5101
|
+
const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
|
|
5102
|
+
const startCommand = [
|
|
5103
|
+
"Start-Process",
|
|
5104
|
+
`-FilePath ${psSingleQuoted2(executable)}`,
|
|
5105
|
+
`-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
|
|
5106
|
+
].join(" ");
|
|
5107
|
+
const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
|
|
5108
|
+
try {
|
|
5109
|
+
execFileSync2(
|
|
5110
|
+
launcher,
|
|
5111
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
|
|
5112
|
+
{
|
|
5113
|
+
stdio: "ignore"
|
|
5114
|
+
}
|
|
5115
|
+
);
|
|
5116
|
+
if (!silent) {
|
|
5117
|
+
console.log(
|
|
5118
|
+
`Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window (${label})`
|
|
5119
|
+
);
|
|
5120
|
+
console.log(` Directory: ${dir}`);
|
|
5121
|
+
}
|
|
5122
|
+
} catch (err) {
|
|
5123
|
+
if (!silent) {
|
|
5124
|
+
console.error(`Failed to open PowerShell: ${err.message}`);
|
|
5125
|
+
console.log(`
|
|
5126
|
+
Run this manually in a new PowerShell terminal:
|
|
5127
|
+
`);
|
|
5128
|
+
console.log(` ${shellSetup}`);
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
function openTerminalWindow(opts) {
|
|
5133
|
+
const { shellSetup, label, dir, shell, silent = false } = opts;
|
|
5134
|
+
const platform = opts.platform ?? process.platform;
|
|
5135
|
+
if (platform === "darwin" && !isPowerShellShell(shell)) {
|
|
5136
|
+
openMacTerminal(shellSetup, label, dir, silent);
|
|
5137
|
+
return;
|
|
5138
|
+
}
|
|
5139
|
+
if (platform === "win32" && isPowerShellShell(shell)) {
|
|
5140
|
+
openWindowsPowerShell(shellSetup, label, dir, shell, silent);
|
|
5141
|
+
return;
|
|
5142
|
+
}
|
|
5143
|
+
if (!silent) {
|
|
5144
|
+
console.log(`Run this in a new terminal:
|
|
5145
|
+
`);
|
|
5146
|
+
console.log(` ${shellSetup}
|
|
5147
|
+
`);
|
|
5148
|
+
console.log(
|
|
5149
|
+
`(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
|
|
5150
|
+
);
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
|
|
5154
|
+
// src/cli/agent-harness.ts
|
|
5155
|
+
var AGENT_HARNESSES = [
|
|
5156
|
+
{ id: "claude-code", label: "Claude Code", kind: "cli", command: "claude" },
|
|
5157
|
+
{ id: "codex", label: "Codex", kind: "cli", command: "codex" },
|
|
5158
|
+
{ id: "opencode", label: "opencode", kind: "cli", command: "opencode" },
|
|
5159
|
+
{
|
|
5160
|
+
id: "cursor",
|
|
5161
|
+
label: "Cursor",
|
|
5162
|
+
kind: "app",
|
|
5163
|
+
command: "cursor",
|
|
5164
|
+
candidatePaths: {
|
|
5165
|
+
win32: [
|
|
5166
|
+
join12(homedir8(), "AppData", "Local", "Programs", "cursor", "Cursor.exe")
|
|
5167
|
+
],
|
|
5168
|
+
darwin: ["/Applications/Cursor.app/Contents/MacOS/Cursor"]
|
|
5169
|
+
}
|
|
5170
|
+
},
|
|
5171
|
+
{ id: "copilot", label: "GitHub Copilot", kind: "app", command: "copilot" },
|
|
5172
|
+
{
|
|
5173
|
+
id: "antigravity",
|
|
5174
|
+
label: "Antigravity",
|
|
5175
|
+
kind: "app",
|
|
5176
|
+
command: "antigravity"
|
|
5177
|
+
}
|
|
5178
|
+
];
|
|
5179
|
+
function getHarness(id) {
|
|
5180
|
+
return AGENT_HARNESSES.find((h) => h.id === id);
|
|
5181
|
+
}
|
|
5182
|
+
function resolveHarnessExecutable(harness, overrideCommand, deps = {}) {
|
|
5183
|
+
const find = deps.find ?? findExecutable;
|
|
5184
|
+
const exists = deps.exists ?? existsSync12;
|
|
5185
|
+
const platform = deps.platform ?? process.platform;
|
|
5186
|
+
const found = find(overrideCommand || harness.command);
|
|
5187
|
+
if (found) return found;
|
|
5188
|
+
if (harness.kind === "app") {
|
|
5189
|
+
for (const candidate of harness.candidatePaths?.[platform] ?? []) {
|
|
5190
|
+
if (exists(candidate)) return candidate;
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
return null;
|
|
5194
|
+
}
|
|
5195
|
+
function planHarnessLaunch(harness, opts) {
|
|
5196
|
+
if (harness.kind === "cli") {
|
|
5197
|
+
const invocation = isPowerShellShell(opts.shell) ? `& ${psSingleQuoted2(opts.executable)}` : JSON.stringify(opts.executable);
|
|
5198
|
+
return {
|
|
5199
|
+
kind: "cli",
|
|
5200
|
+
shell: opts.shell,
|
|
5201
|
+
shellSetup: buildShellSetupCommand(
|
|
5202
|
+
opts.workspace,
|
|
5203
|
+
opts.shell,
|
|
5204
|
+
invocation
|
|
5205
|
+
)
|
|
5206
|
+
};
|
|
5207
|
+
}
|
|
5208
|
+
return { kind: "app", executable: opts.executable, args: [opts.workspace] };
|
|
5209
|
+
}
|
|
5210
|
+
function launchHarness(harness, opts) {
|
|
5211
|
+
const plan = planHarnessLaunch(harness, opts);
|
|
5212
|
+
if (plan.kind === "cli") {
|
|
5213
|
+
openTerminalWindow({
|
|
5214
|
+
shellSetup: plan.shellSetup,
|
|
5215
|
+
label: `agent-${harness.id}`,
|
|
5216
|
+
dir: opts.workspace,
|
|
5217
|
+
shell: plan.shell,
|
|
5218
|
+
silent: opts.silent,
|
|
5219
|
+
platform: opts.platform
|
|
5220
|
+
});
|
|
5221
|
+
return;
|
|
5222
|
+
}
|
|
5223
|
+
spawn(plan.executable, plan.args, {
|
|
5224
|
+
detached: true,
|
|
5225
|
+
stdio: "ignore",
|
|
5226
|
+
windowsHide: true
|
|
5227
|
+
}).unref();
|
|
5228
|
+
if (!opts.silent) {
|
|
5229
|
+
console.log(`Launched ${harness.label} in ${opts.workspace}`);
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
|
|
4882
5233
|
// src/cli/commands/agent.ts
|
|
4883
5234
|
var C = {
|
|
4884
5235
|
reset: "\x1B[0m",
|
|
@@ -4890,7 +5241,7 @@ var C = {
|
|
|
4890
5241
|
};
|
|
4891
5242
|
var SUPPORTED_AGENTS = ["opencode"];
|
|
4892
5243
|
function agentsMdPresent(cwd = process.cwd()) {
|
|
4893
|
-
return
|
|
5244
|
+
return existsSync13(join13(cwd, "AGENTS.md"));
|
|
4894
5245
|
}
|
|
4895
5246
|
function printStatus() {
|
|
4896
5247
|
const installed = hasCommand("opencode");
|
|
@@ -4928,19 +5279,87 @@ var installCmd = new Command("install").description("Download and install the de
|
|
|
4928
5279
|
console.log(` Start it with: ${C.cyan}opencode${C.reset}`);
|
|
4929
5280
|
});
|
|
4930
5281
|
var statusCmd = new Command("status").description("Show whether the agent is installed and wired to ZAM").action(printStatus);
|
|
4931
|
-
var
|
|
5282
|
+
var listCmd = new Command("list").description("List known agent harnesses and whether each is detected").action(() => {
|
|
5283
|
+
console.log(`${C.bold}Agent harnesses${C.reset}`);
|
|
5284
|
+
for (const h of AGENT_HARNESSES) {
|
|
5285
|
+
const status = resolveHarnessExecutable(h) ? `${C.green}detected${C.reset}` : `${C.yellow}not found${C.reset}`;
|
|
5286
|
+
console.log(
|
|
5287
|
+
` ${h.id.padEnd(13)} ${status} ${C.dim}${h.label} (${h.kind})${C.reset}`
|
|
5288
|
+
);
|
|
5289
|
+
}
|
|
5290
|
+
console.log(`
|
|
5291
|
+
${C.dim}Open one: zam agent open --id <id>${C.reset}`);
|
|
5292
|
+
console.log(
|
|
5293
|
+
` ${C.dim}Point ZAM at a custom path: zam settings set agent.<id>.command <path>${C.reset}`
|
|
5294
|
+
);
|
|
5295
|
+
});
|
|
5296
|
+
var openCmd = new Command("open").description("Open an agent harness in the workspace, ready to drive ZAM").option(
|
|
5297
|
+
"--id <id>",
|
|
5298
|
+
"Harness id (default: agent.default setting, else first detected)"
|
|
5299
|
+
).option("--dir <path>", "Workspace directory (defaults to cwd)").option(
|
|
5300
|
+
"--shell <type>",
|
|
5301
|
+
"Shell type: zsh | bash | pwsh | powershell (auto-detected)"
|
|
5302
|
+
).action(async (opts) => {
|
|
5303
|
+
let shell;
|
|
5304
|
+
try {
|
|
5305
|
+
shell = normalizeShell(opts.shell);
|
|
5306
|
+
} catch (err) {
|
|
5307
|
+
console.error(`Error: ${err.message}`);
|
|
5308
|
+
process.exit(1);
|
|
5309
|
+
}
|
|
5310
|
+
let id = opts.id;
|
|
5311
|
+
let override;
|
|
5312
|
+
let db;
|
|
5313
|
+
try {
|
|
5314
|
+
db = await openDatabase();
|
|
5315
|
+
if (!id) {
|
|
5316
|
+
id = await getSetting(db, "agent.default") || void 0;
|
|
5317
|
+
}
|
|
5318
|
+
if (!id) {
|
|
5319
|
+
id = AGENT_HARNESSES.find((h) => resolveHarnessExecutable(h))?.id;
|
|
5320
|
+
}
|
|
5321
|
+
if (id) {
|
|
5322
|
+
override = await getSetting(db, `agent.${id}.command`) || void 0;
|
|
5323
|
+
}
|
|
5324
|
+
} finally {
|
|
5325
|
+
await db?.close();
|
|
5326
|
+
}
|
|
5327
|
+
if (!id) {
|
|
5328
|
+
console.error(
|
|
5329
|
+
"No agent harness configured or detected. Install one (e.g. zam agent install), then set a default: zam settings set agent.default <id>."
|
|
5330
|
+
);
|
|
5331
|
+
process.exit(1);
|
|
5332
|
+
}
|
|
5333
|
+
const harness = getHarness(id);
|
|
5334
|
+
if (!harness) {
|
|
5335
|
+
console.error(
|
|
5336
|
+
`Unknown harness: ${id}. Known: ${AGENT_HARNESSES.map((h) => h.id).join(", ")}.`
|
|
5337
|
+
);
|
|
5338
|
+
process.exit(1);
|
|
5339
|
+
}
|
|
5340
|
+
const executable = resolveHarnessExecutable(harness, override);
|
|
5341
|
+
if (!executable) {
|
|
5342
|
+
console.error(
|
|
5343
|
+
`${harness.label} was not detected. Install it, or point ZAM at it: zam settings set agent.${harness.id}.command <path>.`
|
|
5344
|
+
);
|
|
5345
|
+
process.exit(1);
|
|
5346
|
+
}
|
|
5347
|
+
const workspace = opts.dir ?? process.cwd();
|
|
5348
|
+
launchHarness(harness, { executable, workspace, shell });
|
|
5349
|
+
});
|
|
5350
|
+
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);
|
|
4932
5351
|
|
|
4933
5352
|
// src/cli/commands/bridge.ts
|
|
4934
|
-
import { execFileSync as
|
|
5353
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
4935
5354
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
4936
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
4937
|
-
import { homedir as
|
|
4938
|
-
import { join as
|
|
4939
|
-
import { Command as
|
|
5355
|
+
import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync2 } from "fs";
|
|
5356
|
+
import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
|
|
5357
|
+
import { basename as basename5, join as join17, resolve as resolve5 } from "path";
|
|
5358
|
+
import { Command as Command4 } from "commander";
|
|
4940
5359
|
|
|
4941
5360
|
// src/cli/llm/client.ts
|
|
4942
|
-
import { spawn } from "child_process";
|
|
4943
|
-
import { existsSync as
|
|
5361
|
+
import { spawn as spawn2 } from "child_process";
|
|
5362
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4944
5363
|
var DEFAULT_LLM_URL = "http://localhost:8000/v1";
|
|
4945
5364
|
var DEFAULT_LLM_MODEL = "qwen3.5:4b";
|
|
4946
5365
|
var DEFAULT_LLM_API_KEY = "sk-none";
|
|
@@ -4953,22 +5372,149 @@ async function getLlmConfig(db) {
|
|
|
4953
5372
|
locale: await getSetting(db, "system.locale") || "en"
|
|
4954
5373
|
};
|
|
4955
5374
|
}
|
|
4956
|
-
|
|
4957
|
-
const
|
|
4958
|
-
|
|
4959
|
-
|
|
5375
|
+
function getCloudModelRecommendation(url) {
|
|
5376
|
+
const lowercase = url.toLowerCase();
|
|
5377
|
+
if (lowercase.includes("openrouter.ai")) {
|
|
5378
|
+
return "openrouter/free";
|
|
5379
|
+
}
|
|
5380
|
+
if (lowercase.includes("openai.com") || lowercase.includes("api.openai")) {
|
|
5381
|
+
return "gpt-5-mini";
|
|
5382
|
+
}
|
|
5383
|
+
if (lowercase.includes("googleapis.com") || lowercase.includes("google")) {
|
|
5384
|
+
return "gemini-3.5-flash";
|
|
5385
|
+
}
|
|
5386
|
+
if (lowercase.includes("deepseek.com")) {
|
|
5387
|
+
return "deepseek-v4-flash";
|
|
5388
|
+
}
|
|
5389
|
+
if (lowercase.includes("mimo")) {
|
|
5390
|
+
return "mimo-v2.5";
|
|
5391
|
+
}
|
|
5392
|
+
return null;
|
|
5393
|
+
}
|
|
5394
|
+
function inferApiFlavor(url) {
|
|
5395
|
+
try {
|
|
5396
|
+
return new URL(url).hostname.toLowerCase().endsWith("anthropic.com") ? "anthropic-messages" : "chat-completions";
|
|
5397
|
+
} catch {
|
|
5398
|
+
return "chat-completions";
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
async function readJsonSetting(db, key) {
|
|
5402
|
+
const raw = await getSetting(db, key);
|
|
5403
|
+
if (!raw) return null;
|
|
5404
|
+
try {
|
|
5405
|
+
return JSON.parse(raw);
|
|
5406
|
+
} catch {
|
|
5407
|
+
return null;
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
function resolveProviderApiKey(rec) {
|
|
5411
|
+
if (rec.apiKey) return rec.apiKey;
|
|
5412
|
+
if (rec.apiKeyRef) {
|
|
5413
|
+
const key = getProviderApiKey(rec.apiKeyRef);
|
|
5414
|
+
if (key) return key;
|
|
5415
|
+
}
|
|
5416
|
+
return DEFAULT_LLM_API_KEY;
|
|
5417
|
+
}
|
|
5418
|
+
async function getProviderForRole(db, role) {
|
|
5419
|
+
const enabled = role === "vision" ? await getSetting(db, "llm.vision.enabled") === "true" : await getSetting(db, "llm.enabled") === "true";
|
|
5420
|
+
const base = await getLegacyRoleConfig(db, role, enabled);
|
|
5421
|
+
const providers = await readJsonSetting(db, "llm.providers");
|
|
5422
|
+
const roles = await readJsonSetting(db, "llm.roles");
|
|
5423
|
+
const binding = roles?.[role];
|
|
5424
|
+
let resolved = base;
|
|
5425
|
+
if (providers && binding?.primary && providers[binding.primary]) {
|
|
5426
|
+
const primary = materializeProvider(
|
|
5427
|
+
providers[binding.primary],
|
|
5428
|
+
base,
|
|
5429
|
+
role,
|
|
5430
|
+
{
|
|
5431
|
+
providerName: binding.primary,
|
|
5432
|
+
source: "shared"
|
|
5433
|
+
}
|
|
5434
|
+
);
|
|
5435
|
+
const fallback = binding.fallback && providers[binding.fallback] ? materializeProvider(providers[binding.fallback], base, role, {
|
|
5436
|
+
providerName: binding.fallback,
|
|
5437
|
+
source: "shared"
|
|
5438
|
+
}) : void 0;
|
|
5439
|
+
resolved = { ...primary, fallback };
|
|
5440
|
+
}
|
|
5441
|
+
const machine = getMachineAiConfig();
|
|
5442
|
+
const machineProviders = machine.providers;
|
|
5443
|
+
const machineBinding = machine.roles?.[role];
|
|
5444
|
+
if (machineProviders && machineBinding?.primary && machineProviders[machineBinding.primary]) {
|
|
5445
|
+
const primary = materializeProvider(
|
|
5446
|
+
machineProviders[machineBinding.primary],
|
|
5447
|
+
resolved,
|
|
5448
|
+
role,
|
|
5449
|
+
{ providerName: machineBinding.primary, source: "machine" }
|
|
5450
|
+
);
|
|
5451
|
+
const fallback = machineBinding.fallback && machineProviders[machineBinding.fallback] ? materializeProvider(
|
|
5452
|
+
machineProviders[machineBinding.fallback],
|
|
5453
|
+
resolved,
|
|
5454
|
+
role,
|
|
5455
|
+
{ providerName: machineBinding.fallback, source: "machine" }
|
|
5456
|
+
) : void 0;
|
|
5457
|
+
return { ...primary, fallback };
|
|
5458
|
+
}
|
|
5459
|
+
return resolved;
|
|
5460
|
+
}
|
|
5461
|
+
function materializeProvider(rec, base, role, meta) {
|
|
5462
|
+
const url = rec.url || base.url;
|
|
4960
5463
|
return {
|
|
4961
|
-
enabled:
|
|
4962
|
-
url
|
|
4963
|
-
model:
|
|
4964
|
-
apiKey:
|
|
5464
|
+
enabled: base.enabled,
|
|
5465
|
+
url,
|
|
5466
|
+
model: rec.model || base.model,
|
|
5467
|
+
apiKey: resolveProviderApiKey(rec),
|
|
5468
|
+
apiFlavor: rec.apiFlavor || inferApiFlavor(url),
|
|
4965
5469
|
locale: base.locale,
|
|
4966
|
-
|
|
5470
|
+
providerName: meta.providerName,
|
|
5471
|
+
label: rec.label,
|
|
5472
|
+
source: meta.source,
|
|
5473
|
+
local: rec.local ?? isLocalEndpoint(url),
|
|
5474
|
+
...role === "vision" ? { maxFrames: base.maxFrames } : {}
|
|
4967
5475
|
};
|
|
4968
5476
|
}
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
5477
|
+
async function getLegacyRoleConfig(db, role, enabled) {
|
|
5478
|
+
const base = await getLlmConfig(db);
|
|
5479
|
+
if (role === "vision") {
|
|
5480
|
+
const maxFramesStr = await getSetting(db, "llm.vision.max_frames");
|
|
5481
|
+
const parsed = maxFramesStr ? parseInt(maxFramesStr, 10) : 100;
|
|
5482
|
+
const url = await getSetting(db, "llm.vision.url") || base.url;
|
|
5483
|
+
let model = await getSetting(db, "llm.vision.model");
|
|
5484
|
+
if (!model) model = getCloudModelRecommendation(url) || base.model;
|
|
5485
|
+
return {
|
|
5486
|
+
enabled,
|
|
5487
|
+
url,
|
|
5488
|
+
model,
|
|
5489
|
+
apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
|
|
5490
|
+
apiFlavor: inferApiFlavor(url),
|
|
5491
|
+
locale: base.locale,
|
|
5492
|
+
source: "legacy",
|
|
5493
|
+
local: isLocalEndpoint(url),
|
|
5494
|
+
maxFrames: Number.isNaN(parsed) ? 100 : parsed
|
|
5495
|
+
};
|
|
5496
|
+
}
|
|
5497
|
+
return {
|
|
5498
|
+
enabled,
|
|
5499
|
+
url: base.url,
|
|
5500
|
+
model: base.model,
|
|
5501
|
+
apiKey: base.apiKey,
|
|
5502
|
+
apiFlavor: inferApiFlavor(base.url),
|
|
5503
|
+
locale: base.locale,
|
|
5504
|
+
source: "legacy",
|
|
5505
|
+
local: isLocalEndpoint(base.url)
|
|
5506
|
+
};
|
|
5507
|
+
}
|
|
5508
|
+
function assertChatCompletions(cfg) {
|
|
5509
|
+
if (cfg.apiFlavor !== "chat-completions") {
|
|
5510
|
+
throw new Error(
|
|
5511
|
+
`This role is configured for a "${cfg.apiFlavor}" provider, which is not supported here yet. Use a chat-completions provider for the recall role.`
|
|
5512
|
+
);
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
var LANGUAGE_NAMES = {
|
|
5516
|
+
en: "English",
|
|
5517
|
+
de: "German",
|
|
4972
5518
|
es: "Spanish",
|
|
4973
5519
|
fr: "French",
|
|
4974
5520
|
pt: "Portuguese",
|
|
@@ -5006,10 +5552,11 @@ async function readChatContent(res, label) {
|
|
|
5006
5552
|
return content.trim();
|
|
5007
5553
|
}
|
|
5008
5554
|
async function generateQuestionViaLLM(db, input8) {
|
|
5009
|
-
const cfg = await
|
|
5555
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5010
5556
|
if (!cfg.enabled) {
|
|
5011
5557
|
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5012
5558
|
}
|
|
5559
|
+
assertChatCompletions(cfg);
|
|
5013
5560
|
const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
|
|
5014
5561
|
const verb = BLOOM_VERBS2[bloom];
|
|
5015
5562
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
@@ -5049,10 +5596,11 @@ Active-Recall Question:`;
|
|
|
5049
5596
|
return readChatContent(res, "LLM request");
|
|
5050
5597
|
}
|
|
5051
5598
|
async function evaluateAnswerViaLLM(db, input8) {
|
|
5052
|
-
const cfg = await
|
|
5599
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5053
5600
|
if (!cfg.enabled) {
|
|
5054
5601
|
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5055
5602
|
}
|
|
5603
|
+
assertChatCompletions(cfg);
|
|
5056
5604
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
5057
5605
|
const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
|
|
5058
5606
|
const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
|
|
@@ -5102,10 +5650,11 @@ Evaluation:`;
|
|
|
5102
5650
|
return readChatContent(res, "LLM evaluation");
|
|
5103
5651
|
}
|
|
5104
5652
|
async function translateQuestionViaLLM(db, question) {
|
|
5105
|
-
const cfg = await
|
|
5653
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5106
5654
|
if (!cfg.enabled) {
|
|
5107
5655
|
throw new Error("LLM integration is disabled in settings");
|
|
5108
5656
|
}
|
|
5657
|
+
assertChatCompletions(cfg);
|
|
5109
5658
|
const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
5110
5659
|
const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
|
|
5111
5660
|
Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
|
|
@@ -5159,38 +5708,174 @@ async function getAvailableModels(url, apiKey = DEFAULT_LLM_API_KEY) {
|
|
|
5159
5708
|
return [];
|
|
5160
5709
|
}
|
|
5161
5710
|
}
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
(candidate) => candidate.toLowerCase() === model.toLowerCase()
|
|
5175
|
-
);
|
|
5176
|
-
}
|
|
5711
|
+
function isLocalEndpoint(url) {
|
|
5712
|
+
return url.includes("localhost") || url.includes("127.0.0.1") || url.includes("[::1]") || url.includes("::1");
|
|
5713
|
+
}
|
|
5714
|
+
async function checkProviderEndpoint(endpoint) {
|
|
5715
|
+
const online = await isLlmOnline(endpoint.url);
|
|
5716
|
+
if (!online) {
|
|
5717
|
+
return {
|
|
5718
|
+
endpoint,
|
|
5719
|
+
online: false,
|
|
5720
|
+
availableModels: [],
|
|
5721
|
+
modelAvailable: false
|
|
5722
|
+
};
|
|
5177
5723
|
}
|
|
5724
|
+
const availableModels = await getAvailableModels(
|
|
5725
|
+
endpoint.url,
|
|
5726
|
+
endpoint.apiKey
|
|
5727
|
+
);
|
|
5728
|
+
const modelAvailable = availableModels.length === 0 || availableModels.some(
|
|
5729
|
+
(candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
|
|
5730
|
+
);
|
|
5731
|
+
return { endpoint, online, availableModels, modelAvailable };
|
|
5732
|
+
}
|
|
5733
|
+
function isEndpointUsable(readiness) {
|
|
5734
|
+
return readiness.online && readiness.modelAvailable;
|
|
5735
|
+
}
|
|
5736
|
+
function providerChain(primary) {
|
|
5737
|
+
return [primary, ...primary.fallback ? [primary.fallback] : []];
|
|
5738
|
+
}
|
|
5739
|
+
async function checkProviderChain(primary) {
|
|
5740
|
+
let first;
|
|
5741
|
+
for (const endpoint of providerChain(primary)) {
|
|
5742
|
+
const readiness = await checkProviderEndpoint(endpoint);
|
|
5743
|
+
first ??= readiness;
|
|
5744
|
+
if (isEndpointUsable(readiness)) {
|
|
5745
|
+
return { primary: first, firstUsable: readiness };
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
return { primary: first };
|
|
5749
|
+
}
|
|
5750
|
+
async function isVisionProviderModelExplicit(db, active) {
|
|
5751
|
+
if (await getSetting(db, "llm.vision.model")) return true;
|
|
5752
|
+
const providers = await readJsonSetting(db, "llm.providers");
|
|
5753
|
+
const roles = await readJsonSetting(db, "llm.roles");
|
|
5754
|
+
const binding = roles?.vision;
|
|
5755
|
+
const sharedExplicit = [binding?.primary, binding?.fallback].some((id) => {
|
|
5756
|
+
if (!id) return false;
|
|
5757
|
+
const rec = providers?.[id];
|
|
5758
|
+
return rec?.model === active.model && (rec.url === void 0 || rec.url === active.url);
|
|
5759
|
+
});
|
|
5760
|
+
if (sharedExplicit) return true;
|
|
5761
|
+
const machine = getMachineAiConfig();
|
|
5762
|
+
const machineBinding = machine.roles?.vision;
|
|
5763
|
+
return [machineBinding?.primary, machineBinding?.fallback].some((id) => {
|
|
5764
|
+
if (!id) return false;
|
|
5765
|
+
const rec = machine.providers?.[id];
|
|
5766
|
+
return rec?.model === active.model && (rec.url === void 0 || rec.url === active.url);
|
|
5767
|
+
});
|
|
5768
|
+
}
|
|
5769
|
+
async function checkVisionReadiness(db) {
|
|
5770
|
+
const cfg = await getProviderForRole(db, "vision");
|
|
5771
|
+
const chain = cfg.enabled ? await checkProviderChain(cfg) : void 0;
|
|
5772
|
+
const selected = chain?.firstUsable ?? chain?.primary;
|
|
5773
|
+
const active = selected?.endpoint ?? cfg;
|
|
5774
|
+
const online = selected?.online ?? false;
|
|
5775
|
+
const availableModels = selected?.availableModels ?? [];
|
|
5776
|
+
const modelAvailable = selected?.modelAvailable ?? false;
|
|
5777
|
+
const visionModelExplicit = await isVisionProviderModelExplicit(db, active);
|
|
5178
5778
|
let warning;
|
|
5179
|
-
if (enabled && online && modelAvailable && !visionModelExplicit) {
|
|
5180
|
-
|
|
5779
|
+
if (cfg.enabled && online && modelAvailable && !visionModelExplicit) {
|
|
5780
|
+
const cloudRec = getCloudModelRecommendation(active.url);
|
|
5781
|
+
if (cloudRec && active.model === cloudRec) {
|
|
5782
|
+
} else {
|
|
5783
|
+
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>`;
|
|
5784
|
+
}
|
|
5181
5785
|
}
|
|
5182
5786
|
return {
|
|
5183
|
-
enabled,
|
|
5787
|
+
enabled: cfg.enabled,
|
|
5184
5788
|
online,
|
|
5185
|
-
url,
|
|
5186
|
-
model,
|
|
5789
|
+
url: active.url,
|
|
5790
|
+
model: active.model,
|
|
5187
5791
|
modelAvailable,
|
|
5188
5792
|
availableModels,
|
|
5189
|
-
usable: enabled && online && modelAvailable,
|
|
5793
|
+
usable: cfg.enabled && online && modelAvailable,
|
|
5190
5794
|
visionModelExplicit,
|
|
5191
5795
|
warning
|
|
5192
5796
|
};
|
|
5193
5797
|
}
|
|
5798
|
+
function summarizeFallback(provider) {
|
|
5799
|
+
if (!provider) return void 0;
|
|
5800
|
+
return {
|
|
5801
|
+
providerName: provider.providerName,
|
|
5802
|
+
label: provider.label,
|
|
5803
|
+
source: provider.source,
|
|
5804
|
+
url: provider.url,
|
|
5805
|
+
model: provider.model,
|
|
5806
|
+
apiFlavor: provider.apiFlavor,
|
|
5807
|
+
local: provider.local
|
|
5808
|
+
};
|
|
5809
|
+
}
|
|
5810
|
+
async function getProviderRoleStatus(db, role) {
|
|
5811
|
+
const cfg = await getProviderForRole(db, role);
|
|
5812
|
+
const unsupportedProvider = role !== "vision" && cfg.apiFlavor !== "chat-completions";
|
|
5813
|
+
if (!cfg.enabled) {
|
|
5814
|
+
return {
|
|
5815
|
+
role,
|
|
5816
|
+
enabled: false,
|
|
5817
|
+
providerName: cfg.providerName,
|
|
5818
|
+
label: cfg.label,
|
|
5819
|
+
source: cfg.source,
|
|
5820
|
+
url: cfg.url,
|
|
5821
|
+
model: cfg.model,
|
|
5822
|
+
apiFlavor: cfg.apiFlavor,
|
|
5823
|
+
local: cfg.local,
|
|
5824
|
+
online: false,
|
|
5825
|
+
modelAvailable: false,
|
|
5826
|
+
availableModels: [],
|
|
5827
|
+
usable: false,
|
|
5828
|
+
reason: "disabled",
|
|
5829
|
+
fallback: summarizeFallback(cfg.fallback)
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
if (unsupportedProvider) {
|
|
5833
|
+
return {
|
|
5834
|
+
role,
|
|
5835
|
+
enabled: true,
|
|
5836
|
+
providerName: cfg.providerName,
|
|
5837
|
+
label: cfg.label,
|
|
5838
|
+
source: cfg.source,
|
|
5839
|
+
url: cfg.url,
|
|
5840
|
+
model: cfg.model,
|
|
5841
|
+
apiFlavor: cfg.apiFlavor,
|
|
5842
|
+
local: cfg.local,
|
|
5843
|
+
online: false,
|
|
5844
|
+
modelAvailable: false,
|
|
5845
|
+
availableModels: [],
|
|
5846
|
+
usable: false,
|
|
5847
|
+
reason: "unsupported-provider",
|
|
5848
|
+
fallback: summarizeFallback(cfg.fallback)
|
|
5849
|
+
};
|
|
5850
|
+
}
|
|
5851
|
+
let selected;
|
|
5852
|
+
if (role === "vision") {
|
|
5853
|
+
const chain = await checkProviderChain(cfg);
|
|
5854
|
+
selected = chain.firstUsable ?? chain.primary;
|
|
5855
|
+
} else {
|
|
5856
|
+
selected = await checkProviderEndpoint(cfg);
|
|
5857
|
+
}
|
|
5858
|
+
const active = selected.endpoint;
|
|
5859
|
+
const usable = selected.online && selected.modelAvailable;
|
|
5860
|
+
const reason = usable ? void 0 : selected.online ? "model-not-found" : "offline";
|
|
5861
|
+
return {
|
|
5862
|
+
role,
|
|
5863
|
+
enabled: true,
|
|
5864
|
+
providerName: active.providerName,
|
|
5865
|
+
label: active.label,
|
|
5866
|
+
source: active.source,
|
|
5867
|
+
url: active.url,
|
|
5868
|
+
model: active.model,
|
|
5869
|
+
apiFlavor: active.apiFlavor,
|
|
5870
|
+
local: active.local,
|
|
5871
|
+
online: selected.online,
|
|
5872
|
+
modelAvailable: selected.modelAvailable,
|
|
5873
|
+
availableModels: selected.availableModels,
|
|
5874
|
+
usable,
|
|
5875
|
+
reason,
|
|
5876
|
+
fallback: summarizeFallback(cfg.fallback)
|
|
5877
|
+
};
|
|
5878
|
+
}
|
|
5194
5879
|
function detectRunner(url, model) {
|
|
5195
5880
|
let runner = "unknown";
|
|
5196
5881
|
let port = "8000";
|
|
@@ -5211,16 +5896,16 @@ function spawnLocalRunner(url, model) {
|
|
|
5211
5896
|
const { runner, port } = detectRunner(url, model);
|
|
5212
5897
|
try {
|
|
5213
5898
|
if (runner === "fastflowlm") {
|
|
5214
|
-
const flmExe =
|
|
5899
|
+
const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
|
|
5215
5900
|
if (!hasCommand("flm") && flmExe === "flm") return;
|
|
5216
|
-
|
|
5901
|
+
spawn2(flmExe, ["serve", model, "--port", port], {
|
|
5217
5902
|
detached: true,
|
|
5218
5903
|
stdio: "ignore",
|
|
5219
5904
|
windowsHide: true
|
|
5220
5905
|
}).unref();
|
|
5221
5906
|
} else if (runner === "ollama" || runner === "generic") {
|
|
5222
5907
|
if (!hasCommand("ollama")) return;
|
|
5223
|
-
|
|
5908
|
+
spawn2("ollama", ["serve"], {
|
|
5224
5909
|
detached: true,
|
|
5225
5910
|
stdio: "ignore",
|
|
5226
5911
|
windowsHide: true
|
|
@@ -5232,7 +5917,7 @@ function spawnLocalRunner(url, model) {
|
|
|
5232
5917
|
async function startLocalRunner(url, model, locale) {
|
|
5233
5918
|
const { runner, port } = detectRunner(url, model);
|
|
5234
5919
|
if (runner === "fastflowlm") {
|
|
5235
|
-
const flmExe =
|
|
5920
|
+
const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
|
|
5236
5921
|
if (!hasCommand("flm") && flmExe === "flm") {
|
|
5237
5922
|
console.warn(
|
|
5238
5923
|
"\x1B[31m\u2717 FastFlowLM is configured but could not be found on the system.\x1B[0m"
|
|
@@ -5245,7 +5930,7 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5245
5930
|
`\x1B[36mStarting FastFlowLM serve process: ${flmExe} ${args.join(" ")}\x1B[0m`
|
|
5246
5931
|
);
|
|
5247
5932
|
try {
|
|
5248
|
-
|
|
5933
|
+
spawn2(flmExe, args, {
|
|
5249
5934
|
detached: true,
|
|
5250
5935
|
stdio: "ignore",
|
|
5251
5936
|
windowsHide: true
|
|
@@ -5266,7 +5951,7 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5266
5951
|
}
|
|
5267
5952
|
console.log("\x1B[36mStarting Ollama serve process: ollama serve\x1B[0m");
|
|
5268
5953
|
try {
|
|
5269
|
-
|
|
5954
|
+
spawn2("ollama", ["serve"], {
|
|
5270
5955
|
detached: true,
|
|
5271
5956
|
stdio: "ignore",
|
|
5272
5957
|
windowsHide: true
|
|
@@ -5289,7 +5974,7 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5289
5974
|
let attempts = 0;
|
|
5290
5975
|
const dotsPerLine = 30;
|
|
5291
5976
|
while (true) {
|
|
5292
|
-
await new Promise((
|
|
5977
|
+
await new Promise((resolve8) => setTimeout(resolve8, 500));
|
|
5293
5978
|
if (await isLlmOnline(url)) {
|
|
5294
5979
|
if (attempts > 0) process.stdout.write("\n");
|
|
5295
5980
|
return true;
|
|
@@ -5314,12 +5999,18 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5314
5999
|
}
|
|
5315
6000
|
}
|
|
5316
6001
|
async function ensureLocalLlmRunning(db) {
|
|
5317
|
-
const cfg = await
|
|
6002
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5318
6003
|
if (!cfg.enabled) {
|
|
5319
6004
|
return { usable: false, reason: "disabled" };
|
|
5320
6005
|
}
|
|
6006
|
+
if (cfg.apiFlavor !== "chat-completions") {
|
|
6007
|
+
console.warn(
|
|
6008
|
+
`\x1B[31m\u2717 Recall is configured for "${cfg.apiFlavor}", but active recall currently supports chat-completions providers only.\x1B[0m`
|
|
6009
|
+
);
|
|
6010
|
+
return { usable: false, reason: "unsupported-provider" };
|
|
6011
|
+
}
|
|
5321
6012
|
const { url, model, apiKey, locale } = cfg;
|
|
5322
|
-
const isLocal =
|
|
6013
|
+
const isLocal = isLocalEndpoint(url);
|
|
5323
6014
|
console.log(`Checking if local LLM server is online at ${url}...`);
|
|
5324
6015
|
let online = await isLlmOnline(url);
|
|
5325
6016
|
if (!online && isLocal) {
|
|
@@ -5353,8 +6044,9 @@ async function ensureLocalLlmRunning(db) {
|
|
|
5353
6044
|
}
|
|
5354
6045
|
async function ensureLlmReadyHeadless(db, opts = {}) {
|
|
5355
6046
|
const timeoutMs = opts.timeoutMs ?? 25e3;
|
|
5356
|
-
const
|
|
5357
|
-
|
|
6047
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
6048
|
+
const { url, model, apiKey } = cfg;
|
|
6049
|
+
if (!cfg.enabled) {
|
|
5358
6050
|
return {
|
|
5359
6051
|
usable: false,
|
|
5360
6052
|
reason: "disabled",
|
|
@@ -5363,7 +6055,16 @@ async function ensureLlmReadyHeadless(db, opts = {}) {
|
|
|
5363
6055
|
availableModels: []
|
|
5364
6056
|
};
|
|
5365
6057
|
}
|
|
5366
|
-
|
|
6058
|
+
if (cfg.apiFlavor !== "chat-completions") {
|
|
6059
|
+
return {
|
|
6060
|
+
usable: false,
|
|
6061
|
+
reason: "unsupported-provider",
|
|
6062
|
+
online: false,
|
|
6063
|
+
model,
|
|
6064
|
+
availableModels: []
|
|
6065
|
+
};
|
|
6066
|
+
}
|
|
6067
|
+
const isLocal = isLocalEndpoint(url);
|
|
5367
6068
|
let online = await isLlmOnline(url);
|
|
5368
6069
|
if (!online && isLocal) {
|
|
5369
6070
|
spawnLocalRunner(url, model);
|
|
@@ -5428,8 +6129,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
|
5428
6129
|
const dotsPerLine = 30;
|
|
5429
6130
|
while (true) {
|
|
5430
6131
|
let timeoutId;
|
|
5431
|
-
const timeoutPromise = new Promise((
|
|
5432
|
-
timeoutId = setTimeout(() =>
|
|
6132
|
+
const timeoutPromise = new Promise((resolve8) => {
|
|
6133
|
+
timeoutId = setTimeout(() => resolve8("timeout"), timeoutMs);
|
|
5433
6134
|
});
|
|
5434
6135
|
const dotsInterval = setInterval(() => {
|
|
5435
6136
|
process.stdout.write(".");
|
|
@@ -5502,8 +6203,8 @@ async function ensureHighQualityQuestion(db, token) {
|
|
|
5502
6203
|
// src/cli/llm/vision.ts
|
|
5503
6204
|
import { randomBytes } from "crypto";
|
|
5504
6205
|
import { readFileSync as readFileSync9 } from "fs";
|
|
5505
|
-
import { tmpdir } from "os";
|
|
5506
|
-
import { basename as
|
|
6206
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
6207
|
+
import { basename as basename3, join as join14 } from "path";
|
|
5507
6208
|
var LANGUAGE_NAMES2 = {
|
|
5508
6209
|
en: "English",
|
|
5509
6210
|
de: "German",
|
|
@@ -5530,7 +6231,7 @@ var ACTION_TYPES2 = /* @__PURE__ */ new Set([
|
|
|
5530
6231
|
"window-change"
|
|
5531
6232
|
]);
|
|
5532
6233
|
async function observeUiSnapshotViaLLM(db, input8) {
|
|
5533
|
-
const cfg = await
|
|
6234
|
+
const cfg = await getProviderForRole(db, "vision");
|
|
5534
6235
|
if (!cfg.enabled) {
|
|
5535
6236
|
throw new Error(
|
|
5536
6237
|
"Vision observation is disabled in settings (llm.vision.enabled)"
|
|
@@ -5539,21 +6240,21 @@ async function observeUiSnapshotViaLLM(db, input8) {
|
|
|
5539
6240
|
const imageUrls = [];
|
|
5540
6241
|
const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
|
|
5541
6242
|
if (isVideo) {
|
|
5542
|
-
const { mkdirSync:
|
|
5543
|
-
const { execSync:
|
|
5544
|
-
const tempDir =
|
|
5545
|
-
|
|
6243
|
+
const { mkdirSync: mkdirSync13, readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
|
|
6244
|
+
const { execSync: execSync6 } = await import("child_process");
|
|
6245
|
+
const tempDir = join14(
|
|
6246
|
+
tmpdir2(),
|
|
5546
6247
|
`zam-frames-${randomBytes(4).toString("hex")}`
|
|
5547
6248
|
);
|
|
5548
|
-
|
|
6249
|
+
mkdirSync13(tempDir, { recursive: true });
|
|
5549
6250
|
try {
|
|
5550
|
-
|
|
6251
|
+
execSync6(
|
|
5551
6252
|
`ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
|
|
5552
6253
|
{ stdio: "ignore" }
|
|
5553
6254
|
);
|
|
5554
6255
|
let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
|
|
5555
6256
|
if (files.length === 0) {
|
|
5556
|
-
|
|
6257
|
+
execSync6(
|
|
5557
6258
|
`ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
|
|
5558
6259
|
{ stdio: "ignore" }
|
|
5559
6260
|
);
|
|
@@ -5574,7 +6275,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
|
|
|
5574
6275
|
}
|
|
5575
6276
|
}
|
|
5576
6277
|
for (const file of sampledFiles) {
|
|
5577
|
-
const bytes = readFileSync9(
|
|
6278
|
+
const bytes = readFileSync9(join14(tempDir, file));
|
|
5578
6279
|
imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
|
|
5579
6280
|
}
|
|
5580
6281
|
} finally {
|
|
@@ -5592,42 +6293,92 @@ async function observeUiSnapshotViaLLM(db, input8) {
|
|
|
5592
6293
|
if (imageUrls.length === 0) {
|
|
5593
6294
|
throw new Error("No image data available for vision analysis");
|
|
5594
6295
|
}
|
|
5595
|
-
const
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
"Vision model returned output that could not be parsed as JSON."
|
|
5611
|
-
);
|
|
6296
|
+
const endpoints = [
|
|
6297
|
+
{
|
|
6298
|
+
url: cfg.url,
|
|
6299
|
+
apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
|
|
6300
|
+
model: input8.model ?? cfg.model,
|
|
6301
|
+
apiFlavor: cfg.apiFlavor
|
|
6302
|
+
}
|
|
6303
|
+
];
|
|
6304
|
+
if (cfg.fallback) {
|
|
6305
|
+
endpoints.push({
|
|
6306
|
+
url: cfg.fallback.url,
|
|
6307
|
+
apiKey: cfg.fallback.apiKey || DEFAULT_LLM_API_KEY,
|
|
6308
|
+
model: cfg.fallback.model,
|
|
6309
|
+
apiFlavor: cfg.fallback.apiFlavor
|
|
6310
|
+
});
|
|
5612
6311
|
}
|
|
5613
|
-
|
|
5614
|
-
|
|
6312
|
+
let lastRequestError;
|
|
6313
|
+
let sawUnparseableDraft = false;
|
|
6314
|
+
let sawInvalidDraft = false;
|
|
6315
|
+
for (const endpoint of endpoints) {
|
|
6316
|
+
let content;
|
|
6317
|
+
try {
|
|
6318
|
+
content = await requestVisionDraft({
|
|
6319
|
+
...endpoint,
|
|
6320
|
+
locale: cfg.locale,
|
|
6321
|
+
imageUrls,
|
|
6322
|
+
input: input8
|
|
6323
|
+
});
|
|
6324
|
+
} catch (err) {
|
|
6325
|
+
lastRequestError = err;
|
|
6326
|
+
continue;
|
|
6327
|
+
}
|
|
6328
|
+
let draft;
|
|
6329
|
+
try {
|
|
6330
|
+
draft = extractDraft(content);
|
|
6331
|
+
} catch {
|
|
6332
|
+
sawUnparseableDraft = true;
|
|
6333
|
+
continue;
|
|
6334
|
+
}
|
|
6335
|
+
const report = buildReport(input8, draft);
|
|
6336
|
+
if (isUiObservationReport(report)) {
|
|
6337
|
+
return report;
|
|
6338
|
+
}
|
|
6339
|
+
sawInvalidDraft = true;
|
|
6340
|
+
}
|
|
6341
|
+
if (lastRequestError && !sawUnparseableDraft && !sawInvalidDraft) {
|
|
6342
|
+
throw lastRequestError;
|
|
6343
|
+
}
|
|
6344
|
+
if (sawInvalidDraft) {
|
|
5615
6345
|
return uncertainReport(
|
|
5616
6346
|
input8,
|
|
5617
6347
|
"Vision model returned an invalid UI report draft."
|
|
5618
6348
|
);
|
|
5619
6349
|
}
|
|
5620
|
-
return
|
|
6350
|
+
return uncertainReport(
|
|
6351
|
+
input8,
|
|
6352
|
+
"Vision model returned output that could not be parsed as JSON."
|
|
6353
|
+
);
|
|
5621
6354
|
}
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
6355
|
+
var VISION_SYSTEM_PROMPT = "You are ZAM's UI observer. Return only strict JSON. Do not include markdown, prose, or fields outside the requested schema.";
|
|
6356
|
+
function visionSchema(language) {
|
|
6357
|
+
return `{
|
|
5625
6358
|
"kind": "progress | step-completed | error | help-seeking | uncertain",
|
|
5626
6359
|
"summary": "short factual UI summary in ${language}",
|
|
5627
6360
|
"actions": [{"type": "click | shortcut | typing | scroll | window-change", "target": "optional UI target", "result": "optional visible result"}],
|
|
5628
6361
|
"candidateTokens": [{"slug": "optional-kebab-case-skill-token", "confidence": 0.0, "rationale": "why this token may matter"}],
|
|
5629
6362
|
"confidence": 0.0
|
|
5630
6363
|
}`;
|
|
6364
|
+
}
|
|
6365
|
+
function visionUserText(args, language) {
|
|
6366
|
+
const intro = args.imageUrls.length > 1 ? "Observe this sequence of Windows/macOS application snapshots showing a task performed over time." : "Observe this Windows/macOS application snapshot for a learning session.";
|
|
6367
|
+
return `${intro}
|
|
6368
|
+
Application process: ${args.input.application.processName}
|
|
6369
|
+
Window title: ${args.input.application.windowTitle ?? "(unknown)"}
|
|
6370
|
+
|
|
6371
|
+
Return this JSON draft only:
|
|
6372
|
+
${visionSchema(language)}`;
|
|
6373
|
+
}
|
|
6374
|
+
async function requestVisionDraft(args) {
|
|
6375
|
+
if (args.apiFlavor === "anthropic-messages") {
|
|
6376
|
+
return requestAnthropicVisionDraft(args);
|
|
6377
|
+
}
|
|
6378
|
+
return requestChatCompletionsVisionDraft(args);
|
|
6379
|
+
}
|
|
6380
|
+
async function requestChatCompletionsVisionDraft(args) {
|
|
6381
|
+
const language = LANGUAGE_NAMES2[args.locale] ?? "English";
|
|
5631
6382
|
const res = await fetchWithInteractiveTimeout(
|
|
5632
6383
|
`${args.url}/chat/completions`,
|
|
5633
6384
|
{
|
|
@@ -5639,27 +6390,11 @@ async function requestVisionDraft(args) {
|
|
|
5639
6390
|
body: JSON.stringify({
|
|
5640
6391
|
model: args.model,
|
|
5641
6392
|
messages: [
|
|
5642
|
-
{
|
|
5643
|
-
role: "system",
|
|
5644
|
-
content: "You are ZAM's UI observer. Return only strict JSON. Do not include markdown, prose, or fields outside the requested schema."
|
|
5645
|
-
},
|
|
6393
|
+
{ role: "system", content: VISION_SYSTEM_PROMPT },
|
|
5646
6394
|
{
|
|
5647
6395
|
role: "user",
|
|
5648
6396
|
content: [
|
|
5649
|
-
{
|
|
5650
|
-
type: "text",
|
|
5651
|
-
text: args.imageUrls.length > 1 ? `Observe this sequence of Windows/macOS application snapshots showing a task performed over time.
|
|
5652
|
-
Application process: ${args.input.application.processName}
|
|
5653
|
-
Window title: ${args.input.application.windowTitle ?? "(unknown)"}
|
|
5654
|
-
|
|
5655
|
-
Return this JSON draft only:
|
|
5656
|
-
${schema}` : `Observe this Windows/macOS application snapshot for a learning session.
|
|
5657
|
-
Application process: ${args.input.application.processName}
|
|
5658
|
-
Window title: ${args.input.application.windowTitle ?? "(unknown)"}
|
|
5659
|
-
|
|
5660
|
-
Return this JSON draft only:
|
|
5661
|
-
${schema}`
|
|
5662
|
-
},
|
|
6397
|
+
{ type: "text", text: visionUserText(args, language) },
|
|
5663
6398
|
...args.imageUrls.map((url) => ({
|
|
5664
6399
|
type: "image_url",
|
|
5665
6400
|
image_url: { url }
|
|
@@ -5702,6 +6437,64 @@ ${schema}`
|
|
|
5702
6437
|
}
|
|
5703
6438
|
return content.trim();
|
|
5704
6439
|
}
|
|
6440
|
+
function dataUrlToAnthropicImage(dataUrl) {
|
|
6441
|
+
const match = dataUrl.match(/^data:(image\/[a-zA-Z]+);base64,(.+)$/);
|
|
6442
|
+
if (!match) {
|
|
6443
|
+
throw new Error("Unsupported image data for Anthropic vision request");
|
|
6444
|
+
}
|
|
6445
|
+
return {
|
|
6446
|
+
type: "image",
|
|
6447
|
+
source: { type: "base64", media_type: match[1], data: match[2] }
|
|
6448
|
+
};
|
|
6449
|
+
}
|
|
6450
|
+
async function requestAnthropicVisionDraft(args) {
|
|
6451
|
+
const language = LANGUAGE_NAMES2[args.locale] ?? "English";
|
|
6452
|
+
const base = args.url.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
6453
|
+
const res = await fetchWithInteractiveTimeout(`${base}/v1/messages`, {
|
|
6454
|
+
method: "POST",
|
|
6455
|
+
headers: {
|
|
6456
|
+
"Content-Type": "application/json",
|
|
6457
|
+
"x-api-key": args.apiKey,
|
|
6458
|
+
"anthropic-version": "2023-06-01"
|
|
6459
|
+
},
|
|
6460
|
+
body: JSON.stringify({
|
|
6461
|
+
model: args.model,
|
|
6462
|
+
max_tokens: args.input.maxTokens ?? 450,
|
|
6463
|
+
system: VISION_SYSTEM_PROMPT,
|
|
6464
|
+
messages: [
|
|
6465
|
+
{
|
|
6466
|
+
role: "user",
|
|
6467
|
+
content: [
|
|
6468
|
+
{ type: "text", text: visionUserText(args, language) },
|
|
6469
|
+
...args.imageUrls.map(dataUrlToAnthropicImage)
|
|
6470
|
+
]
|
|
6471
|
+
}
|
|
6472
|
+
]
|
|
6473
|
+
}),
|
|
6474
|
+
locale: args.locale,
|
|
6475
|
+
hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
|
|
6476
|
+
});
|
|
6477
|
+
if (!res.ok) {
|
|
6478
|
+
const errorText = await res.text().catch(() => "");
|
|
6479
|
+
throw new Error(
|
|
6480
|
+
`Vision LLM request failed: ${res.statusText} (${res.status}) - ${errorText}`
|
|
6481
|
+
);
|
|
6482
|
+
}
|
|
6483
|
+
const data = await res.json();
|
|
6484
|
+
if (data.error !== void 0) {
|
|
6485
|
+
throw new Error(`Vision model failed: ${formatModelError(data.error)}`);
|
|
6486
|
+
}
|
|
6487
|
+
if (data.stop_reason === "refusal") {
|
|
6488
|
+
throw new Error("Vision model refused the request (safety classifier).");
|
|
6489
|
+
}
|
|
6490
|
+
const text = data.content?.find(
|
|
6491
|
+
(b) => b.type === "text" && typeof b.text === "string"
|
|
6492
|
+
)?.text;
|
|
6493
|
+
if (!text) {
|
|
6494
|
+
throw new Error("Empty response from vision model");
|
|
6495
|
+
}
|
|
6496
|
+
return text.trim();
|
|
6497
|
+
}
|
|
5705
6498
|
function extractDraft(content) {
|
|
5706
6499
|
const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
5707
6500
|
const candidate = (fenced?.[1] ?? content).trim();
|
|
@@ -5730,7 +6523,7 @@ function buildReport(input8, draft) {
|
|
|
5730
6523
|
evidence: [
|
|
5731
6524
|
{
|
|
5732
6525
|
type: "keyframe",
|
|
5733
|
-
ref: input8.evidenceRef ??
|
|
6526
|
+
ref: input8.evidenceRef ?? basename3(input8.imagePath),
|
|
5734
6527
|
redacted: input8.redacted ?? false
|
|
5735
6528
|
}
|
|
5736
6529
|
],
|
|
@@ -5752,7 +6545,7 @@ function uncertainReport(input8, summary) {
|
|
|
5752
6545
|
evidence: [
|
|
5753
6546
|
{
|
|
5754
6547
|
type: "keyframe",
|
|
5755
|
-
ref: input8.evidenceRef ??
|
|
6548
|
+
ref: input8.evidenceRef ?? basename3(input8.imagePath),
|
|
5756
6549
|
redacted: input8.redacted ?? false
|
|
5757
6550
|
}
|
|
5758
6551
|
],
|
|
@@ -5862,50 +6655,728 @@ function jsonOut(data) {
|
|
|
5862
6655
|
console.log(JSON.stringify(data, null, 2));
|
|
5863
6656
|
}
|
|
5864
6657
|
|
|
5865
|
-
// src/cli/commands/
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
6658
|
+
// src/cli/commands/workspace.ts
|
|
6659
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
6660
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
6661
|
+
import { homedir as homedir9 } from "os";
|
|
6662
|
+
import { join as join16, resolve as resolve4 } from "path";
|
|
6663
|
+
import { confirm, input } from "@inquirer/prompts";
|
|
6664
|
+
import { Command as Command3 } from "commander";
|
|
6665
|
+
|
|
6666
|
+
// src/cli/commands/setup.ts
|
|
6667
|
+
import {
|
|
6668
|
+
copyFileSync as copyFileSync2,
|
|
6669
|
+
existsSync as existsSync15,
|
|
6670
|
+
mkdirSync as mkdirSync8,
|
|
6671
|
+
readFileSync as readFileSync10,
|
|
6672
|
+
writeFileSync as writeFileSync7
|
|
6673
|
+
} from "fs";
|
|
6674
|
+
import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
|
|
6675
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6676
|
+
import { Command as Command2 } from "commander";
|
|
6677
|
+
var packageRoot = [
|
|
6678
|
+
fileURLToPath2(new URL("../..", import.meta.url)),
|
|
6679
|
+
fileURLToPath2(new URL("../../..", import.meta.url))
|
|
6680
|
+
].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
|
|
6681
|
+
var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
|
|
6682
|
+
var SKILL_PAIRS = [
|
|
6683
|
+
{
|
|
6684
|
+
from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
|
|
6685
|
+
to: join15(".claude", "skills", "zam", "SKILL.md"),
|
|
6686
|
+
agents: ["claude"]
|
|
6687
|
+
},
|
|
6688
|
+
{
|
|
6689
|
+
from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
|
|
6690
|
+
to: join15(".agent", "skills", "zam", "SKILL.md"),
|
|
6691
|
+
agents: ["agent"]
|
|
6692
|
+
},
|
|
6693
|
+
{
|
|
6694
|
+
from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
|
|
6695
|
+
to: join15(".agents", "skills", "zam", "SKILL.md"),
|
|
6696
|
+
agents: ["copilot", "codex"]
|
|
6697
|
+
}
|
|
6698
|
+
];
|
|
6699
|
+
function parseSetupAgents(value) {
|
|
6700
|
+
if (!value || value.trim().toLowerCase() === "all") {
|
|
6701
|
+
return new Set(ALL_SETUP_AGENTS);
|
|
6702
|
+
}
|
|
6703
|
+
const aliases = {
|
|
6704
|
+
claude: ["claude"],
|
|
6705
|
+
copilot: ["copilot"],
|
|
6706
|
+
codex: ["codex"],
|
|
6707
|
+
agent: ["agent"],
|
|
6708
|
+
opencode: ["agent"]
|
|
6709
|
+
};
|
|
6710
|
+
const selected = /* @__PURE__ */ new Set();
|
|
6711
|
+
for (const raw of value.split(",")) {
|
|
6712
|
+
const key = raw.trim().toLowerCase();
|
|
6713
|
+
const mapped = aliases[key];
|
|
6714
|
+
if (!mapped) {
|
|
6715
|
+
throw new Error(
|
|
6716
|
+
`Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
|
|
6717
|
+
);
|
|
6718
|
+
}
|
|
6719
|
+
for (const item of mapped) selected.add(item);
|
|
6720
|
+
}
|
|
6721
|
+
return selected;
|
|
5869
6722
|
}
|
|
5870
|
-
function
|
|
5871
|
-
|
|
5872
|
-
|
|
6723
|
+
function copySkills(force, cwd = process.cwd(), agents = parseSetupAgents(), dryRun = false) {
|
|
6724
|
+
let anyAction = false;
|
|
6725
|
+
for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
|
|
6726
|
+
if (!pairAgents.some((agent) => agents.has(agent))) continue;
|
|
6727
|
+
const dest = join15(cwd, to);
|
|
6728
|
+
if (!existsSync15(from)) {
|
|
6729
|
+
console.warn(` warn source not found, skipping: ${from}`);
|
|
6730
|
+
continue;
|
|
6731
|
+
}
|
|
6732
|
+
if (existsSync15(dest) && !force) {
|
|
6733
|
+
console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
|
|
6734
|
+
continue;
|
|
6735
|
+
}
|
|
6736
|
+
if (dryRun) {
|
|
6737
|
+
console.log(` would copy ${to}`);
|
|
6738
|
+
anyAction = true;
|
|
6739
|
+
continue;
|
|
6740
|
+
}
|
|
6741
|
+
mkdirSync8(dirname5(dest), { recursive: true });
|
|
6742
|
+
copyFileSync2(from, dest);
|
|
6743
|
+
console.log(` copy ${to}`);
|
|
6744
|
+
anyAction = true;
|
|
6745
|
+
}
|
|
6746
|
+
if (!anyAction && !force) {
|
|
6747
|
+
console.log(
|
|
6748
|
+
"\nSkill files are already up to date. Run with --force to overwrite."
|
|
6749
|
+
);
|
|
5873
6750
|
}
|
|
5874
|
-
console.log(JSON.stringify({ error: message }, null, 2));
|
|
5875
|
-
process.exit(1);
|
|
5876
6751
|
}
|
|
5877
|
-
function
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
6752
|
+
function formatDatabaseInitTarget(target) {
|
|
6753
|
+
switch (target.kind) {
|
|
6754
|
+
case "local":
|
|
6755
|
+
return `ZAM database at ${target.location} (local SQLite)`;
|
|
6756
|
+
case "turso-remote":
|
|
6757
|
+
return `ZAM database via Turso remote at ${target.location}`;
|
|
6758
|
+
case "turso-native":
|
|
6759
|
+
return `ZAM database via Turso native driver at ${target.location}`;
|
|
6760
|
+
case "turso-replica":
|
|
6761
|
+
return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
|
|
5881
6762
|
}
|
|
5882
|
-
return parsed;
|
|
5883
6763
|
}
|
|
5884
|
-
async function
|
|
5885
|
-
|
|
6764
|
+
async function initDatabase(skipInit) {
|
|
6765
|
+
if (skipInit) return;
|
|
6766
|
+
try {
|
|
6767
|
+
const target = getDatabaseTargetInfo();
|
|
6768
|
+
const db = await openDatabaseWithSync({ initialize: true });
|
|
6769
|
+
await db.close();
|
|
6770
|
+
console.log(` init ${formatDatabaseInitTarget(target)}`);
|
|
6771
|
+
} catch (err) {
|
|
6772
|
+
const msg = err.message;
|
|
6773
|
+
if (!msg.includes("already")) {
|
|
6774
|
+
console.warn(` warn database init: ${msg}`);
|
|
6775
|
+
} else {
|
|
6776
|
+
console.log(` skip database already initialized`);
|
|
6777
|
+
}
|
|
6778
|
+
}
|
|
5886
6779
|
}
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
6780
|
+
var ZAM_BLOCK_START = "<!-- ZAM:START -->";
|
|
6781
|
+
var ZAM_BLOCK_END = "<!-- ZAM:END -->";
|
|
6782
|
+
function upsertMarkedBlock(dest, blockBody, dryRun) {
|
|
6783
|
+
const block = `${ZAM_BLOCK_START}
|
|
6784
|
+
${blockBody.trim()}
|
|
6785
|
+
${ZAM_BLOCK_END}`;
|
|
6786
|
+
if (!existsSync15(dest)) {
|
|
6787
|
+
if (!dryRun) {
|
|
6788
|
+
mkdirSync8(dirname5(dest), { recursive: true });
|
|
6789
|
+
writeFileSync7(dest, `${block}
|
|
6790
|
+
`, "utf8");
|
|
6791
|
+
}
|
|
6792
|
+
return "write";
|
|
5896
6793
|
}
|
|
5897
|
-
|
|
5898
|
-
|
|
6794
|
+
const existing = readFileSync10(dest, "utf8");
|
|
6795
|
+
if (existing.includes(block)) return "skip";
|
|
6796
|
+
const start = existing.indexOf(ZAM_BLOCK_START);
|
|
6797
|
+
const end = existing.indexOf(ZAM_BLOCK_END);
|
|
6798
|
+
const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
|
|
6799
|
+
|
|
6800
|
+
${block}
|
|
6801
|
+
`;
|
|
6802
|
+
if (!dryRun) writeFileSync7(dest, next, "utf8");
|
|
6803
|
+
return start >= 0 && end > start ? "update" : "write";
|
|
6804
|
+
}
|
|
6805
|
+
function logInstructionAction(action, label, dryRun) {
|
|
6806
|
+
if (action === "skip") {
|
|
6807
|
+
console.log(` skip ${label} (ZAM block already present)`);
|
|
6808
|
+
} else {
|
|
6809
|
+
console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
|
|
5899
6810
|
}
|
|
5900
|
-
return target;
|
|
5901
6811
|
}
|
|
5902
|
-
function
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
if (
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
6812
|
+
function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
|
|
6813
|
+
if (skipClaudeMd) return;
|
|
6814
|
+
const dest = join15(cwd, "CLAUDE.md");
|
|
6815
|
+
if (existsSync15(dest)) {
|
|
6816
|
+
if (!opts.updateExisting) {
|
|
6817
|
+
console.log(` skip CLAUDE.md (already present)`);
|
|
6818
|
+
return;
|
|
6819
|
+
}
|
|
6820
|
+
const action = upsertMarkedBlock(
|
|
6821
|
+
dest,
|
|
6822
|
+
`## ZAM learning sessions
|
|
6823
|
+
|
|
6824
|
+
ZAM is available in this repository. Use the \`zam\` skill in Claude Code to turn real work into an observed learning session with active recall and FSRS scheduling.
|
|
6825
|
+
|
|
6826
|
+
- Skill files live under \`.claude/skills/zam/\`.
|
|
6827
|
+
- Fast-changing review data lives in \`~/.zam/\`, not in this repository.
|
|
6828
|
+
- Run \`zam setup --target . --agents claude --force\` after upgrading ZAM to refresh the skill.`,
|
|
6829
|
+
Boolean(opts.dryRun)
|
|
6830
|
+
);
|
|
6831
|
+
logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
|
|
6832
|
+
return;
|
|
6833
|
+
}
|
|
6834
|
+
const name = basename4(cwd);
|
|
6835
|
+
const content = `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
|
|
6836
|
+
|
|
6837
|
+
This is a ZAM personal instance. ZAM builds lasting skills through spaced
|
|
6838
|
+
repetition during real work \xE2\u20AC\u201D not separate study sessions.
|
|
6839
|
+
|
|
6840
|
+
## First time here?
|
|
6841
|
+
Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
|
|
6842
|
+
|
|
6843
|
+
## Regular use
|
|
6844
|
+
Run \`/zam\` to start a learning session on whatever you are working on.
|
|
6845
|
+
|
|
6846
|
+
## What lives here
|
|
6847
|
+
- \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
|
|
6848
|
+
- \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
|
|
6849
|
+
|
|
6850
|
+
## Fast-changing data
|
|
6851
|
+
Learning tokens, cards, and review history live in local SQLite by default.
|
|
6852
|
+
Use \`zam connector setup turso\` to store cloud credentials in
|
|
6853
|
+
\`~/.zam/credentials.json\` and use a Turso database across machines.
|
|
6854
|
+
`;
|
|
6855
|
+
if (opts.dryRun) {
|
|
6856
|
+
console.log(` would write CLAUDE.md`);
|
|
6857
|
+
} else {
|
|
6858
|
+
writeFileSync7(dest, content, "utf8");
|
|
6859
|
+
console.log(` write CLAUDE.md`);
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
|
|
6863
|
+
if (skipAgentsMd) return;
|
|
6864
|
+
const dest = join15(cwd, "AGENTS.md");
|
|
6865
|
+
if (existsSync15(dest)) {
|
|
6866
|
+
if (!opts.updateExisting) {
|
|
6867
|
+
console.log(` skip AGENTS.md (already present)`);
|
|
6868
|
+
return;
|
|
6869
|
+
}
|
|
6870
|
+
const action = upsertMarkedBlock(
|
|
6871
|
+
dest,
|
|
6872
|
+
`## ZAM learning sessions
|
|
6873
|
+
|
|
6874
|
+
ZAM is available in this repository. Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` where supported to turn real work into an observed learning session with active recall and FSRS scheduling.
|
|
6875
|
+
|
|
6876
|
+
- Skill files live under \`.agents/skills/zam/\`.
|
|
6877
|
+
- Fast-changing review data lives in \`~/.zam/\`, not in this repository.
|
|
6878
|
+
- Run \`zam setup --target . --agents copilot,codex --force\` after upgrading ZAM to refresh the skill.`,
|
|
6879
|
+
Boolean(opts.dryRun)
|
|
6880
|
+
);
|
|
6881
|
+
logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
|
|
6882
|
+
return;
|
|
6883
|
+
}
|
|
6884
|
+
const name = basename4(cwd);
|
|
6885
|
+
const content = `# ZAM Personal Kernel - ${name}
|
|
6886
|
+
|
|
6887
|
+
This is a ZAM personal instance. ZAM builds lasting skills through spaced
|
|
6888
|
+
repetition during real work, not separate study sessions.
|
|
6889
|
+
|
|
6890
|
+
## First time here?
|
|
6891
|
+
Run \`zam setup\` from the shell. When this repository includes
|
|
6892
|
+
\`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
|
|
6893
|
+
or invoke \`$setup\`.
|
|
6894
|
+
|
|
6895
|
+
## Regular use
|
|
6896
|
+
Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
|
|
6897
|
+
learning session on whatever you are working on.
|
|
6898
|
+
|
|
6899
|
+
## What lives here
|
|
6900
|
+
- \`beliefs/\` - your worldview, approved by git commit
|
|
6901
|
+
- \`goals/\` - your objectives, decomposed into tasks and learning tokens
|
|
6902
|
+
|
|
6903
|
+
## Fast-changing data
|
|
6904
|
+
Learning tokens, cards, and review history live in local SQLite by default.
|
|
6905
|
+
Use \`zam connector setup turso\` to store cloud credentials in
|
|
6906
|
+
\`~/.zam/credentials.json\` and use a Turso database across machines.
|
|
6907
|
+
|
|
6908
|
+
## Codex skills
|
|
6909
|
+
Codex discovers repository skills under \`.agents/skills/\`. Run
|
|
6910
|
+
\`zam setup --force\` after upgrading \`zam-core\` to refresh them.
|
|
6911
|
+
`;
|
|
6912
|
+
if (opts.dryRun) {
|
|
6913
|
+
console.log(` would write AGENTS.md`);
|
|
6914
|
+
} else {
|
|
6915
|
+
writeFileSync7(dest, content, "utf8");
|
|
6916
|
+
console.log(` write AGENTS.md`);
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
|
|
6920
|
+
const dest = join15(cwd, ".github", "copilot-instructions.md");
|
|
6921
|
+
const action = upsertMarkedBlock(
|
|
6922
|
+
dest,
|
|
6923
|
+
`## ZAM learning sessions
|
|
6924
|
+
|
|
6925
|
+
ZAM is available in this repository through \`.agents/skills/zam/\`. Use the \`zam\` skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
|
|
6926
|
+
|
|
6927
|
+
- Fast-changing review data lives in \`~/.zam/\`, not in this repository.
|
|
6928
|
+
- Run \`zam setup --target . --agents copilot --force\` after upgrading ZAM to refresh the skill.`,
|
|
6929
|
+
Boolean(opts.dryRun)
|
|
6930
|
+
);
|
|
6931
|
+
logInstructionAction(
|
|
6932
|
+
action,
|
|
6933
|
+
".github/copilot-instructions.md",
|
|
6934
|
+
Boolean(opts.dryRun)
|
|
6935
|
+
);
|
|
6936
|
+
}
|
|
6937
|
+
var setupCommand = new Command2("setup").description(
|
|
6938
|
+
"Distribute ZAM skill files into this personal instance and initialize the database"
|
|
6939
|
+
).option(
|
|
6940
|
+
"--force",
|
|
6941
|
+
"overwrite existing skill files (use after upgrading zam)",
|
|
6942
|
+
false
|
|
6943
|
+
).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
|
|
6944
|
+
"--agents <list>",
|
|
6945
|
+
"comma-separated agents to wire: all, claude, copilot, codex, agent"
|
|
6946
|
+
).option(
|
|
6947
|
+
"--dry-run",
|
|
6948
|
+
"show what would be written without changing files",
|
|
6949
|
+
false
|
|
6950
|
+
).action(
|
|
6951
|
+
async (opts) => {
|
|
6952
|
+
let agents;
|
|
6953
|
+
try {
|
|
6954
|
+
agents = parseSetupAgents(opts.agents);
|
|
6955
|
+
} catch (err) {
|
|
6956
|
+
console.error(`Error: ${err.message}`);
|
|
6957
|
+
process.exit(1);
|
|
6958
|
+
}
|
|
6959
|
+
const target = resolve3(opts.target ?? process.cwd());
|
|
6960
|
+
const updateExistingInstructions = Boolean(opts.target) || opts.force;
|
|
6961
|
+
console.log(
|
|
6962
|
+
`Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
|
|
6963
|
+
`
|
|
6964
|
+
);
|
|
6965
|
+
copySkills(opts.force, target, agents, opts.dryRun);
|
|
6966
|
+
await initDatabase(opts.skipInit || opts.dryRun);
|
|
6967
|
+
if (agents.has("claude")) {
|
|
6968
|
+
writeClaudeMd(opts.skipClaudeMd, target, {
|
|
6969
|
+
dryRun: opts.dryRun,
|
|
6970
|
+
updateExisting: updateExistingInstructions
|
|
6971
|
+
});
|
|
6972
|
+
}
|
|
6973
|
+
if (agents.has("copilot") || agents.has("codex") || agents.has("agent")) {
|
|
6974
|
+
writeAgentsMd(opts.skipAgentsMd, target, {
|
|
6975
|
+
dryRun: opts.dryRun,
|
|
6976
|
+
updateExisting: updateExistingInstructions
|
|
6977
|
+
});
|
|
6978
|
+
}
|
|
6979
|
+
if (agents.has("copilot") && (opts.target || opts.agents)) {
|
|
6980
|
+
writeCopilotInstructions(target, {
|
|
6981
|
+
dryRun: opts.dryRun,
|
|
6982
|
+
updateExisting: updateExistingInstructions
|
|
6983
|
+
});
|
|
6984
|
+
}
|
|
6985
|
+
console.log(
|
|
6986
|
+
"\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
|
|
6987
|
+
);
|
|
6988
|
+
}
|
|
6989
|
+
);
|
|
6990
|
+
|
|
6991
|
+
// src/cli/commands/workspace.ts
|
|
6992
|
+
function runGit(cwd, args) {
|
|
6993
|
+
try {
|
|
6994
|
+
return execFileSync3("git", args, {
|
|
6995
|
+
cwd,
|
|
6996
|
+
stdio: "pipe",
|
|
6997
|
+
encoding: "utf8"
|
|
6998
|
+
}).trim();
|
|
6999
|
+
} catch (err) {
|
|
7000
|
+
throw new Error(`Git command failed: ${err.message}`);
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
7003
|
+
function ghRepoCreateArgs(repoName, repoVisibility) {
|
|
7004
|
+
return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
|
|
7005
|
+
}
|
|
7006
|
+
function gitRemoteArgs(githubUrl, hasOrigin) {
|
|
7007
|
+
return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
|
|
7008
|
+
}
|
|
7009
|
+
var workspaceCommand = new Command3("workspace").description(
|
|
7010
|
+
"Manage your ZAM learning workspace"
|
|
7011
|
+
);
|
|
7012
|
+
var WORKSPACE_KINDS = [
|
|
7013
|
+
"personal",
|
|
7014
|
+
"team",
|
|
7015
|
+
"family",
|
|
7016
|
+
"community",
|
|
7017
|
+
"organization",
|
|
7018
|
+
"custom"
|
|
7019
|
+
];
|
|
7020
|
+
var WORKSPACE_SOURCE_CONTROLS = [
|
|
7021
|
+
"github",
|
|
7022
|
+
"azure-devops",
|
|
7023
|
+
"git",
|
|
7024
|
+
"none"
|
|
7025
|
+
];
|
|
7026
|
+
function parseWorkspaceKind(value) {
|
|
7027
|
+
const kind = (value ?? "custom").toLowerCase();
|
|
7028
|
+
if (WORKSPACE_KINDS.includes(kind)) {
|
|
7029
|
+
return kind;
|
|
7030
|
+
}
|
|
7031
|
+
throw new Error(
|
|
7032
|
+
`Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
|
|
7033
|
+
);
|
|
7034
|
+
}
|
|
7035
|
+
function parseWorkspaceSourceControl(value) {
|
|
7036
|
+
if (!value) return void 0;
|
|
7037
|
+
const source = value.toLowerCase();
|
|
7038
|
+
if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
|
|
7039
|
+
return source;
|
|
7040
|
+
}
|
|
7041
|
+
throw new Error(
|
|
7042
|
+
`Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
|
|
7043
|
+
);
|
|
7044
|
+
}
|
|
7045
|
+
function parseScopes(value) {
|
|
7046
|
+
if (!value) return void 0;
|
|
7047
|
+
const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
7048
|
+
return scopes.length > 0 ? scopes : void 0;
|
|
7049
|
+
}
|
|
7050
|
+
function requireWorkspace(id) {
|
|
7051
|
+
const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
|
|
7052
|
+
if (!workspace) {
|
|
7053
|
+
throw new Error(
|
|
7054
|
+
`Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
|
|
7055
|
+
);
|
|
7056
|
+
}
|
|
7057
|
+
return workspace;
|
|
7058
|
+
}
|
|
7059
|
+
workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
|
|
7060
|
+
const workspaces = getConfiguredWorkspaces();
|
|
7061
|
+
if (opts.json) {
|
|
7062
|
+
console.log(JSON.stringify({ workspaces }, null, 2));
|
|
7063
|
+
return;
|
|
7064
|
+
}
|
|
7065
|
+
console.log("Configured ZAM workspaces:\n");
|
|
7066
|
+
if (workspaces.length === 0) {
|
|
7067
|
+
console.log(
|
|
7068
|
+
" (none) \u2014 add one: zam workspace add personal --path <dir>"
|
|
7069
|
+
);
|
|
7070
|
+
return;
|
|
7071
|
+
}
|
|
7072
|
+
for (const workspace of workspaces) {
|
|
7073
|
+
const label = workspace.label ? ` (${workspace.label})` : "";
|
|
7074
|
+
console.log(` ${workspace.id}${label}`);
|
|
7075
|
+
console.log(` kind: ${workspace.kind}`);
|
|
7076
|
+
console.log(` path: ${workspace.path}`);
|
|
7077
|
+
if (workspace.sourceControl) {
|
|
7078
|
+
console.log(` source: ${workspace.sourceControl}`);
|
|
7079
|
+
}
|
|
7080
|
+
if (workspace.knowledgeScopes?.length) {
|
|
7081
|
+
console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
});
|
|
7085
|
+
workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
|
|
7086
|
+
"--kind <kind>",
|
|
7087
|
+
`Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
|
|
7088
|
+
"custom"
|
|
7089
|
+
).option("--label <label>", "Human-readable label").option(
|
|
7090
|
+
"--source-control <provider>",
|
|
7091
|
+
`Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
|
|
7092
|
+
).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
|
|
7093
|
+
try {
|
|
7094
|
+
const path = resolve4(String(opts.path));
|
|
7095
|
+
if (!existsSync16(path)) {
|
|
7096
|
+
console.error(`Workspace path does not exist: ${path}`);
|
|
7097
|
+
process.exit(1);
|
|
7098
|
+
}
|
|
7099
|
+
const workspace = {
|
|
7100
|
+
id,
|
|
7101
|
+
kind: parseWorkspaceKind(opts.kind),
|
|
7102
|
+
path,
|
|
7103
|
+
...opts.label ? { label: opts.label } : {},
|
|
7104
|
+
...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
|
|
7105
|
+
...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
|
|
7106
|
+
...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
|
|
7107
|
+
};
|
|
7108
|
+
upsertConfiguredWorkspace(workspace);
|
|
7109
|
+
console.log(`Registered workspace "${id}" at ${path}.`);
|
|
7110
|
+
} catch (err) {
|
|
7111
|
+
console.error(`Error: ${err.message}`);
|
|
7112
|
+
process.exit(1);
|
|
7113
|
+
}
|
|
7114
|
+
});
|
|
7115
|
+
workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
|
|
7116
|
+
"--agents <list>",
|
|
7117
|
+
"comma-separated agents to wire: all, claude, copilot, codex, agent"
|
|
7118
|
+
).option(
|
|
7119
|
+
"--force",
|
|
7120
|
+
"overwrite existing skill files and refresh ZAM blocks",
|
|
7121
|
+
false
|
|
7122
|
+
).option(
|
|
7123
|
+
"--dry-run",
|
|
7124
|
+
"show what would be written without changing files",
|
|
7125
|
+
false
|
|
7126
|
+
).action((id, opts) => {
|
|
7127
|
+
try {
|
|
7128
|
+
const workspace = requireWorkspace(id);
|
|
7129
|
+
const agents = parseSetupAgents(opts.agents);
|
|
7130
|
+
console.log(
|
|
7131
|
+
`Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
|
|
7132
|
+
`
|
|
7133
|
+
);
|
|
7134
|
+
copySkills(
|
|
7135
|
+
Boolean(opts.force),
|
|
7136
|
+
workspace.path,
|
|
7137
|
+
agents,
|
|
7138
|
+
Boolean(opts.dryRun)
|
|
7139
|
+
);
|
|
7140
|
+
if (agents.has("claude")) {
|
|
7141
|
+
writeClaudeMd(false, workspace.path, {
|
|
7142
|
+
dryRun: Boolean(opts.dryRun),
|
|
7143
|
+
updateExisting: true
|
|
7144
|
+
});
|
|
7145
|
+
}
|
|
7146
|
+
if (agents.has("copilot") || agents.has("codex") || agents.has("agent")) {
|
|
7147
|
+
writeAgentsMd(false, workspace.path, {
|
|
7148
|
+
dryRun: Boolean(opts.dryRun),
|
|
7149
|
+
updateExisting: true
|
|
7150
|
+
});
|
|
7151
|
+
}
|
|
7152
|
+
if (agents.has("copilot")) {
|
|
7153
|
+
writeCopilotInstructions(workspace.path, {
|
|
7154
|
+
dryRun: Boolean(opts.dryRun),
|
|
7155
|
+
updateExisting: true
|
|
7156
|
+
});
|
|
7157
|
+
}
|
|
7158
|
+
} catch (err) {
|
|
7159
|
+
console.error(`Error: ${err.message}`);
|
|
7160
|
+
process.exit(1);
|
|
7161
|
+
}
|
|
7162
|
+
});
|
|
7163
|
+
workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
|
|
7164
|
+
let db;
|
|
7165
|
+
let workspaceDir = "";
|
|
7166
|
+
try {
|
|
7167
|
+
db = await openDatabase();
|
|
7168
|
+
workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
|
|
7169
|
+
await db.close();
|
|
7170
|
+
} catch {
|
|
7171
|
+
await db?.close();
|
|
7172
|
+
}
|
|
7173
|
+
if (!workspaceDir) {
|
|
7174
|
+
console.error(
|
|
7175
|
+
"\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
|
|
7176
|
+
);
|
|
7177
|
+
process.exit(1);
|
|
7178
|
+
}
|
|
7179
|
+
if (!existsSync16(workspaceDir)) {
|
|
7180
|
+
console.error(
|
|
7181
|
+
`\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
|
|
7182
|
+
);
|
|
7183
|
+
process.exit(1);
|
|
7184
|
+
}
|
|
7185
|
+
console.log(`
|
|
7186
|
+
Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
|
|
7187
|
+
if (!hasCommand("git")) {
|
|
7188
|
+
console.error(
|
|
7189
|
+
"\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
|
|
7190
|
+
);
|
|
7191
|
+
process.exit(1);
|
|
7192
|
+
}
|
|
7193
|
+
const gitignorePath = join16(workspaceDir, ".gitignore");
|
|
7194
|
+
if (!existsSync16(gitignorePath)) {
|
|
7195
|
+
writeFileSync8(
|
|
7196
|
+
gitignorePath,
|
|
7197
|
+
"node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
|
|
7198
|
+
"utf8"
|
|
7199
|
+
);
|
|
7200
|
+
}
|
|
7201
|
+
const hasGitRepo = existsSync16(join16(workspaceDir, ".git"));
|
|
7202
|
+
if (!hasGitRepo) {
|
|
7203
|
+
console.log("Initializing local Git repository...");
|
|
7204
|
+
runGit(workspaceDir, ["init", "-b", "main"]);
|
|
7205
|
+
runGit(workspaceDir, ["add", "."]);
|
|
7206
|
+
runGit(workspaceDir, [
|
|
7207
|
+
"commit",
|
|
7208
|
+
"-m",
|
|
7209
|
+
"chore: initial workspace sandbox bootstrap"
|
|
7210
|
+
]);
|
|
7211
|
+
console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
|
|
7212
|
+
} else {
|
|
7213
|
+
console.log("Git repository is already initialized.");
|
|
7214
|
+
}
|
|
7215
|
+
const repoName = await input({
|
|
7216
|
+
message: "Choose a name for your GitHub repository:",
|
|
7217
|
+
default: "zam-personal"
|
|
7218
|
+
});
|
|
7219
|
+
const isPrivate = await confirm({
|
|
7220
|
+
message: "Should the repository be private?",
|
|
7221
|
+
default: true
|
|
7222
|
+
});
|
|
7223
|
+
const repoVisibility = isPrivate ? "--private" : "--public";
|
|
7224
|
+
if (hasCommand("gh")) {
|
|
7225
|
+
console.log("GitHub CLI detected! Automating repository creation...");
|
|
7226
|
+
const proceedGh = await confirm({
|
|
7227
|
+
message: "Would you like ZAM to create the repository using the GitHub CLI?",
|
|
7228
|
+
default: true
|
|
7229
|
+
});
|
|
7230
|
+
if (proceedGh) {
|
|
7231
|
+
try {
|
|
7232
|
+
console.log(`Creating GitHub repository ${repoName}...`);
|
|
7233
|
+
execFileSync3("gh", ghRepoCreateArgs(repoName, repoVisibility), {
|
|
7234
|
+
cwd: workspaceDir,
|
|
7235
|
+
stdio: "inherit"
|
|
7236
|
+
});
|
|
7237
|
+
console.log(
|
|
7238
|
+
"\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
|
|
7239
|
+
);
|
|
7240
|
+
process.exit(0);
|
|
7241
|
+
} catch (err) {
|
|
7242
|
+
console.warn(
|
|
7243
|
+
`\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
|
|
7244
|
+
);
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
7247
|
+
}
|
|
7248
|
+
console.log(
|
|
7249
|
+
"\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
|
|
7250
|
+
);
|
|
7251
|
+
console.log(" 1. Go to https://github.com/new");
|
|
7252
|
+
console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
|
|
7253
|
+
console.log(
|
|
7254
|
+
` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
|
|
7255
|
+
);
|
|
7256
|
+
console.log(
|
|
7257
|
+
" 4. Do NOT initialize it with README, .gitignore, or license"
|
|
7258
|
+
);
|
|
7259
|
+
console.log(" 5. Click 'Create repository'\n");
|
|
7260
|
+
const githubUrl = await input({
|
|
7261
|
+
message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
|
|
7262
|
+
});
|
|
7263
|
+
if (githubUrl) {
|
|
7264
|
+
try {
|
|
7265
|
+
console.log("Linking remote repository and pushing...");
|
|
7266
|
+
let hasOrigin = false;
|
|
7267
|
+
try {
|
|
7268
|
+
runGit(workspaceDir, ["remote", "get-url", "origin"]);
|
|
7269
|
+
hasOrigin = true;
|
|
7270
|
+
} catch {
|
|
7271
|
+
}
|
|
7272
|
+
runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
|
|
7273
|
+
runGit(workspaceDir, ["push", "-u", "origin", "main"]);
|
|
7274
|
+
console.log(
|
|
7275
|
+
"\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
|
|
7276
|
+
);
|
|
7277
|
+
} catch (err) {
|
|
7278
|
+
console.error(
|
|
7279
|
+
`\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
|
|
7280
|
+
);
|
|
7281
|
+
console.log(
|
|
7282
|
+
"You can push manually later using: git push -u origin main"
|
|
7283
|
+
);
|
|
7284
|
+
}
|
|
7285
|
+
}
|
|
7286
|
+
});
|
|
7287
|
+
async function backupDatabaseTo(db, targetDir) {
|
|
7288
|
+
const backupDir = join16(targetDir, "zam-backups");
|
|
7289
|
+
mkdirSync9(backupDir, { recursive: true });
|
|
7290
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7291
|
+
const dest = join16(backupDir, `zam-${stamp}.db`);
|
|
7292
|
+
await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
7293
|
+
return dest;
|
|
7294
|
+
}
|
|
7295
|
+
workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
|
|
7296
|
+
const dir = join16(homedir9(), ".zam");
|
|
7297
|
+
console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
|
|
7298
|
+
});
|
|
7299
|
+
workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
|
|
7300
|
+
"--dir <path>",
|
|
7301
|
+
"Target directory (default: workspace dir, else ~/Documents/zam)"
|
|
7302
|
+
).option("--json", "Output as JSON").action(async (opts) => {
|
|
7303
|
+
const target = getDatabaseTargetInfo();
|
|
7304
|
+
if (target.kind !== "local") {
|
|
7305
|
+
const reason = `The database is ${target.kind} (${target.location}); file backup applies only to a local database \u2014 your Turso remote is already the cloud backup.`;
|
|
7306
|
+
if (opts.json) {
|
|
7307
|
+
console.log(JSON.stringify({ ok: false, reason }));
|
|
7308
|
+
return;
|
|
7309
|
+
}
|
|
7310
|
+
console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
|
|
7311
|
+
process.exit(1);
|
|
7312
|
+
}
|
|
7313
|
+
let db;
|
|
7314
|
+
try {
|
|
7315
|
+
db = await openDatabase();
|
|
7316
|
+
const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir9(), "Documents", "zam");
|
|
7317
|
+
const dest = await backupDatabaseTo(db, workspaceDir);
|
|
7318
|
+
if (opts.json) {
|
|
7319
|
+
console.log(JSON.stringify({ ok: true, path: dest }));
|
|
7320
|
+
} else {
|
|
7321
|
+
console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
|
|
7322
|
+
}
|
|
7323
|
+
} catch (err) {
|
|
7324
|
+
const reason = err.message;
|
|
7325
|
+
if (opts.json) {
|
|
7326
|
+
console.log(JSON.stringify({ ok: false, reason }));
|
|
7327
|
+
} else {
|
|
7328
|
+
console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
|
|
7329
|
+
}
|
|
7330
|
+
process.exit(1);
|
|
7331
|
+
} finally {
|
|
7332
|
+
await db?.close();
|
|
7333
|
+
}
|
|
7334
|
+
});
|
|
7335
|
+
|
|
7336
|
+
// src/cli/commands/bridge.ts
|
|
7337
|
+
var isServeMode = false;
|
|
7338
|
+
function jsonOut2(data) {
|
|
7339
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7340
|
+
}
|
|
7341
|
+
function jsonError(message) {
|
|
7342
|
+
if (isServeMode) {
|
|
7343
|
+
throw new Error(JSON.stringify({ error: message }));
|
|
7344
|
+
}
|
|
7345
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
7346
|
+
process.exit(1);
|
|
7347
|
+
}
|
|
7348
|
+
function parseNonNegativeIntegerOption(name, value) {
|
|
7349
|
+
const parsed = Number(value);
|
|
7350
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
7351
|
+
jsonError(`${name} must be a non-negative integer`);
|
|
7352
|
+
}
|
|
7353
|
+
return parsed;
|
|
7354
|
+
}
|
|
7355
|
+
async function withDb2(fn) {
|
|
7356
|
+
await withDb(fn, jsonError);
|
|
7357
|
+
}
|
|
7358
|
+
async function getReviewTarget2(db, cardId, userId) {
|
|
7359
|
+
const target = await db.prepare(
|
|
7360
|
+
`SELECT c.id AS card_id, c.token_id, c.user_id, t.slug
|
|
7361
|
+
FROM cards c
|
|
7362
|
+
JOIN tokens t ON t.id = c.token_id
|
|
7363
|
+
WHERE c.id = ?`
|
|
7364
|
+
).get(cardId);
|
|
7365
|
+
if (!target) {
|
|
7366
|
+
jsonError(`Card not found: ${cardId}`);
|
|
7367
|
+
}
|
|
7368
|
+
if (target.user_id !== userId) {
|
|
7369
|
+
jsonError(`Card ${cardId} does not belong to user ${userId}`);
|
|
7370
|
+
}
|
|
7371
|
+
return target;
|
|
7372
|
+
}
|
|
7373
|
+
function parseTokenUpdates(opts) {
|
|
7374
|
+
const updates = {};
|
|
7375
|
+
if (opts.concept !== void 0) updates.concept = opts.concept;
|
|
7376
|
+
if (opts.domain !== void 0) updates.domain = opts.domain;
|
|
7377
|
+
if (opts.bloom !== void 0)
|
|
7378
|
+
updates.bloom_level = Number(opts.bloom);
|
|
7379
|
+
if (opts.context !== void 0) updates.context = opts.context;
|
|
5909
7380
|
if (opts.sourceLink !== void 0) {
|
|
5910
7381
|
updates.source_link = opts.sourceLink === "" ? null : opts.sourceLink;
|
|
5911
7382
|
}
|
|
@@ -5918,7 +7389,7 @@ function parseTokenUpdates(opts) {
|
|
|
5918
7389
|
}
|
|
5919
7390
|
return updates;
|
|
5920
7391
|
}
|
|
5921
|
-
var bridgeCommand = new
|
|
7392
|
+
var bridgeCommand = new Command4("bridge").description(
|
|
5922
7393
|
"Machine-readable JSON protocol for AI integration"
|
|
5923
7394
|
);
|
|
5924
7395
|
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) => {
|
|
@@ -5946,6 +7417,160 @@ bridgeCommand.command("check-due").description("Check due cards for a user (JSON
|
|
|
5946
7417
|
});
|
|
5947
7418
|
});
|
|
5948
7419
|
});
|
|
7420
|
+
bridgeCommand.command("backup-db").description("Back up the local database into the workspace (JSON)").option(
|
|
7421
|
+
"--dir <path>",
|
|
7422
|
+
"Target directory (default: workspace dir, else ~/Documents/zam)"
|
|
7423
|
+
).action(async (opts) => {
|
|
7424
|
+
const target = getDatabaseTargetInfo();
|
|
7425
|
+
if (target.kind !== "local") {
|
|
7426
|
+
jsonOut2({
|
|
7427
|
+
ok: false,
|
|
7428
|
+
reason: "remote",
|
|
7429
|
+
target: target.kind,
|
|
7430
|
+
location: target.location
|
|
7431
|
+
});
|
|
7432
|
+
return;
|
|
7433
|
+
}
|
|
7434
|
+
await withDb2(async (db) => {
|
|
7435
|
+
const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
|
|
7436
|
+
const path = await backupDatabaseTo(db, workspaceDir);
|
|
7437
|
+
jsonOut2({ ok: true, path });
|
|
7438
|
+
});
|
|
7439
|
+
});
|
|
7440
|
+
bridgeCommand.command("workspace-info").description("Report the workspace dir, its default, and the data dir (JSON)").action(async () => {
|
|
7441
|
+
await withDb2(async (db) => {
|
|
7442
|
+
const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
|
|
7443
|
+
jsonOut2({
|
|
7444
|
+
workspaceDir,
|
|
7445
|
+
defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
|
|
7446
|
+
dataDir: join17(homedir10(), ".zam")
|
|
7447
|
+
});
|
|
7448
|
+
});
|
|
7449
|
+
});
|
|
7450
|
+
bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
|
|
7451
|
+
await withDb2(async (db) => {
|
|
7452
|
+
const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
|
|
7453
|
+
jsonOut2({
|
|
7454
|
+
workspaces: getConfiguredWorkspaces(),
|
|
7455
|
+
workspaceDir,
|
|
7456
|
+
defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
|
|
7457
|
+
dataDir: join17(homedir10(), ".zam")
|
|
7458
|
+
});
|
|
7459
|
+
});
|
|
7460
|
+
});
|
|
7461
|
+
function workspaceIdFromPath(dir) {
|
|
7462
|
+
const base = basename5(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7463
|
+
const prefix = base || "workspace";
|
|
7464
|
+
const existing = new Set(getConfiguredWorkspaces().map((item) => item.id));
|
|
7465
|
+
if (!existing.has(prefix)) return prefix;
|
|
7466
|
+
let index = 2;
|
|
7467
|
+
while (existing.has(`${prefix}-${index}`)) index++;
|
|
7468
|
+
return `${prefix}-${index}`;
|
|
7469
|
+
}
|
|
7470
|
+
function parseBridgeWorkspaceKind(value) {
|
|
7471
|
+
const kind = (value || "custom").toLowerCase();
|
|
7472
|
+
if (kind === "personal" || kind === "team" || kind === "family" || kind === "community" || kind === "organization" || kind === "custom") {
|
|
7473
|
+
return kind;
|
|
7474
|
+
}
|
|
7475
|
+
jsonError(`Invalid workspace kind: ${value}`);
|
|
7476
|
+
}
|
|
7477
|
+
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) => {
|
|
7478
|
+
const raw = String(opts.path ?? "").trim();
|
|
7479
|
+
if (!raw) jsonError("A non-empty --path is required");
|
|
7480
|
+
const path = resolve5(raw);
|
|
7481
|
+
if (!existsSync17(path)) jsonError(`Workspace path does not exist: ${path}`);
|
|
7482
|
+
const id = String(opts.id || workspaceIdFromPath(path)).trim();
|
|
7483
|
+
if (!id) jsonError("A non-empty --id is required");
|
|
7484
|
+
const workspace = {
|
|
7485
|
+
id,
|
|
7486
|
+
label: opts.label || basename5(path) || id,
|
|
7487
|
+
kind: parseBridgeWorkspaceKind(opts.kind),
|
|
7488
|
+
path
|
|
7489
|
+
};
|
|
7490
|
+
await withDb2(async (db) => {
|
|
7491
|
+
await setSetting(db, "personal.workspace_dir", path);
|
|
7492
|
+
upsertConfiguredWorkspace(workspace);
|
|
7493
|
+
jsonOut2({
|
|
7494
|
+
ok: true,
|
|
7495
|
+
workspace,
|
|
7496
|
+
workspaces: getConfiguredWorkspaces(),
|
|
7497
|
+
workspaceDir: path
|
|
7498
|
+
});
|
|
7499
|
+
});
|
|
7500
|
+
});
|
|
7501
|
+
bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
|
|
7502
|
+
const raw = String(opts.dir ?? "").trim();
|
|
7503
|
+
if (!raw) jsonError("A non-empty --dir is required");
|
|
7504
|
+
const dir = resolve5(raw);
|
|
7505
|
+
await withDb2(async (db) => {
|
|
7506
|
+
await setSetting(db, "personal.workspace_dir", dir);
|
|
7507
|
+
jsonOut2({ ok: true, workspaceDir: dir });
|
|
7508
|
+
});
|
|
7509
|
+
});
|
|
7510
|
+
bridgeCommand.command("agent-list").description("List agent harnesses with detection state + the default (JSON)").action(async () => {
|
|
7511
|
+
await withDb2(async (db) => {
|
|
7512
|
+
const configuredDefault = await getSetting(db, "agent.default") || null;
|
|
7513
|
+
const harnesses = await Promise.all(
|
|
7514
|
+
AGENT_HARNESSES.map(async (h) => {
|
|
7515
|
+
const override = await getSetting(db, `agent.${h.id}.command`) || void 0;
|
|
7516
|
+
return {
|
|
7517
|
+
id: h.id,
|
|
7518
|
+
label: h.label,
|
|
7519
|
+
kind: h.kind,
|
|
7520
|
+
detected: resolveHarnessExecutable(h, override) !== null
|
|
7521
|
+
};
|
|
7522
|
+
})
|
|
7523
|
+
);
|
|
7524
|
+
const fallbackDefault = harnesses.find((h) => h.detected)?.id ?? null;
|
|
7525
|
+
jsonOut2({ harnesses, default: configuredDefault ?? fallbackDefault });
|
|
7526
|
+
});
|
|
7527
|
+
});
|
|
7528
|
+
bridgeCommand.command("agent-open").description("Launch an agent harness in the workspace (JSON)").option(
|
|
7529
|
+
"--id <id>",
|
|
7530
|
+
"Harness id (default: agent.default setting, else first detected)"
|
|
7531
|
+
).option("--workspace <id>", "Configured workspace id to open").option("--dir <path>", "Explicit workspace directory to open").action(async (opts) => {
|
|
7532
|
+
await withDb2(async (db) => {
|
|
7533
|
+
let id = opts.id || await getSetting(db, "agent.default") || void 0;
|
|
7534
|
+
if (!id) {
|
|
7535
|
+
id = AGENT_HARNESSES.find((h) => resolveHarnessExecutable(h))?.id;
|
|
7536
|
+
}
|
|
7537
|
+
if (!id) {
|
|
7538
|
+
jsonError(
|
|
7539
|
+
"No agent harness configured or detected. Install one (Claude Code, Codex, opencode) or set agent.default."
|
|
7540
|
+
);
|
|
7541
|
+
}
|
|
7542
|
+
const harness = getHarness(id);
|
|
7543
|
+
if (!harness) {
|
|
7544
|
+
jsonError(`Unknown harness: ${id}`);
|
|
7545
|
+
}
|
|
7546
|
+
const override = await getSetting(db, `agent.${harness.id}.command`) || void 0;
|
|
7547
|
+
const executable = resolveHarnessExecutable(harness, override);
|
|
7548
|
+
if (!executable) {
|
|
7549
|
+
jsonError(
|
|
7550
|
+
`${harness.label} was not detected. Set its path: zam settings set agent.${harness.id}.command <path>`
|
|
7551
|
+
);
|
|
7552
|
+
}
|
|
7553
|
+
const configuredWorkspace = opts.workspace ? getConfiguredWorkspaces().find((item) => item.id === opts.workspace) : void 0;
|
|
7554
|
+
if (opts.workspace && !configuredWorkspace) {
|
|
7555
|
+
jsonError(`Workspace is not configured: ${opts.workspace}`);
|
|
7556
|
+
}
|
|
7557
|
+
let workspace = opts.dir || configuredWorkspace?.path || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
|
|
7558
|
+
if (!existsSync17(workspace)) workspace = homedir10();
|
|
7559
|
+
launchHarness(harness, {
|
|
7560
|
+
executable,
|
|
7561
|
+
workspace,
|
|
7562
|
+
shell: normalizeShell(void 0),
|
|
7563
|
+
silent: true
|
|
7564
|
+
});
|
|
7565
|
+
jsonOut2({
|
|
7566
|
+
ok: true,
|
|
7567
|
+
id: harness.id,
|
|
7568
|
+
label: harness.label,
|
|
7569
|
+
kind: harness.kind,
|
|
7570
|
+
workspace
|
|
7571
|
+
});
|
|
7572
|
+
});
|
|
7573
|
+
});
|
|
5949
7574
|
bridgeCommand.command("get-review").description("Get next review card with prompt (JSON)").option("--user <id>", "User ID (default: whoami)").option("--no-resolve", "Skip resolving the token's source_link into context").action(async (opts) => {
|
|
5950
7575
|
await withDb2(async (db) => {
|
|
5951
7576
|
const userId = await resolveUser(opts, db, { json: true });
|
|
@@ -6304,7 +7929,7 @@ bridgeCommand.command("discover-skills").description(
|
|
|
6304
7929
|
"20"
|
|
6305
7930
|
).action(async (opts) => {
|
|
6306
7931
|
try {
|
|
6307
|
-
const monitorDir =
|
|
7932
|
+
const monitorDir = join17(homedir10(), ".zam", "monitor");
|
|
6308
7933
|
let files;
|
|
6309
7934
|
try {
|
|
6310
7935
|
files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -6317,7 +7942,7 @@ bridgeCommand.command("discover-skills").description(
|
|
|
6317
7942
|
return;
|
|
6318
7943
|
}
|
|
6319
7944
|
const limit = Number(opts.limit);
|
|
6320
|
-
const sorted = files.map((f) => ({ name: f, path:
|
|
7945
|
+
const sorted = files.map((f) => ({ name: f, path: join17(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
|
|
6321
7946
|
const sessionCommands = /* @__PURE__ */ new Map();
|
|
6322
7947
|
for (const file of sorted) {
|
|
6323
7948
|
const sessionId = file.name.replace(".jsonl", "");
|
|
@@ -6429,7 +8054,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
|
|
|
6429
8054
|
});
|
|
6430
8055
|
function resolveWindowsPowerShell() {
|
|
6431
8056
|
try {
|
|
6432
|
-
|
|
8057
|
+
execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
|
|
6433
8058
|
return "pwsh";
|
|
6434
8059
|
} catch {
|
|
6435
8060
|
return "powershell";
|
|
@@ -6444,7 +8069,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
|
|
|
6444
8069
|
throw new Error(`Invalid process name format: ${processName}`);
|
|
6445
8070
|
}
|
|
6446
8071
|
if (platform === "win32") {
|
|
6447
|
-
const stdout =
|
|
8072
|
+
const stdout = execFileSync4(
|
|
6448
8073
|
resolveWindowsPowerShell(),
|
|
6449
8074
|
[
|
|
6450
8075
|
"-NoProfile",
|
|
@@ -6757,13 +8382,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
|
|
|
6757
8382
|
} else if (platform === "darwin") {
|
|
6758
8383
|
if (hwnd) {
|
|
6759
8384
|
const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
|
|
6760
|
-
|
|
8385
|
+
execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
|
|
6761
8386
|
stdio: "pipe"
|
|
6762
8387
|
});
|
|
6763
8388
|
return { method: "screencapture-window", target: null };
|
|
6764
8389
|
} else if (processName) {
|
|
6765
8390
|
try {
|
|
6766
|
-
const windowId =
|
|
8391
|
+
const windowId = execFileSync4(
|
|
6767
8392
|
"osascript",
|
|
6768
8393
|
[
|
|
6769
8394
|
"-e",
|
|
@@ -6772,17 +8397,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
|
|
|
6772
8397
|
{ encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
|
|
6773
8398
|
).trim();
|
|
6774
8399
|
if (windowId && /^\d+$/.test(windowId)) {
|
|
6775
|
-
|
|
8400
|
+
execFileSync4("screencapture", ["-l", windowId, outputPath], {
|
|
6776
8401
|
stdio: "pipe"
|
|
6777
8402
|
});
|
|
6778
8403
|
return { method: "screencapture-window", target: null };
|
|
6779
8404
|
}
|
|
6780
8405
|
} catch {
|
|
6781
8406
|
}
|
|
6782
|
-
|
|
8407
|
+
execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
|
|
6783
8408
|
return { method: "screencapture-full", target: null };
|
|
6784
8409
|
} else {
|
|
6785
|
-
|
|
8410
|
+
execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
|
|
6786
8411
|
return { method: "screencapture-full", target: null };
|
|
6787
8412
|
}
|
|
6788
8413
|
} else {
|
|
@@ -6819,7 +8444,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
|
|
|
6819
8444
|
return;
|
|
6820
8445
|
}
|
|
6821
8446
|
}
|
|
6822
|
-
const outputPath = opts.image ?? opts.output ??
|
|
8447
|
+
const outputPath = opts.image ?? opts.output ?? join17(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
|
|
6823
8448
|
const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
|
|
6824
8449
|
if (!isProvided) {
|
|
6825
8450
|
const post = decidePostCapture(policy, {
|
|
@@ -6847,7 +8472,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
|
|
|
6847
8472
|
return;
|
|
6848
8473
|
}
|
|
6849
8474
|
}
|
|
6850
|
-
const imageBytes =
|
|
8475
|
+
const imageBytes = readFileSync11(outputPath);
|
|
6851
8476
|
const base64 = imageBytes.toString("base64");
|
|
6852
8477
|
jsonOut2({
|
|
6853
8478
|
sessionId: opts.session ?? null,
|
|
@@ -6863,21 +8488,22 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
|
|
|
6863
8488
|
});
|
|
6864
8489
|
});
|
|
6865
8490
|
});
|
|
6866
|
-
bridgeCommand.command("start-recording").description("Start screen recording in the background (
|
|
8491
|
+
bridgeCommand.command("start-recording").description("Start screen recording in the background (JSON)").requiredOption("--session <id>", "ZAM session ID").option("--output <path>", "Video output path").action(async (opts) => {
|
|
6867
8492
|
const platform = process.platform;
|
|
6868
|
-
if (platform !== "darwin") {
|
|
8493
|
+
if (platform !== "darwin" && platform !== "win32") {
|
|
6869
8494
|
jsonOut2({
|
|
6870
8495
|
sessionId: opts.session,
|
|
6871
8496
|
started: false,
|
|
6872
|
-
error: "Screen recording is only supported on macOS (darwin)"
|
|
8497
|
+
error: "Screen recording is only supported on macOS (darwin) and Windows (win32)"
|
|
6873
8498
|
});
|
|
6874
8499
|
return;
|
|
6875
8500
|
}
|
|
6876
8501
|
const sessionId = opts.session;
|
|
6877
|
-
const statePath =
|
|
6878
|
-
const
|
|
6879
|
-
const
|
|
6880
|
-
|
|
8502
|
+
const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
|
|
8503
|
+
const defaultExt = platform === "win32" ? ".mkv" : ".mov";
|
|
8504
|
+
const outputPath = opts.output ?? join17(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
|
|
8505
|
+
const { existsSync: existsSync25, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
|
|
8506
|
+
if (existsSync25(statePath)) {
|
|
6881
8507
|
jsonOut2({
|
|
6882
8508
|
sessionId,
|
|
6883
8509
|
started: false,
|
|
@@ -6885,7 +8511,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
6885
8511
|
});
|
|
6886
8512
|
return;
|
|
6887
8513
|
}
|
|
6888
|
-
const logPath =
|
|
8514
|
+
const logPath = join17(tmpdir3(), `zam-recording-${sessionId}.log`);
|
|
6889
8515
|
let logFd;
|
|
6890
8516
|
try {
|
|
6891
8517
|
logFd = openSync(logPath, "w");
|
|
@@ -6897,26 +8523,34 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
6897
8523
|
});
|
|
6898
8524
|
return;
|
|
6899
8525
|
}
|
|
6900
|
-
const { spawn:
|
|
6901
|
-
const
|
|
6902
|
-
"
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
8526
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
8527
|
+
const ffmpegArgs = platform === "darwin" ? [
|
|
8528
|
+
"-y",
|
|
8529
|
+
"-f",
|
|
8530
|
+
"avfoundation",
|
|
8531
|
+
"-r",
|
|
8532
|
+
"5",
|
|
8533
|
+
"-i",
|
|
8534
|
+
"0",
|
|
8535
|
+
"-pix_fmt",
|
|
8536
|
+
"yuv420p",
|
|
8537
|
+
outputPath
|
|
8538
|
+
] : [
|
|
8539
|
+
"-y",
|
|
8540
|
+
"-f",
|
|
8541
|
+
"gdigrab",
|
|
8542
|
+
"-framerate",
|
|
8543
|
+
"5",
|
|
8544
|
+
"-i",
|
|
8545
|
+
"desktop",
|
|
8546
|
+
"-pix_fmt",
|
|
8547
|
+
"yuv420p",
|
|
8548
|
+
outputPath
|
|
8549
|
+
];
|
|
8550
|
+
const child = spawn4("ffmpeg", ffmpegArgs, {
|
|
8551
|
+
detached: true,
|
|
8552
|
+
stdio: ["pipe", logFd, logFd]
|
|
8553
|
+
});
|
|
6920
8554
|
try {
|
|
6921
8555
|
closeSync(logFd);
|
|
6922
8556
|
} catch {
|
|
@@ -6947,21 +8581,21 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
6947
8581
|
}
|
|
6948
8582
|
});
|
|
6949
8583
|
bridgeCommand.command("stop-recording").description(
|
|
6950
|
-
"Stop active screen recording and apply idle-frame compression (
|
|
8584
|
+
"Stop active screen recording and apply idle-frame compression (JSON)"
|
|
6951
8585
|
).requiredOption("--session <id>", "ZAM session ID").action(async (opts) => {
|
|
6952
8586
|
const platform = process.platform;
|
|
6953
|
-
if (platform !== "darwin") {
|
|
8587
|
+
if (platform !== "darwin" && platform !== "win32") {
|
|
6954
8588
|
jsonOut2({
|
|
6955
8589
|
sessionId: opts.session,
|
|
6956
8590
|
stopped: false,
|
|
6957
|
-
error: "Screen recording is only supported on macOS (darwin)"
|
|
8591
|
+
error: "Screen recording is only supported on macOS (darwin) and Windows (win32)"
|
|
6958
8592
|
});
|
|
6959
8593
|
return;
|
|
6960
8594
|
}
|
|
6961
8595
|
const sessionId = opts.session;
|
|
6962
|
-
const statePath =
|
|
6963
|
-
const { existsSync:
|
|
6964
|
-
if (!
|
|
8596
|
+
const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
|
|
8597
|
+
const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync3 } = await import("fs");
|
|
8598
|
+
if (!existsSync25(statePath)) {
|
|
6965
8599
|
jsonOut2({
|
|
6966
8600
|
sessionId,
|
|
6967
8601
|
stopped: false,
|
|
@@ -6969,7 +8603,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
6969
8603
|
});
|
|
6970
8604
|
return;
|
|
6971
8605
|
}
|
|
6972
|
-
const state = JSON.parse(
|
|
8606
|
+
const state = JSON.parse(readFileSync16(statePath, "utf8"));
|
|
6973
8607
|
const { pid, outputPath } = state;
|
|
6974
8608
|
try {
|
|
6975
8609
|
process.kill(pid, "SIGINT");
|
|
@@ -6985,7 +8619,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
6985
8619
|
};
|
|
6986
8620
|
let attempts = 0;
|
|
6987
8621
|
while (isProcessRunning(pid) && attempts < 20) {
|
|
6988
|
-
await new Promise((
|
|
8622
|
+
await new Promise((resolve8) => setTimeout(resolve8, 250));
|
|
6989
8623
|
attempts++;
|
|
6990
8624
|
}
|
|
6991
8625
|
if (isProcessRunning(pid)) {
|
|
@@ -6998,7 +8632,7 @@ bridgeCommand.command("stop-recording").description(
|
|
|
6998
8632
|
rmSync3(statePath, { force: true });
|
|
6999
8633
|
} catch {
|
|
7000
8634
|
}
|
|
7001
|
-
if (!
|
|
8635
|
+
if (!existsSync25(outputPath)) {
|
|
7002
8636
|
jsonOut2({
|
|
7003
8637
|
sessionId,
|
|
7004
8638
|
stopped: false,
|
|
@@ -7007,9 +8641,9 @@ bridgeCommand.command("stop-recording").description(
|
|
|
7007
8641
|
return;
|
|
7008
8642
|
}
|
|
7009
8643
|
const decimatedPath = outputPath.replace(/\.[^.]+$/, "-decimated.mp4");
|
|
7010
|
-
const { execSync:
|
|
8644
|
+
const { execSync: execSync6 } = await import("child_process");
|
|
7011
8645
|
try {
|
|
7012
|
-
|
|
8646
|
+
execSync6(
|
|
7013
8647
|
`ffmpeg -y -i "${outputPath}" -vf "mpdecimate,setpts=N/FRAME_RATE/TB" -an -pix_fmt yuv420p "${decimatedPath}"`,
|
|
7014
8648
|
{ stdio: "ignore" }
|
|
7015
8649
|
);
|
|
@@ -7062,11 +8696,13 @@ bridgeCommand.command("sync-observer-policy").description(
|
|
|
7062
8696
|
});
|
|
7063
8697
|
bridgeCommand.command("check-llm").description("Check if LLM is enabled and online (JSON)").action(async () => {
|
|
7064
8698
|
await withDb2(async (db) => {
|
|
7065
|
-
const
|
|
8699
|
+
const provider = await getProviderForRole(db, "recall");
|
|
8700
|
+
const { enabled, url, model, apiKey } = provider;
|
|
8701
|
+
const unsupportedProvider = provider.apiFlavor !== "chat-completions";
|
|
7066
8702
|
let online = false;
|
|
7067
8703
|
let availableModels = [];
|
|
7068
8704
|
let modelAvailable = false;
|
|
7069
|
-
if (enabled) {
|
|
8705
|
+
if (enabled && !unsupportedProvider) {
|
|
7070
8706
|
online = await isLlmOnline(url);
|
|
7071
8707
|
if (online) {
|
|
7072
8708
|
availableModels = await getAvailableModels(url, apiKey);
|
|
@@ -7081,10 +8717,22 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
|
|
|
7081
8717
|
url,
|
|
7082
8718
|
model,
|
|
7083
8719
|
modelAvailable,
|
|
7084
|
-
availableModels
|
|
8720
|
+
availableModels,
|
|
8721
|
+
apiFlavor: provider.apiFlavor,
|
|
8722
|
+
unsupportedProvider
|
|
7085
8723
|
});
|
|
7086
8724
|
});
|
|
7087
8725
|
});
|
|
8726
|
+
bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
|
|
8727
|
+
await withDb2(async (db) => {
|
|
8728
|
+
const [recall, vision, text] = await Promise.all([
|
|
8729
|
+
getProviderRoleStatus(db, "recall"),
|
|
8730
|
+
getProviderRoleStatus(db, "vision"),
|
|
8731
|
+
getProviderRoleStatus(db, "text")
|
|
8732
|
+
]);
|
|
8733
|
+
jsonOut2({ roles: { recall, vision, text } });
|
|
8734
|
+
});
|
|
8735
|
+
});
|
|
7088
8736
|
bridgeCommand.command("check-vision").description(
|
|
7089
8737
|
"Check if UI observer vision analysis is enabled and ready (JSON)"
|
|
7090
8738
|
).action(async () => {
|
|
@@ -7397,8 +9045,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
7397
9045
|
});
|
|
7398
9046
|
|
|
7399
9047
|
// src/cli/commands/card.ts
|
|
7400
|
-
import { Command as
|
|
7401
|
-
var cardCommand = new
|
|
9048
|
+
import { Command as Command5 } from "commander";
|
|
9049
|
+
var cardCommand = new Command5("card").description(
|
|
7402
9050
|
"Manage spaced-repetition cards"
|
|
7403
9051
|
);
|
|
7404
9052
|
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) => {
|
|
@@ -7596,9 +9244,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
|
|
|
7596
9244
|
});
|
|
7597
9245
|
|
|
7598
9246
|
// src/cli/commands/connector.ts
|
|
7599
|
-
import { input, password } from "@inquirer/prompts";
|
|
7600
|
-
import { Command as
|
|
7601
|
-
var connectorCommand = new
|
|
9247
|
+
import { input as input2, password } from "@inquirer/prompts";
|
|
9248
|
+
import { Command as Command6 } from "commander";
|
|
9249
|
+
var connectorCommand = new Command6("connector").description(
|
|
7602
9250
|
"Manage external service connectors"
|
|
7603
9251
|
);
|
|
7604
9252
|
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(
|
|
@@ -7617,10 +9265,10 @@ connectorCommand.command("setup").description("Configure a connector").argument(
|
|
|
7617
9265
|
process.exit(1);
|
|
7618
9266
|
}
|
|
7619
9267
|
try {
|
|
7620
|
-
const orgUrl = await
|
|
9268
|
+
const orgUrl = await input2({
|
|
7621
9269
|
message: "Organization URL (e.g. https://dev.azure.com/myorg):"
|
|
7622
9270
|
});
|
|
7623
|
-
const project = await
|
|
9271
|
+
const project = await input2({
|
|
7624
9272
|
message: "Project name:"
|
|
7625
9273
|
});
|
|
7626
9274
|
const pat = await password({
|
|
@@ -7711,7 +9359,7 @@ connectorCommand.command("sync").description("Verify the Turso cloud database co
|
|
|
7711
9359
|
async function setupTurso(urlArg, tokenArg, mode) {
|
|
7712
9360
|
let db;
|
|
7713
9361
|
try {
|
|
7714
|
-
const url = urlArg ?? await
|
|
9362
|
+
const url = urlArg ?? await input2({
|
|
7715
9363
|
message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
|
|
7716
9364
|
});
|
|
7717
9365
|
const token = tokenArg ?? await password({
|
|
@@ -7740,27 +9388,27 @@ async function setupTurso(urlArg, tokenArg, mode) {
|
|
|
7740
9388
|
}
|
|
7741
9389
|
|
|
7742
9390
|
// src/cli/commands/git-sync.ts
|
|
7743
|
-
import { execSync as
|
|
7744
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
7745
|
-
import { join as
|
|
7746
|
-
import { Command as
|
|
9391
|
+
import { execSync as execSync5 } from "child_process";
|
|
9392
|
+
import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync9 } from "fs";
|
|
9393
|
+
import { join as join18 } from "path";
|
|
9394
|
+
import { Command as Command7 } from "commander";
|
|
7747
9395
|
function installHook2() {
|
|
7748
|
-
const gitDir =
|
|
7749
|
-
if (!
|
|
9396
|
+
const gitDir = join18(process.cwd(), ".git");
|
|
9397
|
+
if (!existsSync18(gitDir)) {
|
|
7750
9398
|
console.error(
|
|
7751
9399
|
"Error: Current directory is not the root of a Git repository."
|
|
7752
9400
|
);
|
|
7753
9401
|
process.exit(1);
|
|
7754
9402
|
}
|
|
7755
|
-
const hooksDir =
|
|
7756
|
-
const hookPath =
|
|
9403
|
+
const hooksDir = join18(gitDir, "hooks");
|
|
9404
|
+
const hookPath = join18(hooksDir, "post-commit");
|
|
7757
9405
|
const hookContent = `#!/bin/sh
|
|
7758
9406
|
# ZAM Spaced Repetition Auto-Stale Hook
|
|
7759
9407
|
# Triggered automatically on git commits to decay modified concept cards.
|
|
7760
9408
|
zam git-sync --commit HEAD --quiet
|
|
7761
9409
|
`;
|
|
7762
9410
|
try {
|
|
7763
|
-
|
|
9411
|
+
writeFileSync9(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
|
|
7764
9412
|
try {
|
|
7765
9413
|
chmodSync2(hookPath, "755");
|
|
7766
9414
|
} catch (_e) {
|
|
@@ -7773,7 +9421,7 @@ zam git-sync --commit HEAD --quiet
|
|
|
7773
9421
|
process.exit(1);
|
|
7774
9422
|
}
|
|
7775
9423
|
}
|
|
7776
|
-
var gitSyncCommand = new
|
|
9424
|
+
var gitSyncCommand = new Command7("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) => {
|
|
7777
9425
|
if (opts.install) {
|
|
7778
9426
|
installHook2();
|
|
7779
9427
|
return;
|
|
@@ -7782,7 +9430,7 @@ var gitSyncCommand = new Command5("git-sync").description("Sync learning cards w
|
|
|
7782
9430
|
const userId = await resolveUser(opts, db);
|
|
7783
9431
|
let changedFiles = [];
|
|
7784
9432
|
try {
|
|
7785
|
-
const output =
|
|
9433
|
+
const output = execSync5(
|
|
7786
9434
|
`git diff-tree --no-commit-id --name-only -r ${opts.commit}`,
|
|
7787
9435
|
{
|
|
7788
9436
|
encoding: "utf-8",
|
|
@@ -7862,10 +9510,10 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
|
|
|
7862
9510
|
});
|
|
7863
9511
|
|
|
7864
9512
|
// src/cli/commands/goal.ts
|
|
7865
|
-
import { existsSync as
|
|
7866
|
-
import { resolve as
|
|
7867
|
-
import { input as
|
|
7868
|
-
import { Command as
|
|
9513
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10 } from "fs";
|
|
9514
|
+
import { resolve as resolve6 } from "path";
|
|
9515
|
+
import { input as input3 } from "@inquirer/prompts";
|
|
9516
|
+
import { Command as Command8 } from "commander";
|
|
7869
9517
|
async function resolveGoalsDir() {
|
|
7870
9518
|
let goalsDir;
|
|
7871
9519
|
let db;
|
|
@@ -7876,9 +9524,9 @@ async function resolveGoalsDir() {
|
|
|
7876
9524
|
} finally {
|
|
7877
9525
|
await db?.close();
|
|
7878
9526
|
}
|
|
7879
|
-
return goalsDir ?
|
|
9527
|
+
return goalsDir ? resolve6(goalsDir) : resolve6("goals");
|
|
7880
9528
|
}
|
|
7881
|
-
var goalCommand = new
|
|
9529
|
+
var goalCommand = new Command8("goal").description(
|
|
7882
9530
|
"Manage learning goals (markdown files)"
|
|
7883
9531
|
);
|
|
7884
9532
|
goalCommand.command("list").description("List all goals").option(
|
|
@@ -7886,7 +9534,7 @@ goalCommand.command("list").description("List all goals").option(
|
|
|
7886
9534
|
"Filter by status (active, completed, paused, abandoned)"
|
|
7887
9535
|
).option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action(async (opts) => {
|
|
7888
9536
|
const goalsDir = await resolveGoalsDir();
|
|
7889
|
-
if (!
|
|
9537
|
+
if (!existsSync19(goalsDir)) {
|
|
7890
9538
|
console.error(`Goals directory not found: ${goalsDir}`);
|
|
7891
9539
|
console.error(
|
|
7892
9540
|
"Set it with: zam settings set personal.goals_dir /path/to/goals"
|
|
@@ -7987,8 +9635,8 @@ ${"\u2500".repeat(50)}`);
|
|
|
7987
9635
|
});
|
|
7988
9636
|
goalCommand.command("create").description("Create a new goal").option("--slug <slug>", "Goal slug (used as filename)").option("--title <title>", "Goal title").option("--parent <slug>", "Parent goal slug").option("--description <text>", "Goal description").option("--json", "Output as JSON").action(async (opts) => {
|
|
7989
9637
|
const goalsDir = await resolveGoalsDir();
|
|
7990
|
-
if (!
|
|
7991
|
-
|
|
9638
|
+
if (!existsSync19(goalsDir)) {
|
|
9639
|
+
mkdirSync10(goalsDir, { recursive: true });
|
|
7992
9640
|
}
|
|
7993
9641
|
let slug = opts.slug;
|
|
7994
9642
|
let title = opts.title;
|
|
@@ -7997,11 +9645,11 @@ goalCommand.command("create").description("Create a new goal").option("--slug <s
|
|
|
7997
9645
|
if (!slug || !title) {
|
|
7998
9646
|
try {
|
|
7999
9647
|
if (!title) {
|
|
8000
|
-
title = await
|
|
9648
|
+
title = await input3({ message: "Goal title:" });
|
|
8001
9649
|
}
|
|
8002
9650
|
if (!slug) {
|
|
8003
9651
|
const suggested = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
8004
|
-
slug = await
|
|
9652
|
+
slug = await input3({
|
|
8005
9653
|
message: "Goal slug (filename):",
|
|
8006
9654
|
default: suggested
|
|
8007
9655
|
});
|
|
@@ -8047,22 +9695,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
|
|
|
8047
9695
|
});
|
|
8048
9696
|
|
|
8049
9697
|
// src/cli/commands/init.ts
|
|
8050
|
-
import { existsSync as
|
|
8051
|
-
import { homedir as
|
|
8052
|
-
import { join as
|
|
8053
|
-
import { confirm, input as
|
|
8054
|
-
import { Command as
|
|
8055
|
-
var HOME2 =
|
|
9698
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
|
|
9699
|
+
import { homedir as homedir11 } from "os";
|
|
9700
|
+
import { join as join19 } from "path";
|
|
9701
|
+
import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
|
|
9702
|
+
import { Command as Command9 } from "commander";
|
|
9703
|
+
var HOME2 = homedir11();
|
|
8056
9704
|
function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
|
|
8057
9705
|
console.log(`${color}${char.repeat(len)}\x1B[0m`);
|
|
8058
9706
|
}
|
|
8059
9707
|
function bootstrapSandboxWorkspace(workspaceDir) {
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
const worldviewFile =
|
|
8064
|
-
if (!
|
|
8065
|
-
|
|
9708
|
+
mkdirSync11(join19(workspaceDir, "beliefs"), { recursive: true });
|
|
9709
|
+
mkdirSync11(join19(workspaceDir, "goals"), { recursive: true });
|
|
9710
|
+
mkdirSync11(join19(workspaceDir, "skills"), { recursive: true });
|
|
9711
|
+
const worldviewFile = join19(workspaceDir, "beliefs", "worldview.md");
|
|
9712
|
+
if (!existsSync20(worldviewFile)) {
|
|
9713
|
+
writeFileSync10(
|
|
8066
9714
|
worldviewFile,
|
|
8067
9715
|
`# Personal Worldview
|
|
8068
9716
|
|
|
@@ -8074,9 +9722,9 @@ Here, I declare the core concepts and principles I want to master.
|
|
|
8074
9722
|
"utf8"
|
|
8075
9723
|
);
|
|
8076
9724
|
}
|
|
8077
|
-
const goalsFile =
|
|
8078
|
-
if (!
|
|
8079
|
-
|
|
9725
|
+
const goalsFile = join19(workspaceDir, "goals", "goals.md");
|
|
9726
|
+
if (!existsSync20(goalsFile)) {
|
|
9727
|
+
writeFileSync10(
|
|
8080
9728
|
goalsFile,
|
|
8081
9729
|
`# Personal Goals
|
|
8082
9730
|
|
|
@@ -8088,7 +9736,7 @@ Here, I declare the core concepts and principles I want to master.
|
|
|
8088
9736
|
);
|
|
8089
9737
|
}
|
|
8090
9738
|
}
|
|
8091
|
-
var initCommand = new
|
|
9739
|
+
var initCommand = new Command9("init").description("Launch the guided interactive onboarding wizard").action(async () => {
|
|
8092
9740
|
printLine();
|
|
8093
9741
|
console.log(
|
|
8094
9742
|
"\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
|
|
@@ -8098,8 +9746,8 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
|
|
|
8098
9746
|
);
|
|
8099
9747
|
printLine();
|
|
8100
9748
|
console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
|
|
8101
|
-
const defaultWorkspace =
|
|
8102
|
-
const workspacePath = await
|
|
9749
|
+
const defaultWorkspace = join19(HOME2, "Documents", "zam");
|
|
9750
|
+
const workspacePath = await input4({
|
|
8103
9751
|
message: "Choose your ZAM workspace directory:",
|
|
8104
9752
|
default: defaultWorkspace
|
|
8105
9753
|
});
|
|
@@ -8135,7 +9783,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
|
|
|
8135
9783
|
\x1B[1mRecommendation:\x1B[0m ZAM suggests installing \x1B[32m${runnerLabel}\x1B[0m with \x1B[36m${profile.recommendedModel}\x1B[0m.`
|
|
8136
9784
|
);
|
|
8137
9785
|
console.log("\n\x1B[1m[3/5] Setting up Local LLM Runner\x1B[0m");
|
|
8138
|
-
const proceedInstall = await
|
|
9786
|
+
const proceedInstall = await confirm2({
|
|
8139
9787
|
message: `Would you like ZAM to install and configure ${runnerLabel} automatically?`,
|
|
8140
9788
|
default: true
|
|
8141
9789
|
});
|
|
@@ -8203,7 +9851,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
|
|
|
8203
9851
|
console.log(
|
|
8204
9852
|
"\n\x1B[1m[5/5] Wiring Developer Agents & Terminal Helpers\x1B[0m"
|
|
8205
9853
|
);
|
|
8206
|
-
const proceedHooks = await
|
|
9854
|
+
const proceedHooks = await confirm2({
|
|
8207
9855
|
message: "Distribute ZAM skills and install optional monitored-session shell helpers?",
|
|
8208
9856
|
default: true
|
|
8209
9857
|
});
|
|
@@ -8254,8 +9902,8 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
|
|
|
8254
9902
|
});
|
|
8255
9903
|
|
|
8256
9904
|
// src/cli/commands/learn.ts
|
|
8257
|
-
import { input as
|
|
8258
|
-
import { Command as
|
|
9905
|
+
import { input as input6 } from "@inquirer/prompts";
|
|
9906
|
+
import { Command as Command10 } from "commander";
|
|
8259
9907
|
|
|
8260
9908
|
// src/cli/learn-format.ts
|
|
8261
9909
|
var BLOOM_VERBS3 = {
|
|
@@ -8304,7 +9952,7 @@ function formatReveal(input8) {
|
|
|
8304
9952
|
}
|
|
8305
9953
|
|
|
8306
9954
|
// src/cli/review-actions.ts
|
|
8307
|
-
import { confirm as
|
|
9955
|
+
import { confirm as confirm3, input as input5, select } from "@inquirer/prompts";
|
|
8308
9956
|
async function runInteractiveReviewAction(inputData) {
|
|
8309
9957
|
let currentItem = { ...inputData.item };
|
|
8310
9958
|
while (true) {
|
|
@@ -8419,7 +10067,7 @@ async function runInteractiveReviewAction(inputData) {
|
|
|
8419
10067
|
continue;
|
|
8420
10068
|
}
|
|
8421
10069
|
if (choice === "deprecate-token") {
|
|
8422
|
-
const approved = await
|
|
10070
|
+
const approved = await confirm3({
|
|
8423
10071
|
message: `Deprecate ${currentItem.slug} so it stops appearing in review queues?`,
|
|
8424
10072
|
default: false
|
|
8425
10073
|
});
|
|
@@ -8450,7 +10098,7 @@ async function runInteractiveReviewAction(inputData) {
|
|
|
8450
10098
|
console.log(` Session steps: ${impact.session_steps}`);
|
|
8451
10099
|
console.log(` Sessions touched: ${impact.sessions_touched}`);
|
|
8452
10100
|
console.log(` Agent skills updated: ${impact.agent_skills}`);
|
|
8453
|
-
const approved = await
|
|
10101
|
+
const approved = await confirm3({
|
|
8454
10102
|
message: "Permanently delete this token and its dependent learning data?",
|
|
8455
10103
|
default: false
|
|
8456
10104
|
});
|
|
@@ -8475,7 +10123,7 @@ async function runInteractiveReviewAction(inputData) {
|
|
|
8475
10123
|
);
|
|
8476
10124
|
console.log(`Delete your card for ${currentItem.slug}?`);
|
|
8477
10125
|
console.log(` Review logs removed: ${impact.review_logs}`);
|
|
8478
|
-
const approved = await
|
|
10126
|
+
const approved = await confirm3({
|
|
8479
10127
|
message: "Delete only your card for this token?",
|
|
8480
10128
|
default: false
|
|
8481
10129
|
});
|
|
@@ -8513,21 +10161,21 @@ async function promptTokenEdit(token) {
|
|
|
8513
10161
|
}
|
|
8514
10162
|
switch (field) {
|
|
8515
10163
|
case "concept": {
|
|
8516
|
-
const concept = await
|
|
10164
|
+
const concept = await input5({
|
|
8517
10165
|
message: "Concept:",
|
|
8518
10166
|
default: token.concept
|
|
8519
10167
|
});
|
|
8520
10168
|
return concept === token.concept ? null : { concept };
|
|
8521
10169
|
}
|
|
8522
10170
|
case "domain": {
|
|
8523
|
-
const domain = await
|
|
10171
|
+
const domain = await input5({
|
|
8524
10172
|
message: "Domain (blank allowed):",
|
|
8525
10173
|
default: token.domain
|
|
8526
10174
|
});
|
|
8527
10175
|
return domain === token.domain ? null : { domain };
|
|
8528
10176
|
}
|
|
8529
10177
|
case "context": {
|
|
8530
|
-
const context = await
|
|
10178
|
+
const context = await input5({
|
|
8531
10179
|
message: "Context (blank allowed):",
|
|
8532
10180
|
default: token.context
|
|
8533
10181
|
});
|
|
@@ -8568,14 +10216,14 @@ async function promptTokenEdit(token) {
|
|
|
8568
10216
|
return mode === token.symbiosis_mode ? null : { symbiosis_mode: mode };
|
|
8569
10217
|
}
|
|
8570
10218
|
case "source_link": {
|
|
8571
|
-
const link = await
|
|
10219
|
+
const link = await input5({
|
|
8572
10220
|
message: "Source link (blank allowed):",
|
|
8573
10221
|
default: token.source_link ?? ""
|
|
8574
10222
|
});
|
|
8575
10223
|
return link === token.source_link ? null : { source_link: link || null };
|
|
8576
10224
|
}
|
|
8577
10225
|
case "question": {
|
|
8578
|
-
const question = await
|
|
10226
|
+
const question = await input5({
|
|
8579
10227
|
message: "Recall question (blank allowed):",
|
|
8580
10228
|
default: token.question ?? ""
|
|
8581
10229
|
});
|
|
@@ -8593,7 +10241,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
|
|
|
8593
10241
|
function isExitPrompt(err) {
|
|
8594
10242
|
return err instanceof Error && err.name === "ExitPromptError";
|
|
8595
10243
|
}
|
|
8596
|
-
var learnCommand = new
|
|
10244
|
+
var learnCommand = new Command10("learn").description(
|
|
8597
10245
|
"Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
|
|
8598
10246
|
).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) => {
|
|
8599
10247
|
let db;
|
|
@@ -8673,7 +10321,7 @@ ${"\u2500".repeat(50)}`);
|
|
|
8673
10321
|
${prompt.question}`);
|
|
8674
10322
|
let answer;
|
|
8675
10323
|
try {
|
|
8676
|
-
answer = await
|
|
10324
|
+
answer = await input6({
|
|
8677
10325
|
message: t(locale, "prompt_answer")
|
|
8678
10326
|
});
|
|
8679
10327
|
} catch (err) {
|
|
@@ -8793,36 +10441,52 @@ ${"\u2550".repeat(50)}`);
|
|
|
8793
10441
|
process.exit(1);
|
|
8794
10442
|
}
|
|
8795
10443
|
});
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
import { tmpdir as tmpdir3 } from "os";
|
|
8801
|
-
import { basename as basename3, join as join16 } from "path";
|
|
8802
|
-
import { Command as Command9 } from "commander";
|
|
8803
|
-
function isPowerShellShell(shell) {
|
|
8804
|
-
return shell === "pwsh" || shell === "powershell";
|
|
10444
|
+
function buildLearnCommand(shell, userId) {
|
|
10445
|
+
const zamInvocation = resolveZamInvocation(shell);
|
|
10446
|
+
const learnArgs = userId ? ` learn --user ${userId}` : " learn";
|
|
10447
|
+
return `${zamInvocation}${learnArgs}`;
|
|
8805
10448
|
}
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
10449
|
+
learnCommand.command("open").description("Open a new terminal window running zam learn (Active Recall)").option("--user <id>", "User ID (default: whoami)").option("--dir <path>", "Working directory (defaults to cwd)").option(
|
|
10450
|
+
"--shell <type>",
|
|
10451
|
+
"Shell type: zsh | bash | pwsh | powershell (auto-detected)"
|
|
10452
|
+
).action(async (opts) => {
|
|
10453
|
+
let shell;
|
|
10454
|
+
try {
|
|
10455
|
+
shell = normalizeShell(opts.shell);
|
|
10456
|
+
} catch (err) {
|
|
10457
|
+
console.error(`Error: ${err.message}`);
|
|
10458
|
+
process.exit(1);
|
|
8811
10459
|
}
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
}
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
10460
|
+
let db;
|
|
10461
|
+
let userId = opts.user;
|
|
10462
|
+
try {
|
|
10463
|
+
db = await openDatabase();
|
|
10464
|
+
if (!userId) {
|
|
10465
|
+
userId = await resolveUser(opts, db);
|
|
10466
|
+
}
|
|
10467
|
+
if (!await getSetting(db, "review_method")) {
|
|
10468
|
+
await setSetting(db, "review_method", "console");
|
|
10469
|
+
}
|
|
10470
|
+
} catch (err) {
|
|
10471
|
+
console.error(`Error: ${err.message}`);
|
|
10472
|
+
process.exit(1);
|
|
10473
|
+
} finally {
|
|
10474
|
+
await db?.close();
|
|
10475
|
+
}
|
|
10476
|
+
const dir = opts.dir ?? process.cwd();
|
|
10477
|
+
const learnCommandLine = buildLearnCommand(shell, userId);
|
|
10478
|
+
const shellSetup = buildShellSetupCommand(dir, shell, learnCommandLine);
|
|
10479
|
+
openTerminalWindow({
|
|
10480
|
+
shellSetup,
|
|
10481
|
+
label: "learn",
|
|
10482
|
+
dir,
|
|
10483
|
+
shell
|
|
10484
|
+
});
|
|
10485
|
+
});
|
|
10486
|
+
|
|
10487
|
+
// src/cli/commands/monitor.ts
|
|
10488
|
+
import { Command as Command11 } from "commander";
|
|
10489
|
+
var monitorCommand = new Command11("monitor").description(
|
|
8826
10490
|
"Shell observation for real-time task monitoring"
|
|
8827
10491
|
);
|
|
8828
10492
|
monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
|
|
@@ -8949,39 +10613,6 @@ monitorCommand.command("status").description("Show monitoring status for a sessi
|
|
|
8949
10613
|
console.log(` To: ${result.timeSpan.end}`);
|
|
8950
10614
|
}
|
|
8951
10615
|
});
|
|
8952
|
-
function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
|
|
8953
|
-
if (results.length === 0) return null;
|
|
8954
|
-
const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
|
|
8955
|
-
const runnable = results.find(
|
|
8956
|
-
(result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
|
|
8957
|
-
);
|
|
8958
|
-
return runnable ?? results[0];
|
|
8959
|
-
}
|
|
8960
|
-
function findExecutable(command) {
|
|
8961
|
-
try {
|
|
8962
|
-
const lookup = process.platform === "win32" ? `where.exe ${command}` : `command -v ${command}`;
|
|
8963
|
-
const results = execSync5(lookup, { encoding: "utf-8" }).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
8964
|
-
if (results.length === 0) return null;
|
|
8965
|
-
if (process.platform === "win32") {
|
|
8966
|
-
return selectWindowsExecutable(results);
|
|
8967
|
-
}
|
|
8968
|
-
return results[0];
|
|
8969
|
-
} catch {
|
|
8970
|
-
return null;
|
|
8971
|
-
}
|
|
8972
|
-
}
|
|
8973
|
-
function resolveZamInvocation(shell) {
|
|
8974
|
-
const installed = findExecutable("zam");
|
|
8975
|
-
if (installed) {
|
|
8976
|
-
return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
|
|
8977
|
-
}
|
|
8978
|
-
const projectRoot = join16(import.meta.dirname, "..", "..", "..");
|
|
8979
|
-
const cliSource = join16(projectRoot, "src/cli/index.ts");
|
|
8980
|
-
if (isPowerShellShell(shell)) {
|
|
8981
|
-
return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
|
|
8982
|
-
}
|
|
8983
|
-
return `npx --prefix ${JSON.stringify(projectRoot)} tsx ${JSON.stringify(cliSource)}`;
|
|
8984
|
-
}
|
|
8985
10616
|
function buildMonitorSetupCommand(dir, sessionId, shell) {
|
|
8986
10617
|
const zamInvocation = resolveZamInvocation(shell);
|
|
8987
10618
|
if (isPowerShellShell(shell)) {
|
|
@@ -8994,17 +10625,6 @@ function buildMonitorSetupCommand(dir, sessionId, shell) {
|
|
|
8994
10625
|
}
|
|
8995
10626
|
return `cd ${JSON.stringify(dir)} && eval "$(${zamInvocation} monitor start --session ${sessionId} --shell ${shell})"`;
|
|
8996
10627
|
}
|
|
8997
|
-
function isItermRunning() {
|
|
8998
|
-
try {
|
|
8999
|
-
const result = execSync5(
|
|
9000
|
-
`osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"' 2>/dev/null`,
|
|
9001
|
-
{ encoding: "utf-8" }
|
|
9002
|
-
).trim();
|
|
9003
|
-
return result === "true";
|
|
9004
|
-
} catch {
|
|
9005
|
-
return false;
|
|
9006
|
-
}
|
|
9007
|
-
}
|
|
9008
10628
|
monitorCommand.command("open").description("Open a new monitored terminal window for a session").requiredOption("--session <id>", "Session ID to monitor").option("--dir <path>", "Working directory (defaults to cwd)").option(
|
|
9009
10629
|
"--shell <type>",
|
|
9010
10630
|
"Shell type: zsh | bash | pwsh | powershell (auto-detected)"
|
|
@@ -9019,107 +10639,37 @@ monitorCommand.command("open").description("Open a new monitored terminal window
|
|
|
9019
10639
|
let db;
|
|
9020
10640
|
try {
|
|
9021
10641
|
db = await openDatabase();
|
|
9022
|
-
const session = await db.prepare("SELECT id, completed_at FROM sessions WHERE id = ?").get(opts.session);
|
|
9023
|
-
if (!session) {
|
|
9024
|
-
console.error(`Error: Session not found: ${opts.session}`);
|
|
9025
|
-
process.exit(1);
|
|
9026
|
-
}
|
|
9027
|
-
if (session.completed_at) {
|
|
9028
|
-
console.error(`Error: Session already completed: ${opts.session}`);
|
|
9029
|
-
process.exit(1);
|
|
9030
|
-
}
|
|
9031
|
-
if (!await getSetting(db, "monitor_method")) {
|
|
9032
|
-
await setSetting(db, "monitor_method", "terminal");
|
|
9033
|
-
}
|
|
9034
|
-
} catch (err) {
|
|
9035
|
-
console.error(`Error: ${err.message}`);
|
|
9036
|
-
process.exit(1);
|
|
9037
|
-
} finally {
|
|
9038
|
-
await db?.close();
|
|
9039
|
-
}
|
|
9040
|
-
const dir = opts.dir ?? process.cwd();
|
|
9041
|
-
const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
|
|
9042
|
-
if (process.platform === "darwin" && !isPowerShellShell(shell)) {
|
|
9043
|
-
openMacTerminal(shellSetup, opts.session, dir);
|
|
9044
|
-
} else if (process.platform === "win32" && isPowerShellShell(shell)) {
|
|
9045
|
-
openWindowsPowerShell(shellSetup, opts.session, dir, shell);
|
|
9046
|
-
} else {
|
|
9047
|
-
console.log(`Run this in a new terminal:
|
|
9048
|
-
`);
|
|
9049
|
-
console.log(` ${shellSetup}
|
|
9050
|
-
`);
|
|
9051
|
-
console.log(
|
|
9052
|
-
`(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
|
|
9053
|
-
);
|
|
9054
|
-
}
|
|
9055
|
-
});
|
|
9056
|
-
function openMacTerminal(shellSetup, sessionId, dir) {
|
|
9057
|
-
const useIterm = isItermRunning();
|
|
9058
|
-
const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
9059
|
-
const appleScript = useIterm ? `tell application "iTerm2"
|
|
9060
|
-
activate
|
|
9061
|
-
set newWindow to (create window with default profile)
|
|
9062
|
-
tell current session of newWindow
|
|
9063
|
-
write text "${escaped}"
|
|
9064
|
-
end tell
|
|
9065
|
-
end tell` : `tell application "Terminal"
|
|
9066
|
-
activate
|
|
9067
|
-
do script "${escaped}"
|
|
9068
|
-
end tell`;
|
|
9069
|
-
const tmpFile = join16(tmpdir3(), `zam-monitor-${sessionId}.scpt`);
|
|
9070
|
-
try {
|
|
9071
|
-
writeFileSync8(tmpFile, appleScript);
|
|
9072
|
-
execSync5(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
|
|
9073
|
-
console.log(
|
|
9074
|
-
`Opened ${useIterm ? "iTerm2" : "Terminal.app"} window with monitoring for session ${sessionId}`
|
|
9075
|
-
);
|
|
9076
|
-
console.log(` Directory: ${dir}`);
|
|
9077
|
-
} catch (err) {
|
|
9078
|
-
console.error(`Failed to open terminal: ${err.message}`);
|
|
9079
|
-
console.log(`
|
|
9080
|
-
Run this manually in a new terminal:
|
|
9081
|
-
`);
|
|
9082
|
-
console.log(` ${shellSetup}`);
|
|
9083
|
-
} finally {
|
|
9084
|
-
try {
|
|
9085
|
-
unlinkSync(tmpFile);
|
|
9086
|
-
} catch {
|
|
9087
|
-
}
|
|
9088
|
-
}
|
|
9089
|
-
}
|
|
9090
|
-
function openWindowsPowerShell(shellSetup, sessionId, dir, requestedShell) {
|
|
9091
|
-
const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
|
|
9092
|
-
const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
|
|
9093
|
-
const startCommand = [
|
|
9094
|
-
"Start-Process",
|
|
9095
|
-
`-FilePath ${psSingleQuoted2(executable)}`,
|
|
9096
|
-
`-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
|
|
9097
|
-
].join(" ");
|
|
9098
|
-
const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
|
|
9099
|
-
try {
|
|
9100
|
-
execFileSync3(
|
|
9101
|
-
launcher,
|
|
9102
|
-
["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
|
|
9103
|
-
{
|
|
9104
|
-
stdio: "ignore"
|
|
9105
|
-
}
|
|
9106
|
-
);
|
|
9107
|
-
console.log(
|
|
9108
|
-
`Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window with monitoring for session ${sessionId}`
|
|
9109
|
-
);
|
|
9110
|
-
console.log(` Directory: ${dir}`);
|
|
10642
|
+
const session = await db.prepare("SELECT id, completed_at FROM sessions WHERE id = ?").get(opts.session);
|
|
10643
|
+
if (!session) {
|
|
10644
|
+
console.error(`Error: Session not found: ${opts.session}`);
|
|
10645
|
+
process.exit(1);
|
|
10646
|
+
}
|
|
10647
|
+
if (session.completed_at) {
|
|
10648
|
+
console.error(`Error: Session already completed: ${opts.session}`);
|
|
10649
|
+
process.exit(1);
|
|
10650
|
+
}
|
|
10651
|
+
if (!await getSetting(db, "monitor_method")) {
|
|
10652
|
+
await setSetting(db, "monitor_method", "terminal");
|
|
10653
|
+
}
|
|
9111
10654
|
} catch (err) {
|
|
9112
|
-
console.error(`
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
console.log(` ${shellSetup}`);
|
|
10655
|
+
console.error(`Error: ${err.message}`);
|
|
10656
|
+
process.exit(1);
|
|
10657
|
+
} finally {
|
|
10658
|
+
await db?.close();
|
|
9117
10659
|
}
|
|
9118
|
-
|
|
10660
|
+
const dir = opts.dir ?? process.cwd();
|
|
10661
|
+
const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
|
|
10662
|
+
openTerminalWindow({
|
|
10663
|
+
shellSetup,
|
|
10664
|
+
label: `monitor-${opts.session}`,
|
|
10665
|
+
dir,
|
|
10666
|
+
shell
|
|
10667
|
+
});
|
|
10668
|
+
});
|
|
9119
10669
|
|
|
9120
10670
|
// src/cli/commands/observer.ts
|
|
9121
|
-
import { Command as
|
|
9122
|
-
var observerCommand = new
|
|
10671
|
+
import { Command as Command12 } from "commander";
|
|
10672
|
+
var observerCommand = new Command12("observer").description(
|
|
9123
10673
|
"Configure what the UI observer may capture (Layer 2 policy)"
|
|
9124
10674
|
);
|
|
9125
10675
|
function applyObserverListChange(current, entry, op) {
|
|
@@ -9181,9 +10731,9 @@ observerCommand.command("revoke <process>").description("Remove a process from o
|
|
|
9181
10731
|
});
|
|
9182
10732
|
|
|
9183
10733
|
// src/cli/commands/profile.ts
|
|
9184
|
-
import { homedir as
|
|
9185
|
-
import { dirname as
|
|
9186
|
-
import { Command as
|
|
10734
|
+
import { homedir as homedir12 } from "os";
|
|
10735
|
+
import { dirname as dirname6, join as join20, resolve as resolve7 } from "path";
|
|
10736
|
+
import { Command as Command13 } from "commander";
|
|
9187
10737
|
var C2 = {
|
|
9188
10738
|
reset: "\x1B[0m",
|
|
9189
10739
|
bold: "\x1B[1m",
|
|
@@ -9192,7 +10742,7 @@ var C2 = {
|
|
|
9192
10742
|
green: "\x1B[32m"
|
|
9193
10743
|
};
|
|
9194
10744
|
function defaultPersonalDir() {
|
|
9195
|
-
return
|
|
10745
|
+
return join20(homedir12(), "Documents", "zam");
|
|
9196
10746
|
}
|
|
9197
10747
|
function render(profile) {
|
|
9198
10748
|
const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
|
|
@@ -9203,7 +10753,7 @@ function render(profile) {
|
|
|
9203
10753
|
console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
|
|
9204
10754
|
console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
|
|
9205
10755
|
}
|
|
9206
|
-
var profileCommand = new
|
|
10756
|
+
var profileCommand = new Command13("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
|
|
9207
10757
|
if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
|
|
9208
10758
|
console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
|
|
9209
10759
|
process.exit(1);
|
|
@@ -9213,7 +10763,7 @@ var profileCommand = new Command11("profile").description("Show or change this m
|
|
|
9213
10763
|
if (opts.mode) setInstallMode(opts.mode);
|
|
9214
10764
|
db = await openDatabaseWithSync({ initialize: true });
|
|
9215
10765
|
if (opts.dir) {
|
|
9216
|
-
await setSetting(db, "personal.workspace_dir",
|
|
10766
|
+
await setSetting(db, "personal.workspace_dir", resolve7(opts.dir));
|
|
9217
10767
|
}
|
|
9218
10768
|
const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
|
|
9219
10769
|
await db.close();
|
|
@@ -9223,7 +10773,7 @@ var profileCommand = new Command11("profile").description("Show or change this m
|
|
|
9223
10773
|
mode: getInstallMode(),
|
|
9224
10774
|
personalDir,
|
|
9225
10775
|
syncProvider: detectSyncProvider(personalDir),
|
|
9226
|
-
dataDir:
|
|
10776
|
+
dataDir: dirname6(dbPath),
|
|
9227
10777
|
dbPath
|
|
9228
10778
|
};
|
|
9229
10779
|
if (opts.json) {
|
|
@@ -9238,9 +10788,358 @@ var profileCommand = new Command11("profile").description("Show or change this m
|
|
|
9238
10788
|
}
|
|
9239
10789
|
});
|
|
9240
10790
|
|
|
10791
|
+
// src/cli/commands/provider.ts
|
|
10792
|
+
import { password as password2 } from "@inquirer/prompts";
|
|
10793
|
+
import { Command as Command14 } from "commander";
|
|
10794
|
+
var VALID_API_FLAVORS = [
|
|
10795
|
+
"chat-completions",
|
|
10796
|
+
"anthropic-messages"
|
|
10797
|
+
];
|
|
10798
|
+
var VALID_ROLES = ["vision", "recall", "text"];
|
|
10799
|
+
function upsertProviderRecord(providers, name, patch) {
|
|
10800
|
+
const merged = { ...providers[name] ?? {} };
|
|
10801
|
+
if (patch.url !== void 0) merged.url = patch.url;
|
|
10802
|
+
if (patch.model !== void 0) merged.model = patch.model;
|
|
10803
|
+
if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
|
|
10804
|
+
if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
|
|
10805
|
+
if (patch.label !== void 0) merged.label = patch.label;
|
|
10806
|
+
if (patch.local !== void 0) merged.local = patch.local;
|
|
10807
|
+
if (patch.runner !== void 0) merged.runner = patch.runner;
|
|
10808
|
+
return { ...providers, [name]: merged };
|
|
10809
|
+
}
|
|
10810
|
+
function removeProviderRecord(providers, name) {
|
|
10811
|
+
if (!(name in providers)) return { providers, removed: false };
|
|
10812
|
+
const next = { ...providers };
|
|
10813
|
+
delete next[name];
|
|
10814
|
+
return { providers: next, removed: true };
|
|
10815
|
+
}
|
|
10816
|
+
function rolesReferencing(roles, name) {
|
|
10817
|
+
return VALID_ROLES.filter((role) => {
|
|
10818
|
+
const binding = roles[role];
|
|
10819
|
+
return binding?.primary === name || binding?.fallback === name;
|
|
10820
|
+
});
|
|
10821
|
+
}
|
|
10822
|
+
function bindRoleProviders(roles, role, primary, fallback) {
|
|
10823
|
+
const binding = { primary };
|
|
10824
|
+
if (fallback) binding.fallback = fallback;
|
|
10825
|
+
return { ...roles, [role]: binding };
|
|
10826
|
+
}
|
|
10827
|
+
function maskSecret(key) {
|
|
10828
|
+
return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
|
|
10829
|
+
}
|
|
10830
|
+
function buildProviderListing(providers, hasKey) {
|
|
10831
|
+
return Object.entries(providers).map(([name, rec]) => {
|
|
10832
|
+
const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
|
|
10833
|
+
let keyState;
|
|
10834
|
+
if (!rec.apiKeyRef) keyState = "none";
|
|
10835
|
+
else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
|
|
10836
|
+
const row = {
|
|
10837
|
+
name,
|
|
10838
|
+
url: rec.url,
|
|
10839
|
+
model: rec.model,
|
|
10840
|
+
apiFlavor,
|
|
10841
|
+
apiKeyRef: rec.apiKeyRef,
|
|
10842
|
+
keyState
|
|
10843
|
+
};
|
|
10844
|
+
if (rec.label !== void 0) row.label = rec.label;
|
|
10845
|
+
if (rec.local !== void 0) row.local = rec.local;
|
|
10846
|
+
if (rec.runner !== void 0) row.runner = rec.runner;
|
|
10847
|
+
return row;
|
|
10848
|
+
});
|
|
10849
|
+
}
|
|
10850
|
+
function findOrphanKeyRefs(storedRefs, providers) {
|
|
10851
|
+
const used = new Set(
|
|
10852
|
+
Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
|
|
10853
|
+
);
|
|
10854
|
+
return storedRefs.filter((ref) => !used.has(ref));
|
|
10855
|
+
}
|
|
10856
|
+
async function readJson(db, key, fallback) {
|
|
10857
|
+
const raw = await getSetting(db, key);
|
|
10858
|
+
if (!raw) return fallback;
|
|
10859
|
+
try {
|
|
10860
|
+
return JSON.parse(raw);
|
|
10861
|
+
} catch {
|
|
10862
|
+
return fallback;
|
|
10863
|
+
}
|
|
10864
|
+
}
|
|
10865
|
+
var readProviders = (db) => readJson(db, "llm.providers", {});
|
|
10866
|
+
var readRoles = (db) => readJson(db, "llm.roles", {});
|
|
10867
|
+
async function readScopedProviders(db, machine) {
|
|
10868
|
+
if (machine) return getMachineAiConfig().providers ?? {};
|
|
10869
|
+
if (!db)
|
|
10870
|
+
throw new Error("Database is required for shared provider settings.");
|
|
10871
|
+
return readProviders(db);
|
|
10872
|
+
}
|
|
10873
|
+
async function readScopedRoles(db, machine) {
|
|
10874
|
+
if (machine) return getMachineAiConfig().roles ?? {};
|
|
10875
|
+
if (!db)
|
|
10876
|
+
throw new Error("Database is required for shared provider settings.");
|
|
10877
|
+
return readRoles(db);
|
|
10878
|
+
}
|
|
10879
|
+
async function writeScopedProviders(db, machine, p) {
|
|
10880
|
+
if (machine) {
|
|
10881
|
+
const config = getMachineAiConfig();
|
|
10882
|
+
saveMachineAiConfig({ ...config, providers: p });
|
|
10883
|
+
return;
|
|
10884
|
+
}
|
|
10885
|
+
if (!db)
|
|
10886
|
+
throw new Error("Database is required for shared provider settings.");
|
|
10887
|
+
await setSetting(db, "llm.providers", JSON.stringify(p));
|
|
10888
|
+
}
|
|
10889
|
+
async function writeScopedRoles(db, machine, r) {
|
|
10890
|
+
if (machine) {
|
|
10891
|
+
const config = getMachineAiConfig();
|
|
10892
|
+
saveMachineAiConfig({ ...config, roles: r });
|
|
10893
|
+
return;
|
|
10894
|
+
}
|
|
10895
|
+
if (!db)
|
|
10896
|
+
throw new Error("Database is required for shared provider settings.");
|
|
10897
|
+
await setSetting(db, "llm.roles", JSON.stringify(r));
|
|
10898
|
+
}
|
|
10899
|
+
async function withProviderScope(machine, action) {
|
|
10900
|
+
if (machine) {
|
|
10901
|
+
await action(void 0);
|
|
10902
|
+
return;
|
|
10903
|
+
}
|
|
10904
|
+
await withDb(action);
|
|
10905
|
+
}
|
|
10906
|
+
var providerCommand = new Command14("provider").description(
|
|
10907
|
+
"Configure role-based AI providers (url/model/flavor/key, per role)"
|
|
10908
|
+
);
|
|
10909
|
+
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) => {
|
|
10910
|
+
const machine = Boolean(opts.machine);
|
|
10911
|
+
await withProviderScope(machine, async (db) => {
|
|
10912
|
+
const providers = await readScopedProviders(db, machine);
|
|
10913
|
+
const roles = await readScopedRoles(db, machine);
|
|
10914
|
+
const rows = buildProviderListing(
|
|
10915
|
+
providers,
|
|
10916
|
+
(ref) => getProviderApiKey(ref) !== null
|
|
10917
|
+
);
|
|
10918
|
+
const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
|
|
10919
|
+
if (opts.json) {
|
|
10920
|
+
console.log(
|
|
10921
|
+
JSON.stringify(
|
|
10922
|
+
{
|
|
10923
|
+
scope: machine ? "machine" : "shared",
|
|
10924
|
+
providers: rows,
|
|
10925
|
+
roles,
|
|
10926
|
+
orphans
|
|
10927
|
+
},
|
|
10928
|
+
null,
|
|
10929
|
+
2
|
|
10930
|
+
)
|
|
10931
|
+
);
|
|
10932
|
+
return;
|
|
10933
|
+
}
|
|
10934
|
+
console.log(
|
|
10935
|
+
`Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
|
|
10936
|
+
`
|
|
10937
|
+
);
|
|
10938
|
+
if (rows.length === 0) {
|
|
10939
|
+
console.log(
|
|
10940
|
+
" (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
|
|
10941
|
+
);
|
|
10942
|
+
} else {
|
|
10943
|
+
for (const row of rows) {
|
|
10944
|
+
const key = row.keyState === "set" ? "\x1B[32mkey \u2713\x1B[0m" : row.keyState === "missing" ? `\x1B[31mkey \u2717 (set: zam provider set-key ${row.apiKeyRef})\x1B[0m` : "\x1B[90mno key\x1B[0m";
|
|
10945
|
+
console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
|
|
10946
|
+
console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
|
|
10947
|
+
console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
|
|
10948
|
+
console.log(` flavor: ${row.apiFlavor}`);
|
|
10949
|
+
if (row.label) console.log(` label: ${row.label}`);
|
|
10950
|
+
if (row.local !== void 0) {
|
|
10951
|
+
console.log(` local: ${row.local ? "yes" : "no"}`);
|
|
10952
|
+
}
|
|
10953
|
+
if (row.runner) console.log(` runner: ${row.runner}`);
|
|
10954
|
+
if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
|
|
10955
|
+
}
|
|
10956
|
+
}
|
|
10957
|
+
console.log("\nRoles (llm.roles):\n");
|
|
10958
|
+
for (const role of VALID_ROLES) {
|
|
10959
|
+
const binding = roles[role];
|
|
10960
|
+
if (!binding?.primary) {
|
|
10961
|
+
console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
|
|
10962
|
+
} else {
|
|
10963
|
+
const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
|
|
10964
|
+
console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
|
|
10965
|
+
}
|
|
10966
|
+
}
|
|
10967
|
+
if (orphans.length > 0) {
|
|
10968
|
+
console.log(
|
|
10969
|
+
`
|
|
10970
|
+
\x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
|
|
10971
|
+
);
|
|
10972
|
+
}
|
|
10973
|
+
});
|
|
10974
|
+
});
|
|
10975
|
+
providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--label <label>", "Human-readable provider label").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option("--local", "Mark this provider as a local/on-device endpoint").option(
|
|
10976
|
+
"--runner <runner>",
|
|
10977
|
+
"Local runner hint (flm, foundry-local, ollama, ...)"
|
|
10978
|
+
).option(
|
|
10979
|
+
"--flavor <flavor>",
|
|
10980
|
+
`Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
|
|
10981
|
+
).option(
|
|
10982
|
+
"--key-ref <ref>",
|
|
10983
|
+
"Credential reference for the API key (default: <name> when --key is given)"
|
|
10984
|
+
).option(
|
|
10985
|
+
"--key <value>",
|
|
10986
|
+
"Store this API key now (prefer `set-key` to keep it out of shell history)"
|
|
10987
|
+
).option("--machine", "Store this provider in ~/.zam/config.json").action(async (name, opts) => {
|
|
10988
|
+
let apiFlavor;
|
|
10989
|
+
if (opts.flavor) {
|
|
10990
|
+
if (!VALID_API_FLAVORS.includes(opts.flavor)) {
|
|
10991
|
+
console.error(
|
|
10992
|
+
`Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
|
|
10993
|
+
);
|
|
10994
|
+
process.exit(1);
|
|
10995
|
+
}
|
|
10996
|
+
apiFlavor = opts.flavor;
|
|
10997
|
+
}
|
|
10998
|
+
const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
|
|
10999
|
+
const machine = Boolean(opts.machine);
|
|
11000
|
+
await withProviderScope(machine, async (db) => {
|
|
11001
|
+
const providers = await readScopedProviders(db, machine);
|
|
11002
|
+
const next = upsertProviderRecord(providers, name, {
|
|
11003
|
+
label: opts.label,
|
|
11004
|
+
url: opts.url,
|
|
11005
|
+
model: opts.model,
|
|
11006
|
+
apiFlavor,
|
|
11007
|
+
apiKeyRef,
|
|
11008
|
+
local: opts.local ? true : void 0,
|
|
11009
|
+
runner: opts.runner
|
|
11010
|
+
});
|
|
11011
|
+
await writeScopedProviders(db, machine, next);
|
|
11012
|
+
if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
|
|
11013
|
+
const rec = next[name];
|
|
11014
|
+
console.log(
|
|
11015
|
+
`Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
|
|
11016
|
+
);
|
|
11017
|
+
console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
|
|
11018
|
+
console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
|
|
11019
|
+
if (rec.label) console.log(` label: ${rec.label}`);
|
|
11020
|
+
if (rec.local !== void 0) {
|
|
11021
|
+
console.log(` local: ${rec.local ? "yes" : "no"}`);
|
|
11022
|
+
}
|
|
11023
|
+
if (rec.runner) console.log(` runner: ${rec.runner}`);
|
|
11024
|
+
console.log(
|
|
11025
|
+
` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
|
|
11026
|
+
);
|
|
11027
|
+
console.log(` key-ref: ${rec.apiKeyRef ?? "(none \u2014 uses default key)"}`);
|
|
11028
|
+
if (opts.key) {
|
|
11029
|
+
console.log(` key: stored (${maskSecret(opts.key)})`);
|
|
11030
|
+
} else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
|
|
11031
|
+
console.log(
|
|
11032
|
+
`
|
|
11033
|
+
\u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
|
|
11034
|
+
);
|
|
11035
|
+
}
|
|
11036
|
+
if (!rec.url) {
|
|
11037
|
+
console.log(
|
|
11038
|
+
"\n \u26A0 No --url set; this provider inherits the base llm.url."
|
|
11039
|
+
);
|
|
11040
|
+
}
|
|
11041
|
+
console.log(
|
|
11042
|
+
`
|
|
11043
|
+
Bind it to a role: zam provider use recall --primary ${name}`
|
|
11044
|
+
);
|
|
11045
|
+
});
|
|
11046
|
+
});
|
|
11047
|
+
providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").option("--machine", "Remove from ~/.zam/config.json instead of the DB").action(async (name, opts) => {
|
|
11048
|
+
const machine = Boolean(opts.machine);
|
|
11049
|
+
await withProviderScope(machine, async (db) => {
|
|
11050
|
+
const providers = await readScopedProviders(db, machine);
|
|
11051
|
+
const { providers: next, removed } = removeProviderRecord(
|
|
11052
|
+
providers,
|
|
11053
|
+
name
|
|
11054
|
+
);
|
|
11055
|
+
if (!removed) {
|
|
11056
|
+
console.log(`No such provider: ${name}`);
|
|
11057
|
+
return;
|
|
11058
|
+
}
|
|
11059
|
+
await writeScopedProviders(db, machine, next);
|
|
11060
|
+
console.log(
|
|
11061
|
+
`Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
|
|
11062
|
+
);
|
|
11063
|
+
const referencing = rolesReferencing(
|
|
11064
|
+
await readScopedRoles(db, machine),
|
|
11065
|
+
name
|
|
11066
|
+
);
|
|
11067
|
+
if (referencing.length > 0) {
|
|
11068
|
+
console.log(
|
|
11069
|
+
` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
|
|
11070
|
+
);
|
|
11071
|
+
}
|
|
11072
|
+
});
|
|
11073
|
+
});
|
|
11074
|
+
providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").option("--machine", "Bind the role in ~/.zam/config.json").action(async (role, opts) => {
|
|
11075
|
+
if (!VALID_ROLES.includes(role)) {
|
|
11076
|
+
console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
|
|
11077
|
+
process.exit(1);
|
|
11078
|
+
}
|
|
11079
|
+
if (!opts.primary) {
|
|
11080
|
+
console.error("--primary is required.");
|
|
11081
|
+
process.exit(1);
|
|
11082
|
+
}
|
|
11083
|
+
const machine = Boolean(opts.machine);
|
|
11084
|
+
await withProviderScope(machine, async (db) => {
|
|
11085
|
+
const providers = await readScopedProviders(db, machine);
|
|
11086
|
+
await writeScopedRoles(
|
|
11087
|
+
db,
|
|
11088
|
+
machine,
|
|
11089
|
+
bindRoleProviders(
|
|
11090
|
+
await readScopedRoles(db, machine),
|
|
11091
|
+
role,
|
|
11092
|
+
opts.primary,
|
|
11093
|
+
opts.fallback
|
|
11094
|
+
)
|
|
11095
|
+
);
|
|
11096
|
+
const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
|
|
11097
|
+
console.log(
|
|
11098
|
+
`Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
|
|
11099
|
+
);
|
|
11100
|
+
for (const [label, ref] of [
|
|
11101
|
+
["primary", opts.primary],
|
|
11102
|
+
["fallback", opts.fallback]
|
|
11103
|
+
]) {
|
|
11104
|
+
if (ref && !(ref in providers)) {
|
|
11105
|
+
console.log(
|
|
11106
|
+
` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
|
|
11107
|
+
);
|
|
11108
|
+
}
|
|
11109
|
+
}
|
|
11110
|
+
});
|
|
11111
|
+
});
|
|
11112
|
+
providerCommand.command("set-key <ref>").description(
|
|
11113
|
+
"Store an API key for a provider reference (in credentials.json)"
|
|
11114
|
+
).option(
|
|
11115
|
+
"--key <value>",
|
|
11116
|
+
"The API key (omit to enter it interactively, hidden)"
|
|
11117
|
+
).action(async (ref, opts) => {
|
|
11118
|
+
try {
|
|
11119
|
+
const key = opts.key ?? await password2({ message: `API key for "${ref}":` });
|
|
11120
|
+
if (!key || key.trim().length === 0) {
|
|
11121
|
+
console.error("No key provided.");
|
|
11122
|
+
process.exit(1);
|
|
11123
|
+
}
|
|
11124
|
+
setProviderApiKey(ref, key.trim());
|
|
11125
|
+
console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
|
|
11126
|
+
} catch (err) {
|
|
11127
|
+
if (err.name === "ExitPromptError") {
|
|
11128
|
+
console.log("\nCancelled.");
|
|
11129
|
+
process.exit(0);
|
|
11130
|
+
}
|
|
11131
|
+
console.error("Error:", err.message);
|
|
11132
|
+
process.exit(1);
|
|
11133
|
+
}
|
|
11134
|
+
});
|
|
11135
|
+
providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
|
|
11136
|
+
clearProviderApiKey(ref);
|
|
11137
|
+
console.log(`Cleared API key for "${ref}".`);
|
|
11138
|
+
});
|
|
11139
|
+
|
|
9241
11140
|
// src/cli/commands/review.ts
|
|
9242
|
-
import { Command as
|
|
9243
|
-
var reviewCommand = new
|
|
11141
|
+
import { Command as Command15 } from "commander";
|
|
11142
|
+
var reviewCommand = new Command15("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) => {
|
|
9244
11143
|
let db;
|
|
9245
11144
|
try {
|
|
9246
11145
|
db = await openDatabase();
|
|
@@ -9353,10 +11252,10 @@ ${"\u2550".repeat(50)}`);
|
|
|
9353
11252
|
});
|
|
9354
11253
|
|
|
9355
11254
|
// src/cli/commands/session.ts
|
|
9356
|
-
import { readFileSync as
|
|
9357
|
-
import { input as
|
|
9358
|
-
import { Command as
|
|
9359
|
-
var sessionCommand = new
|
|
11255
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
11256
|
+
import { input as input7, select as select2 } from "@inquirer/prompts";
|
|
11257
|
+
import { Command as Command16 } from "commander";
|
|
11258
|
+
var sessionCommand = new Command16("session").description(
|
|
9360
11259
|
"Manage learning sessions"
|
|
9361
11260
|
);
|
|
9362
11261
|
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(
|
|
@@ -9533,7 +11432,7 @@ async function selectTask() {
|
|
|
9533
11432
|
console.log("No active work items found in Azure DevOps.");
|
|
9534
11433
|
}
|
|
9535
11434
|
}
|
|
9536
|
-
return
|
|
11435
|
+
return input7({ message: "Task description:" });
|
|
9537
11436
|
}
|
|
9538
11437
|
var RATING_LABELS = {
|
|
9539
11438
|
1: "Again",
|
|
@@ -9545,7 +11444,7 @@ function loadPatternFile(path) {
|
|
|
9545
11444
|
if (!path) return [];
|
|
9546
11445
|
let parsed;
|
|
9547
11446
|
try {
|
|
9548
|
-
parsed = JSON.parse(
|
|
11447
|
+
parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
9549
11448
|
} catch (err) {
|
|
9550
11449
|
throw new Error(
|
|
9551
11450
|
`Cannot read synthesis patterns from ${path}: ${err.message}`
|
|
@@ -9735,9 +11634,9 @@ sessionCommand.command("end").description("End a session and show summary").requ
|
|
|
9735
11634
|
});
|
|
9736
11635
|
|
|
9737
11636
|
// src/cli/commands/settings.ts
|
|
9738
|
-
import { existsSync as
|
|
9739
|
-
import { Command as
|
|
9740
|
-
var settingsCommand = new
|
|
11637
|
+
import { existsSync as existsSync21 } from "fs";
|
|
11638
|
+
import { Command as Command17 } from "commander";
|
|
11639
|
+
var settingsCommand = new Command17("settings").description(
|
|
9741
11640
|
"Manage user settings"
|
|
9742
11641
|
);
|
|
9743
11642
|
var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
|
|
@@ -9860,229 +11759,60 @@ settingsCommand.command("locale [lang]").description(
|
|
|
9860
11759
|
);
|
|
9861
11760
|
process.exit(1);
|
|
9862
11761
|
}
|
|
9863
|
-
await setSetting(db, "system.locale", lower);
|
|
9864
|
-
console.log(`Language set to: \x1B[32m${lower}\x1B[0m`);
|
|
9865
|
-
});
|
|
9866
|
-
});
|
|
9867
|
-
settingsCommand.command("repos").description("Show or set Personal, Team, and Organization repository paths").option("--personal <path>", "Set the Personal Repository path").option("--team <path>", "Set the Team Repository path").option("--org <path>", "Set the Organization Repository path").action(async (opts) => {
|
|
9868
|
-
await withDb(async (db) => {
|
|
9869
|
-
let changed = false;
|
|
9870
|
-
if (opts.personal !== void 0) {
|
|
9871
|
-
await setSetting(db, "repo.personal", opts.personal);
|
|
9872
|
-
console.log(`Set repo.personal = ${opts.personal}`);
|
|
9873
|
-
changed = true;
|
|
9874
|
-
}
|
|
9875
|
-
if (opts.team !== void 0) {
|
|
9876
|
-
await setSetting(db, "repo.team", opts.team);
|
|
9877
|
-
console.log(`Set repo.team = ${opts.team}`);
|
|
9878
|
-
changed = true;
|
|
9879
|
-
}
|
|
9880
|
-
if (opts.org !== void 0) {
|
|
9881
|
-
await setSetting(db, "repo.org", opts.org);
|
|
9882
|
-
console.log(`Set repo.org = ${opts.org}`);
|
|
9883
|
-
changed = true;
|
|
9884
|
-
}
|
|
9885
|
-
if (changed) {
|
|
9886
|
-
console.log("\nUpdated Repository Settings:\n");
|
|
9887
|
-
} else {
|
|
9888
|
-
console.log("Repository Settings:\n");
|
|
9889
|
-
}
|
|
9890
|
-
const paths = await getRepoPaths(db);
|
|
9891
|
-
console.log(
|
|
9892
|
-
`Personal Repo: ${paths.personal ? `\x1B[32m${paths.personal}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
|
|
9893
|
-
);
|
|
9894
|
-
console.log(
|
|
9895
|
-
`Team Repo: ${paths.team ? `\x1B[32m${paths.team}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
|
|
9896
|
-
);
|
|
9897
|
-
console.log(
|
|
9898
|
-
`Org Repo: ${paths.org ? `\x1B[32m${paths.org}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
|
|
9899
|
-
);
|
|
9900
|
-
console.log("\nValidation:");
|
|
9901
|
-
for (const [name, path] of Object.entries(paths)) {
|
|
9902
|
-
if (path) {
|
|
9903
|
-
const exists = existsSync16(path);
|
|
9904
|
-
console.log(
|
|
9905
|
-
` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
|
|
9906
|
-
);
|
|
9907
|
-
} else {
|
|
9908
|
-
console.log(` ${name.padEnd(9)}: \x1B[33m- Not Set\x1B[0m`);
|
|
9909
|
-
}
|
|
9910
|
-
}
|
|
9911
|
-
});
|
|
9912
|
-
});
|
|
9913
|
-
|
|
9914
|
-
// src/cli/commands/setup.ts
|
|
9915
|
-
import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
9916
|
-
import { basename as basename4, dirname as dirname6, join as join18 } from "path";
|
|
9917
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9918
|
-
import { Command as Command15 } from "commander";
|
|
9919
|
-
var packageRoot = [
|
|
9920
|
-
fileURLToPath2(new URL("../..", import.meta.url)),
|
|
9921
|
-
fileURLToPath2(new URL("../../..", import.meta.url))
|
|
9922
|
-
].find((candidate) => existsSync17(join18(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
|
|
9923
|
-
var SKILL_PAIRS = [
|
|
9924
|
-
{
|
|
9925
|
-
from: join18(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
|
|
9926
|
-
to: join18(".claude", "skills", "zam", "SKILL.md")
|
|
9927
|
-
},
|
|
9928
|
-
{
|
|
9929
|
-
from: join18(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
|
|
9930
|
-
to: join18(".agent", "skills", "zam", "SKILL.md")
|
|
9931
|
-
},
|
|
9932
|
-
{
|
|
9933
|
-
from: join18(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
|
|
9934
|
-
to: join18(".agents", "skills", "zam", "SKILL.md")
|
|
9935
|
-
}
|
|
9936
|
-
];
|
|
9937
|
-
function copySkills(force, cwd = process.cwd()) {
|
|
9938
|
-
let anyAction = false;
|
|
9939
|
-
for (const { from, to } of SKILL_PAIRS) {
|
|
9940
|
-
const dest = join18(cwd, to);
|
|
9941
|
-
if (!existsSync17(from)) {
|
|
9942
|
-
console.warn(` warn source not found, skipping: ${from}`);
|
|
9943
|
-
continue;
|
|
9944
|
-
}
|
|
9945
|
-
if (existsSync17(dest) && !force) {
|
|
9946
|
-
console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
|
|
9947
|
-
continue;
|
|
9948
|
-
}
|
|
9949
|
-
mkdirSync10(dirname6(dest), { recursive: true });
|
|
9950
|
-
copyFileSync2(from, dest);
|
|
9951
|
-
console.log(` copy ${to}`);
|
|
9952
|
-
anyAction = true;
|
|
9953
|
-
}
|
|
9954
|
-
if (!anyAction && !force) {
|
|
9955
|
-
console.log(
|
|
9956
|
-
"\nSkill files are already up to date. Run with --force to overwrite."
|
|
9957
|
-
);
|
|
9958
|
-
}
|
|
9959
|
-
}
|
|
9960
|
-
function formatDatabaseInitTarget(target) {
|
|
9961
|
-
switch (target.kind) {
|
|
9962
|
-
case "local":
|
|
9963
|
-
return `ZAM database at ${target.location} (local SQLite)`;
|
|
9964
|
-
case "turso-remote":
|
|
9965
|
-
return `ZAM database via Turso remote at ${target.location}`;
|
|
9966
|
-
case "turso-native":
|
|
9967
|
-
return `ZAM database via Turso native driver at ${target.location}`;
|
|
9968
|
-
case "turso-replica":
|
|
9969
|
-
return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
|
|
9970
|
-
}
|
|
9971
|
-
}
|
|
9972
|
-
async function initDatabase(skipInit) {
|
|
9973
|
-
if (skipInit) return;
|
|
9974
|
-
try {
|
|
9975
|
-
const target = getDatabaseTargetInfo();
|
|
9976
|
-
const db = await openDatabaseWithSync({ initialize: true });
|
|
9977
|
-
await db.close();
|
|
9978
|
-
console.log(` init ${formatDatabaseInitTarget(target)}`);
|
|
9979
|
-
} catch (err) {
|
|
9980
|
-
const msg = err.message;
|
|
9981
|
-
if (!msg.includes("already")) {
|
|
9982
|
-
console.warn(` warn database init: ${msg}`);
|
|
11762
|
+
await setSetting(db, "system.locale", lower);
|
|
11763
|
+
console.log(`Language set to: \x1B[32m${lower}\x1B[0m`);
|
|
11764
|
+
});
|
|
11765
|
+
});
|
|
11766
|
+
settingsCommand.command("repos").description("Show or set Personal, Team, and Organization repository paths").option("--personal <path>", "Set the Personal Repository path").option("--team <path>", "Set the Team Repository path").option("--org <path>", "Set the Organization Repository path").action(async (opts) => {
|
|
11767
|
+
await withDb(async (db) => {
|
|
11768
|
+
let changed = false;
|
|
11769
|
+
if (opts.personal !== void 0) {
|
|
11770
|
+
await setSetting(db, "repo.personal", opts.personal);
|
|
11771
|
+
console.log(`Set repo.personal = ${opts.personal}`);
|
|
11772
|
+
changed = true;
|
|
11773
|
+
}
|
|
11774
|
+
if (opts.team !== void 0) {
|
|
11775
|
+
await setSetting(db, "repo.team", opts.team);
|
|
11776
|
+
console.log(`Set repo.team = ${opts.team}`);
|
|
11777
|
+
changed = true;
|
|
11778
|
+
}
|
|
11779
|
+
if (opts.org !== void 0) {
|
|
11780
|
+
await setSetting(db, "repo.org", opts.org);
|
|
11781
|
+
console.log(`Set repo.org = ${opts.org}`);
|
|
11782
|
+
changed = true;
|
|
11783
|
+
}
|
|
11784
|
+
if (changed) {
|
|
11785
|
+
console.log("\nUpdated Repository Settings:\n");
|
|
9983
11786
|
} else {
|
|
9984
|
-
console.log(
|
|
11787
|
+
console.log("Repository Settings:\n");
|
|
9985
11788
|
}
|
|
9986
|
-
|
|
9987
|
-
}
|
|
9988
|
-
function writeClaudeMd(skipClaudeMd, cwd = process.cwd()) {
|
|
9989
|
-
if (skipClaudeMd) return;
|
|
9990
|
-
const dest = join18(cwd, "CLAUDE.md");
|
|
9991
|
-
if (existsSync17(dest)) {
|
|
9992
|
-
console.log(` skip CLAUDE.md (already present)`);
|
|
9993
|
-
return;
|
|
9994
|
-
}
|
|
9995
|
-
const name = basename4(cwd);
|
|
9996
|
-
writeFileSync9(
|
|
9997
|
-
dest,
|
|
9998
|
-
`# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
|
|
9999
|
-
|
|
10000
|
-
This is a ZAM personal instance. ZAM builds lasting skills through spaced
|
|
10001
|
-
repetition during real work \xE2\u20AC\u201D not separate study sessions.
|
|
10002
|
-
|
|
10003
|
-
## First time here?
|
|
10004
|
-
Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
|
|
10005
|
-
|
|
10006
|
-
## Regular use
|
|
10007
|
-
Run \`/zam\` to start a learning session on whatever you are working on.
|
|
10008
|
-
|
|
10009
|
-
## What lives here
|
|
10010
|
-
- \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
|
|
10011
|
-
- \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
|
|
10012
|
-
|
|
10013
|
-
## Fast-changing data
|
|
10014
|
-
Learning tokens, cards, and review history live in local SQLite by default.
|
|
10015
|
-
Use \`zam connector setup turso\` to store cloud credentials in
|
|
10016
|
-
\`~/.zam/credentials.json\` and use a Turso database across machines.
|
|
10017
|
-
`,
|
|
10018
|
-
"utf8"
|
|
10019
|
-
);
|
|
10020
|
-
console.log(` write CLAUDE.md`);
|
|
10021
|
-
}
|
|
10022
|
-
function writeAgentsMd(skipAgentsMd, cwd = process.cwd()) {
|
|
10023
|
-
if (skipAgentsMd) return;
|
|
10024
|
-
const dest = join18(cwd, "AGENTS.md");
|
|
10025
|
-
if (existsSync17(dest)) {
|
|
10026
|
-
console.log(` skip AGENTS.md (already present)`);
|
|
10027
|
-
return;
|
|
10028
|
-
}
|
|
10029
|
-
const name = basename4(cwd);
|
|
10030
|
-
writeFileSync9(
|
|
10031
|
-
dest,
|
|
10032
|
-
`# ZAM Personal Kernel - ${name}
|
|
10033
|
-
|
|
10034
|
-
This is a ZAM personal instance. ZAM builds lasting skills through spaced
|
|
10035
|
-
repetition during real work, not separate study sessions.
|
|
10036
|
-
|
|
10037
|
-
## First time here?
|
|
10038
|
-
Run \`zam setup\` from the shell. When this repository includes
|
|
10039
|
-
\`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
|
|
10040
|
-
or invoke \`$setup\`.
|
|
10041
|
-
|
|
10042
|
-
## Regular use
|
|
10043
|
-
Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
|
|
10044
|
-
learning session on whatever you are working on.
|
|
10045
|
-
|
|
10046
|
-
## What lives here
|
|
10047
|
-
- \`beliefs/\` - your worldview, approved by git commit
|
|
10048
|
-
- \`goals/\` - your objectives, decomposed into tasks and learning tokens
|
|
10049
|
-
|
|
10050
|
-
## Fast-changing data
|
|
10051
|
-
Learning tokens, cards, and review history live in local SQLite by default.
|
|
10052
|
-
Use \`zam connector setup turso\` to store cloud credentials in
|
|
10053
|
-
\`~/.zam/credentials.json\` and use a Turso database across machines.
|
|
10054
|
-
|
|
10055
|
-
## Codex skills
|
|
10056
|
-
Codex discovers repository skills under \`.agents/skills/\`. Run
|
|
10057
|
-
\`zam setup --force\` after upgrading \`zam-core\` to refresh them.
|
|
10058
|
-
`,
|
|
10059
|
-
"utf8"
|
|
10060
|
-
);
|
|
10061
|
-
console.log(` write AGENTS.md`);
|
|
10062
|
-
}
|
|
10063
|
-
var setupCommand = new Command15("setup").description(
|
|
10064
|
-
"Distribute ZAM skill files into this personal instance and initialize the database"
|
|
10065
|
-
).option(
|
|
10066
|
-
"--force",
|
|
10067
|
-
"overwrite existing skill files (use after upgrading zam)",
|
|
10068
|
-
false
|
|
10069
|
-
).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).action(
|
|
10070
|
-
async (opts) => {
|
|
10071
|
-
console.log(`Setting up ZAM in ${process.cwd()}
|
|
10072
|
-
`);
|
|
10073
|
-
copySkills(opts.force);
|
|
10074
|
-
await initDatabase(opts.skipInit);
|
|
10075
|
-
writeClaudeMd(opts.skipClaudeMd);
|
|
10076
|
-
writeAgentsMd(opts.skipAgentsMd);
|
|
11789
|
+
const paths = await getRepoPaths(db);
|
|
10077
11790
|
console.log(
|
|
10078
|
-
|
|
11791
|
+
`Personal Repo: ${paths.personal ? `\x1B[32m${paths.personal}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
|
|
10079
11792
|
);
|
|
10080
|
-
|
|
10081
|
-
|
|
11793
|
+
console.log(
|
|
11794
|
+
`Team Repo: ${paths.team ? `\x1B[32m${paths.team}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
|
|
11795
|
+
);
|
|
11796
|
+
console.log(
|
|
11797
|
+
`Org Repo: ${paths.org ? `\x1B[32m${paths.org}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
|
|
11798
|
+
);
|
|
11799
|
+
console.log("\nValidation:");
|
|
11800
|
+
for (const [name, path] of Object.entries(paths)) {
|
|
11801
|
+
if (path) {
|
|
11802
|
+
const exists = existsSync21(path);
|
|
11803
|
+
console.log(
|
|
11804
|
+
` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
|
|
11805
|
+
);
|
|
11806
|
+
} else {
|
|
11807
|
+
console.log(` ${name.padEnd(9)}: \x1B[33m- Not Set\x1B[0m`);
|
|
11808
|
+
}
|
|
11809
|
+
}
|
|
11810
|
+
});
|
|
11811
|
+
});
|
|
10082
11812
|
|
|
10083
11813
|
// src/cli/commands/skill.ts
|
|
10084
|
-
import { Command as
|
|
10085
|
-
var skillCommand = new
|
|
11814
|
+
import { Command as Command18 } from "commander";
|
|
11815
|
+
var skillCommand = new Command18("skill").description(
|
|
10086
11816
|
"Manage agent skill entries (task recipes)"
|
|
10087
11817
|
);
|
|
10088
11818
|
skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
|
|
@@ -10167,10 +11897,10 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
|
|
|
10167
11897
|
});
|
|
10168
11898
|
|
|
10169
11899
|
// src/cli/commands/snapshot.ts
|
|
10170
|
-
import { existsSync as
|
|
10171
|
-
import { homedir as
|
|
10172
|
-
import { dirname as dirname7, join as
|
|
10173
|
-
import { Command as
|
|
11900
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
|
|
11901
|
+
import { homedir as homedir13 } from "os";
|
|
11902
|
+
import { dirname as dirname7, join as join21 } from "path";
|
|
11903
|
+
import { Command as Command19 } from "commander";
|
|
10174
11904
|
function defaultOutName() {
|
|
10175
11905
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
|
|
10176
11906
|
return `zam-snapshot-${stamp}.sql`;
|
|
@@ -10184,12 +11914,12 @@ function summarize(tables) {
|
|
|
10184
11914
|
}
|
|
10185
11915
|
return { total, nonEmpty };
|
|
10186
11916
|
}
|
|
10187
|
-
var exportCmd = new
|
|
11917
|
+
var exportCmd = new Command19("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
|
|
10188
11918
|
let db;
|
|
10189
11919
|
try {
|
|
10190
11920
|
db = await openDatabaseWithSync({ initialize: true });
|
|
10191
11921
|
const snapshot = await exportSnapshot(db);
|
|
10192
|
-
const personalDir = await getSetting(db, "personal.workspace_dir") ||
|
|
11922
|
+
const personalDir = await getSetting(db, "personal.workspace_dir") || join21(homedir13(), "Documents", "zam");
|
|
10193
11923
|
await db.close();
|
|
10194
11924
|
db = void 0;
|
|
10195
11925
|
const manifest = verifySnapshot(snapshot);
|
|
@@ -10198,12 +11928,12 @@ var exportCmd = new Command17("export").description("Write a portable SQL-text s
|
|
|
10198
11928
|
process.stdout.write(snapshot);
|
|
10199
11929
|
return;
|
|
10200
11930
|
}
|
|
10201
|
-
const out = opts.out ??
|
|
11931
|
+
const out = opts.out ?? join21(personalDir, "snapshots", defaultOutName());
|
|
10202
11932
|
const dir = dirname7(out);
|
|
10203
|
-
if (dir && dir !== "." && !
|
|
10204
|
-
|
|
11933
|
+
if (dir && dir !== "." && !existsSync22(dir)) {
|
|
11934
|
+
mkdirSync12(dir, { recursive: true });
|
|
10205
11935
|
}
|
|
10206
|
-
|
|
11936
|
+
writeFileSync11(out, snapshot, "utf-8");
|
|
10207
11937
|
console.log(`Snapshot written: ${out}`);
|
|
10208
11938
|
console.log(
|
|
10209
11939
|
` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
|
|
@@ -10214,14 +11944,14 @@ var exportCmd = new Command17("export").description("Write a portable SQL-text s
|
|
|
10214
11944
|
process.exit(1);
|
|
10215
11945
|
}
|
|
10216
11946
|
});
|
|
10217
|
-
var importCmd = new
|
|
11947
|
+
var importCmd = new Command19("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) => {
|
|
10218
11948
|
let db;
|
|
10219
11949
|
try {
|
|
10220
|
-
if (!
|
|
11950
|
+
if (!existsSync22(file)) {
|
|
10221
11951
|
console.error(`Error: Snapshot file not found: ${file}`);
|
|
10222
11952
|
process.exit(1);
|
|
10223
11953
|
}
|
|
10224
|
-
const snapshot =
|
|
11954
|
+
const snapshot = readFileSync13(file, "utf-8");
|
|
10225
11955
|
db = await openDatabaseWithSync({ initialize: true });
|
|
10226
11956
|
const result = await importSnapshot(db, snapshot, { force: opts.force });
|
|
10227
11957
|
await db.close();
|
|
@@ -10237,13 +11967,13 @@ var importCmd = new Command17("import").description("Restore a snapshot into the
|
|
|
10237
11967
|
process.exit(1);
|
|
10238
11968
|
}
|
|
10239
11969
|
});
|
|
10240
|
-
var verifyCmd = new
|
|
11970
|
+
var verifyCmd = new Command19("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
|
|
10241
11971
|
try {
|
|
10242
|
-
if (!
|
|
11972
|
+
if (!existsSync22(file)) {
|
|
10243
11973
|
console.error(`Error: Snapshot file not found: ${file}`);
|
|
10244
11974
|
process.exit(1);
|
|
10245
11975
|
}
|
|
10246
|
-
const manifest = verifySnapshot(
|
|
11976
|
+
const manifest = verifySnapshot(readFileSync13(file, "utf-8"));
|
|
10247
11977
|
const { total, nonEmpty } = summarize(manifest.tables);
|
|
10248
11978
|
console.log(`Valid snapshot (format v${manifest.version})`);
|
|
10249
11979
|
console.log(` created: ${manifest.createdAt}`);
|
|
@@ -10255,11 +11985,11 @@ var verifyCmd = new Command17("verify").description("Check a snapshot's manifest
|
|
|
10255
11985
|
process.exit(1);
|
|
10256
11986
|
}
|
|
10257
11987
|
});
|
|
10258
|
-
var snapshotCommand = new
|
|
11988
|
+
var snapshotCommand = new Command19("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
|
|
10259
11989
|
|
|
10260
11990
|
// src/cli/commands/stats.ts
|
|
10261
|
-
import { Command as
|
|
10262
|
-
var statsCommand = new
|
|
11991
|
+
import { Command as Command20 } from "commander";
|
|
11992
|
+
var statsCommand = new Command20("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
|
|
10263
11993
|
await withDb(async (db) => {
|
|
10264
11994
|
const userId = await resolveUser(opts, db);
|
|
10265
11995
|
const stats = await getUserStats(db, userId);
|
|
@@ -10295,8 +12025,8 @@ var statsCommand = new Command18("stats").description("Show learning dashboard f
|
|
|
10295
12025
|
});
|
|
10296
12026
|
|
|
10297
12027
|
// src/cli/commands/token.ts
|
|
10298
|
-
import { Command as
|
|
10299
|
-
var tokenCommand = new
|
|
12028
|
+
import { Command as Command21 } from "commander";
|
|
12029
|
+
var tokenCommand = new Command21("token").description(
|
|
10300
12030
|
"Manage knowledge tokens"
|
|
10301
12031
|
);
|
|
10302
12032
|
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) => {
|
|
@@ -10582,12 +12312,12 @@ tokenCommand.command("status").description("Show full status of a token for a us
|
|
|
10582
12312
|
});
|
|
10583
12313
|
|
|
10584
12314
|
// src/cli/commands/ui.ts
|
|
10585
|
-
import { spawn as
|
|
10586
|
-
import { existsSync as
|
|
10587
|
-
import { homedir as
|
|
10588
|
-
import { dirname as dirname8, join as
|
|
12315
|
+
import { spawn as spawn3, spawnSync } from "child_process";
|
|
12316
|
+
import { existsSync as existsSync23 } from "fs";
|
|
12317
|
+
import { homedir as homedir14 } from "os";
|
|
12318
|
+
import { dirname as dirname8, join as join22 } from "path";
|
|
10589
12319
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10590
|
-
import { Command as
|
|
12320
|
+
import { Command as Command22 } from "commander";
|
|
10591
12321
|
var C3 = {
|
|
10592
12322
|
reset: "\x1B[0m",
|
|
10593
12323
|
red: "\x1B[31m",
|
|
@@ -10601,8 +12331,8 @@ function findDesktopDir() {
|
|
|
10601
12331
|
for (const start of starts) {
|
|
10602
12332
|
let dir = start;
|
|
10603
12333
|
for (let i = 0; i < 10; i++) {
|
|
10604
|
-
if (
|
|
10605
|
-
return
|
|
12334
|
+
if (existsSync23(join22(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
|
|
12335
|
+
return join22(dir, "desktop");
|
|
10606
12336
|
}
|
|
10607
12337
|
const parent = dirname8(dir);
|
|
10608
12338
|
if (parent === dir) break;
|
|
@@ -10612,30 +12342,30 @@ function findDesktopDir() {
|
|
|
10612
12342
|
return null;
|
|
10613
12343
|
}
|
|
10614
12344
|
function findBuiltApp(desktopDir) {
|
|
10615
|
-
const releaseDir =
|
|
12345
|
+
const releaseDir = join22(desktopDir, "src-tauri", "target", "release");
|
|
10616
12346
|
if (process.platform === "win32") {
|
|
10617
12347
|
for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
|
|
10618
|
-
const p =
|
|
10619
|
-
if (
|
|
12348
|
+
const p = join22(releaseDir, name);
|
|
12349
|
+
if (existsSync23(p)) return p;
|
|
10620
12350
|
}
|
|
10621
12351
|
} else if (process.platform === "darwin") {
|
|
10622
|
-
const app =
|
|
10623
|
-
if (
|
|
12352
|
+
const app = join22(releaseDir, "bundle", "macos", "ZAM.app");
|
|
12353
|
+
if (existsSync23(app)) return app;
|
|
10624
12354
|
} else {
|
|
10625
12355
|
for (const name of ["zam", "ZAM", "zam-desktop"]) {
|
|
10626
|
-
const p =
|
|
10627
|
-
if (
|
|
12356
|
+
const p = join22(releaseDir, name);
|
|
12357
|
+
if (existsSync23(p)) return p;
|
|
10628
12358
|
}
|
|
10629
12359
|
}
|
|
10630
12360
|
return null;
|
|
10631
12361
|
}
|
|
10632
12362
|
function findInstalledApp() {
|
|
10633
12363
|
const candidates = process.platform === "win32" ? [
|
|
10634
|
-
process.env.LOCALAPPDATA &&
|
|
10635
|
-
process.env.ProgramFiles &&
|
|
10636
|
-
process.env["ProgramFiles(x86)"] &&
|
|
10637
|
-
] : process.platform === "darwin" ? ["/Applications/ZAM.app",
|
|
10638
|
-
return candidates.find((candidate) => candidate &&
|
|
12364
|
+
process.env.LOCALAPPDATA && join22(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
|
|
12365
|
+
process.env.ProgramFiles && join22(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
|
|
12366
|
+
process.env["ProgramFiles(x86)"] && join22(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
|
|
12367
|
+
] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(homedir14(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
|
|
12368
|
+
return candidates.find((candidate) => candidate && existsSync23(candidate)) || null;
|
|
10639
12369
|
}
|
|
10640
12370
|
function runNpm(args, opts) {
|
|
10641
12371
|
const res = spawnSync("npm", args, {
|
|
@@ -10646,7 +12376,7 @@ function runNpm(args, opts) {
|
|
|
10646
12376
|
return res.status ?? 1;
|
|
10647
12377
|
}
|
|
10648
12378
|
function ensureDesktopDeps(desktopDir) {
|
|
10649
|
-
if (
|
|
12379
|
+
if (existsSync23(join22(desktopDir, "node_modules"))) return true;
|
|
10650
12380
|
console.log(
|
|
10651
12381
|
`${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
|
|
10652
12382
|
);
|
|
@@ -10672,13 +12402,13 @@ function requireRust() {
|
|
|
10672
12402
|
function hasMsvcBuildTools() {
|
|
10673
12403
|
if (process.platform !== "win32") return true;
|
|
10674
12404
|
const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
|
10675
|
-
const vswhere =
|
|
12405
|
+
const vswhere = join22(
|
|
10676
12406
|
pf86,
|
|
10677
12407
|
"Microsoft Visual Studio",
|
|
10678
12408
|
"Installer",
|
|
10679
12409
|
"vswhere.exe"
|
|
10680
12410
|
);
|
|
10681
|
-
if (!
|
|
12411
|
+
if (!existsSync23(vswhere)) return false;
|
|
10682
12412
|
const res = spawnSync(
|
|
10683
12413
|
vswhere,
|
|
10684
12414
|
[
|
|
@@ -10710,7 +12440,7 @@ function requireMsvcOnWindows() {
|
|
|
10710
12440
|
return false;
|
|
10711
12441
|
}
|
|
10712
12442
|
function warnIfCliMissing(repoRoot) {
|
|
10713
|
-
if (!
|
|
12443
|
+
if (!existsSync23(join22(repoRoot, "dist", "cli", "index.js"))) {
|
|
10714
12444
|
console.warn(
|
|
10715
12445
|
`${C3.yellow}\u26A0 CLI build not found (dist/cli/index.js). The GUI bridge needs it \u2014 run 'npm run build' at the repo root.${C3.reset}`
|
|
10716
12446
|
);
|
|
@@ -10719,13 +12449,13 @@ function warnIfCliMissing(repoRoot) {
|
|
|
10719
12449
|
function launchApp(appPath, workingDir) {
|
|
10720
12450
|
console.log(`${C3.green}\u2713 Launching ZAM Desktop...${C3.reset}`);
|
|
10721
12451
|
if (process.platform === "darwin" && appPath.endsWith(".app")) {
|
|
10722
|
-
|
|
12452
|
+
spawn3("open", [appPath], {
|
|
10723
12453
|
cwd: workingDir,
|
|
10724
12454
|
detached: true,
|
|
10725
12455
|
stdio: "ignore"
|
|
10726
12456
|
}).unref();
|
|
10727
12457
|
} else {
|
|
10728
|
-
|
|
12458
|
+
spawn3(appPath, [], {
|
|
10729
12459
|
cwd: workingDir,
|
|
10730
12460
|
detached: true,
|
|
10731
12461
|
stdio: "ignore",
|
|
@@ -10767,13 +12497,13 @@ function createShortcuts(appPath, repoRoot) {
|
|
|
10767
12497
|
console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
|
|
10768
12498
|
}
|
|
10769
12499
|
}
|
|
10770
|
-
var uiCommand = new
|
|
12500
|
+
var uiCommand = new Command22("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
|
|
10771
12501
|
"--build",
|
|
10772
12502
|
"Build the native installer for your OS (needs Rust, one-time)"
|
|
10773
12503
|
).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
|
|
10774
12504
|
const installedApp = findInstalledApp();
|
|
10775
12505
|
if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
|
|
10776
|
-
launchApp(installedApp,
|
|
12506
|
+
launchApp(installedApp, homedir14());
|
|
10777
12507
|
return;
|
|
10778
12508
|
}
|
|
10779
12509
|
const desktopDir = findDesktopDir();
|
|
@@ -10794,7 +12524,7 @@ var uiCommand = new Command20("ui").description("Launch the ZAM Desktop GUI (Act
|
|
|
10794
12524
|
);
|
|
10795
12525
|
const code = runNpm(["run", "tauri", "build"], { cwd: desktopDir });
|
|
10796
12526
|
if (code === 0) {
|
|
10797
|
-
const bundle =
|
|
12527
|
+
const bundle = join22(
|
|
10798
12528
|
desktopDir,
|
|
10799
12529
|
"src-tauri",
|
|
10800
12530
|
"target",
|
|
@@ -10871,11 +12601,11 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
|
|
|
10871
12601
|
|
|
10872
12602
|
// src/cli/commands/update.ts
|
|
10873
12603
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
10874
|
-
import { existsSync as
|
|
10875
|
-
import { dirname as dirname9, join as
|
|
12604
|
+
import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync } from "fs";
|
|
12605
|
+
import { dirname as dirname9, join as join23 } from "path";
|
|
10876
12606
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
10877
|
-
import { confirm as
|
|
10878
|
-
import { Command as
|
|
12607
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
12608
|
+
import { Command as Command23 } from "commander";
|
|
10879
12609
|
var GITHUB_REPO = "zam-os/zam";
|
|
10880
12610
|
var CHANNELS = [
|
|
10881
12611
|
"developer",
|
|
@@ -10897,7 +12627,7 @@ function currentVersion() {
|
|
|
10897
12627
|
for (const up of ["..", "../..", "../../.."]) {
|
|
10898
12628
|
try {
|
|
10899
12629
|
const pkg2 = JSON.parse(
|
|
10900
|
-
|
|
12630
|
+
readFileSync14(join23(here, up, "package.json"), "utf-8")
|
|
10901
12631
|
);
|
|
10902
12632
|
if (pkg2.version) return pkg2.version;
|
|
10903
12633
|
} catch {
|
|
@@ -10908,7 +12638,7 @@ function currentVersion() {
|
|
|
10908
12638
|
function versionAt(dir) {
|
|
10909
12639
|
try {
|
|
10910
12640
|
const pkg2 = JSON.parse(
|
|
10911
|
-
|
|
12641
|
+
readFileSync14(join23(dir, "package.json"), "utf-8")
|
|
10912
12642
|
);
|
|
10913
12643
|
return pkg2.version ?? "unknown";
|
|
10914
12644
|
} catch {
|
|
@@ -10953,7 +12683,7 @@ function render2(decision) {
|
|
|
10953
12683
|
);
|
|
10954
12684
|
}
|
|
10955
12685
|
}
|
|
10956
|
-
var checkCmd = new
|
|
12686
|
+
var checkCmd = new Command23("check").description("Check whether a newer ZAM has been released").option(
|
|
10957
12687
|
"--latest <version>",
|
|
10958
12688
|
"Compare against this version instead of fetching"
|
|
10959
12689
|
).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
|
|
@@ -10988,13 +12718,13 @@ function findSourceRepo() {
|
|
|
10988
12718
|
let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
|
|
10989
12719
|
let parent = dirname9(dir);
|
|
10990
12720
|
while (parent !== dir) {
|
|
10991
|
-
if (
|
|
12721
|
+
if (existsSync24(join23(dir, ".git"))) return dir;
|
|
10992
12722
|
dir = parent;
|
|
10993
12723
|
parent = dirname9(dir);
|
|
10994
12724
|
}
|
|
10995
|
-
return
|
|
12725
|
+
return existsSync24(join23(dir, ".git")) ? dir : null;
|
|
10996
12726
|
}
|
|
10997
|
-
function
|
|
12727
|
+
function runGit2(cwd, args, capture) {
|
|
10998
12728
|
const res = spawnSync2("git", args, {
|
|
10999
12729
|
cwd,
|
|
11000
12730
|
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
@@ -11026,7 +12756,7 @@ function applyDeveloperUpdate(force) {
|
|
|
11026
12756
|
);
|
|
11027
12757
|
process.exit(1);
|
|
11028
12758
|
}
|
|
11029
|
-
const status =
|
|
12759
|
+
const status = runGit2(src, ["status", "--porcelain"], true);
|
|
11030
12760
|
if (status.ok && status.out && !force) {
|
|
11031
12761
|
console.error(
|
|
11032
12762
|
`${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
|
|
@@ -11038,7 +12768,7 @@ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
|
|
|
11038
12768
|
console.log(
|
|
11039
12769
|
`${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
|
|
11040
12770
|
);
|
|
11041
|
-
if (!
|
|
12771
|
+
if (!runGit2(src, ["pull", "--ff-only"], false).ok) {
|
|
11042
12772
|
console.error(
|
|
11043
12773
|
`${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
|
|
11044
12774
|
);
|
|
@@ -11057,7 +12787,7 @@ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
|
|
|
11057
12787
|
console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
|
|
11058
12788
|
const setup = spawnSync2(
|
|
11059
12789
|
process.execPath,
|
|
11060
|
-
[
|
|
12790
|
+
[join23(src, "dist", "cli", "index.js"), "setup", "--force"],
|
|
11061
12791
|
{ cwd: process.cwd(), stdio: "inherit" }
|
|
11062
12792
|
);
|
|
11063
12793
|
if (setup.status !== 0) {
|
|
@@ -11100,7 +12830,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
|
|
|
11100
12830
|
return;
|
|
11101
12831
|
}
|
|
11102
12832
|
if (!opts.yes) {
|
|
11103
|
-
const ok = await
|
|
12833
|
+
const ok = await confirm4({
|
|
11104
12834
|
message: "Apply this update?",
|
|
11105
12835
|
default: true
|
|
11106
12836
|
});
|
|
@@ -11120,7 +12850,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
|
|
|
11120
12850
|
process.exit(1);
|
|
11121
12851
|
}
|
|
11122
12852
|
}
|
|
11123
|
-
var updateCommand = new
|
|
12853
|
+
var updateCommand = new Command23("update").description(
|
|
11124
12854
|
"Update ZAM to the latest release (use `update check` to only check)"
|
|
11125
12855
|
).option("-y, --yes", "Apply without confirmation").option(
|
|
11126
12856
|
"--force",
|
|
@@ -11130,8 +12860,8 @@ var updateCommand = new Command21("update").description(
|
|
|
11130
12860
|
}).addCommand(checkCmd);
|
|
11131
12861
|
|
|
11132
12862
|
// src/cli/commands/whoami.ts
|
|
11133
|
-
import { Command as
|
|
11134
|
-
var whoamiCommand = new
|
|
12863
|
+
import { Command as Command24 } from "commander";
|
|
12864
|
+
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) => {
|
|
11135
12865
|
await withDb(async (db) => {
|
|
11136
12866
|
if (opts.set) {
|
|
11137
12867
|
await setSetting(db, "user.id", opts.set);
|
|
@@ -11166,163 +12896,12 @@ var whoamiCommand = new Command22("whoami").description("Show or set the default
|
|
|
11166
12896
|
});
|
|
11167
12897
|
});
|
|
11168
12898
|
|
|
11169
|
-
// src/cli/commands/workspace.ts
|
|
11170
|
-
import { execSync as execSync6 } from "child_process";
|
|
11171
|
-
import { existsSync as existsSync21, writeFileSync as writeFileSync11 } from "fs";
|
|
11172
|
-
import { join as join22 } from "path";
|
|
11173
|
-
import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
|
|
11174
|
-
import { Command as Command23 } from "commander";
|
|
11175
|
-
function runGit2(cwd, args) {
|
|
11176
|
-
try {
|
|
11177
|
-
return execSync6(`git ${args}`, {
|
|
11178
|
-
cwd,
|
|
11179
|
-
stdio: "pipe",
|
|
11180
|
-
encoding: "utf8"
|
|
11181
|
-
}).trim();
|
|
11182
|
-
} catch (err) {
|
|
11183
|
-
throw new Error(`Git command failed: ${err.message}`);
|
|
11184
|
-
}
|
|
11185
|
-
}
|
|
11186
|
-
var workspaceCommand = new Command23("workspace").description(
|
|
11187
|
-
"Manage your ZAM learning workspace"
|
|
11188
|
-
);
|
|
11189
|
-
workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
|
|
11190
|
-
let db;
|
|
11191
|
-
let workspaceDir = "";
|
|
11192
|
-
try {
|
|
11193
|
-
db = await openDatabase();
|
|
11194
|
-
workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
|
|
11195
|
-
await db.close();
|
|
11196
|
-
} catch {
|
|
11197
|
-
await db?.close();
|
|
11198
|
-
}
|
|
11199
|
-
if (!workspaceDir) {
|
|
11200
|
-
console.error(
|
|
11201
|
-
"\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
|
|
11202
|
-
);
|
|
11203
|
-
process.exit(1);
|
|
11204
|
-
}
|
|
11205
|
-
if (!existsSync21(workspaceDir)) {
|
|
11206
|
-
console.error(
|
|
11207
|
-
`\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
|
|
11208
|
-
);
|
|
11209
|
-
process.exit(1);
|
|
11210
|
-
}
|
|
11211
|
-
console.log(`
|
|
11212
|
-
Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
|
|
11213
|
-
if (!hasCommand("git")) {
|
|
11214
|
-
console.error(
|
|
11215
|
-
"\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
|
|
11216
|
-
);
|
|
11217
|
-
process.exit(1);
|
|
11218
|
-
}
|
|
11219
|
-
const gitignorePath = join22(workspaceDir, ".gitignore");
|
|
11220
|
-
if (!existsSync21(gitignorePath)) {
|
|
11221
|
-
writeFileSync11(
|
|
11222
|
-
gitignorePath,
|
|
11223
|
-
"node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
|
|
11224
|
-
"utf8"
|
|
11225
|
-
);
|
|
11226
|
-
}
|
|
11227
|
-
const hasGitRepo = existsSync21(join22(workspaceDir, ".git"));
|
|
11228
|
-
if (!hasGitRepo) {
|
|
11229
|
-
console.log("Initializing local Git repository...");
|
|
11230
|
-
runGit2(workspaceDir, "init -b main");
|
|
11231
|
-
runGit2(workspaceDir, "add .");
|
|
11232
|
-
runGit2(
|
|
11233
|
-
workspaceDir,
|
|
11234
|
-
'commit -m "chore: initial workspace sandbox bootstrap"'
|
|
11235
|
-
);
|
|
11236
|
-
console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
|
|
11237
|
-
} else {
|
|
11238
|
-
console.log("Git repository is already initialized.");
|
|
11239
|
-
}
|
|
11240
|
-
const repoName = await input7({
|
|
11241
|
-
message: "Choose a name for your GitHub repository:",
|
|
11242
|
-
default: "zam-personal"
|
|
11243
|
-
});
|
|
11244
|
-
const isPrivate = await confirm4({
|
|
11245
|
-
message: "Should the repository be private?",
|
|
11246
|
-
default: true
|
|
11247
|
-
});
|
|
11248
|
-
const repoVisibility = isPrivate ? "--private" : "--public";
|
|
11249
|
-
if (hasCommand("gh")) {
|
|
11250
|
-
console.log("GitHub CLI detected! Automating repository creation...");
|
|
11251
|
-
const proceedGh = await confirm4({
|
|
11252
|
-
message: "Would you like ZAM to create the repository using the GitHub CLI?",
|
|
11253
|
-
default: true
|
|
11254
|
-
});
|
|
11255
|
-
if (proceedGh) {
|
|
11256
|
-
try {
|
|
11257
|
-
console.log(`Creating GitHub repository ${repoName}...`);
|
|
11258
|
-
execSync6(
|
|
11259
|
-
`gh repo create ${repoName} ${repoVisibility} --source=. --push`,
|
|
11260
|
-
{
|
|
11261
|
-
cwd: workspaceDir,
|
|
11262
|
-
stdio: "inherit"
|
|
11263
|
-
}
|
|
11264
|
-
);
|
|
11265
|
-
console.log(
|
|
11266
|
-
"\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
|
|
11267
|
-
);
|
|
11268
|
-
process.exit(0);
|
|
11269
|
-
} catch (err) {
|
|
11270
|
-
console.warn(
|
|
11271
|
-
`\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
|
|
11272
|
-
);
|
|
11273
|
-
}
|
|
11274
|
-
}
|
|
11275
|
-
}
|
|
11276
|
-
console.log(
|
|
11277
|
-
"\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
|
|
11278
|
-
);
|
|
11279
|
-
console.log(" 1. Go to https://github.com/new");
|
|
11280
|
-
console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
|
|
11281
|
-
console.log(
|
|
11282
|
-
` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
|
|
11283
|
-
);
|
|
11284
|
-
console.log(
|
|
11285
|
-
" 4. Do NOT initialize it with README, .gitignore, or license"
|
|
11286
|
-
);
|
|
11287
|
-
console.log(" 5. Click 'Create repository'\n");
|
|
11288
|
-
const githubUrl = await input7({
|
|
11289
|
-
message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
|
|
11290
|
-
});
|
|
11291
|
-
if (githubUrl) {
|
|
11292
|
-
try {
|
|
11293
|
-
console.log("Linking remote repository and pushing...");
|
|
11294
|
-
let hasOrigin = false;
|
|
11295
|
-
try {
|
|
11296
|
-
runGit2(workspaceDir, "remote get-url origin");
|
|
11297
|
-
hasOrigin = true;
|
|
11298
|
-
} catch {
|
|
11299
|
-
}
|
|
11300
|
-
if (hasOrigin) {
|
|
11301
|
-
runGit2(workspaceDir, `remote set-url origin ${githubUrl}`);
|
|
11302
|
-
} else {
|
|
11303
|
-
runGit2(workspaceDir, `remote add origin ${githubUrl}`);
|
|
11304
|
-
}
|
|
11305
|
-
runGit2(workspaceDir, "push -u origin main");
|
|
11306
|
-
console.log(
|
|
11307
|
-
"\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
|
|
11308
|
-
);
|
|
11309
|
-
} catch (err) {
|
|
11310
|
-
console.error(
|
|
11311
|
-
`\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
|
|
11312
|
-
);
|
|
11313
|
-
console.log(
|
|
11314
|
-
"You can push manually later using: git push -u origin main"
|
|
11315
|
-
);
|
|
11316
|
-
}
|
|
11317
|
-
}
|
|
11318
|
-
});
|
|
11319
|
-
|
|
11320
12899
|
// src/cli/index.ts
|
|
11321
12900
|
var __dirname = dirname10(fileURLToPath5(import.meta.url));
|
|
11322
12901
|
var pkg = JSON.parse(
|
|
11323
|
-
|
|
12902
|
+
readFileSync15(join24(__dirname, "..", "..", "package.json"), "utf-8")
|
|
11324
12903
|
);
|
|
11325
|
-
var program = new
|
|
12904
|
+
var program = new Command25();
|
|
11326
12905
|
program.name("zam").description(
|
|
11327
12906
|
"The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
|
|
11328
12907
|
).version(pkg.version);
|
|
@@ -11342,6 +12921,7 @@ program.addCommand(observerCommand);
|
|
|
11342
12921
|
program.addCommand(settingsCommand);
|
|
11343
12922
|
program.addCommand(whoamiCommand);
|
|
11344
12923
|
program.addCommand(connectorCommand);
|
|
12924
|
+
program.addCommand(providerCommand);
|
|
11345
12925
|
program.addCommand(snapshotCommand);
|
|
11346
12926
|
program.addCommand(profileCommand);
|
|
11347
12927
|
program.addCommand(updateCommand);
|