zam-core 0.4.2 → 0.4.4
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 +1561 -653
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +40 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { readFileSync as readFileSync14 } from "fs";
|
|
5
|
-
import { dirname as dirname10, join as
|
|
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() {
|
|
@@ -4707,13 +4726,11 @@ function runCommand(cmd) {
|
|
|
4707
4726
|
return "";
|
|
4708
4727
|
}
|
|
4709
4728
|
}
|
|
4710
|
-
function
|
|
4729
|
+
function detectWindowsNPU() {
|
|
4711
4730
|
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
|
|
4731
|
+
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
4732
|
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
|
-
);
|
|
4733
|
+
return Boolean(output && output.length > 0);
|
|
4717
4734
|
}
|
|
4718
4735
|
function getSystemProfile() {
|
|
4719
4736
|
const platform = process.platform;
|
|
@@ -4725,13 +4742,21 @@ function getSystemProfile() {
|
|
|
4725
4742
|
let arch = "unknown";
|
|
4726
4743
|
if (archStr === "x64") arch = "x64";
|
|
4727
4744
|
else if (archStr === "arm64") arch = "arm64";
|
|
4728
|
-
const
|
|
4745
|
+
const hasNpu = os === "windows" && detectWindowsNPU();
|
|
4729
4746
|
const hasAppleSilicon = os === "macos" && arch === "arm64";
|
|
4730
4747
|
let recommendedRunner = "generic";
|
|
4731
4748
|
let recommendedModel = "qwen3.5:4b";
|
|
4732
|
-
if (
|
|
4733
|
-
|
|
4734
|
-
|
|
4749
|
+
if (hasNpu) {
|
|
4750
|
+
const isQualcomm = runCommand(
|
|
4751
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -like '*Qualcomm*' } | Select-Object -First 1"`
|
|
4752
|
+
).length > 0;
|
|
4753
|
+
if (isQualcomm) {
|
|
4754
|
+
recommendedRunner = "generic";
|
|
4755
|
+
recommendedModel = "qwen3.5-4b";
|
|
4756
|
+
} else {
|
|
4757
|
+
recommendedRunner = "fastflowlm";
|
|
4758
|
+
recommendedModel = "qwen3.5:4b";
|
|
4759
|
+
}
|
|
4735
4760
|
} else if (hasAppleSilicon) {
|
|
4736
4761
|
recommendedRunner = "ollama";
|
|
4737
4762
|
recommendedModel = "llama3.2:3b";
|
|
@@ -4742,7 +4767,8 @@ function getSystemProfile() {
|
|
|
4742
4767
|
return {
|
|
4743
4768
|
os,
|
|
4744
4769
|
arch,
|
|
4745
|
-
hasRyzenNPU,
|
|
4770
|
+
hasRyzenNPU: hasNpu && !archStr.includes("arm64"),
|
|
4771
|
+
// backwards compatibility flag
|
|
4746
4772
|
hasAppleSilicon,
|
|
4747
4773
|
recommendedRunner,
|
|
4748
4774
|
recommendedModel
|
|
@@ -4879,6 +4905,289 @@ function planUpdate(decision) {
|
|
|
4879
4905
|
}
|
|
4880
4906
|
}
|
|
4881
4907
|
|
|
4908
|
+
// src/cli/agent-harness.ts
|
|
4909
|
+
import { spawn } from "child_process";
|
|
4910
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4911
|
+
import { homedir as homedir8 } from "os";
|
|
4912
|
+
import { join as join12 } from "path";
|
|
4913
|
+
|
|
4914
|
+
// src/cli/terminal-open.ts
|
|
4915
|
+
import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
|
|
4916
|
+
import {
|
|
4917
|
+
accessSync,
|
|
4918
|
+
constants,
|
|
4919
|
+
existsSync as existsSync11,
|
|
4920
|
+
unlinkSync,
|
|
4921
|
+
writeFileSync as writeFileSync6
|
|
4922
|
+
} from "fs";
|
|
4923
|
+
import { tmpdir } from "os";
|
|
4924
|
+
import { basename as basename2, delimiter, extname, isAbsolute, join as join11 } from "path";
|
|
4925
|
+
function isPowerShellShell(shell) {
|
|
4926
|
+
return shell === "pwsh" || shell === "powershell";
|
|
4927
|
+
}
|
|
4928
|
+
function psSingleQuoted2(value) {
|
|
4929
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
4930
|
+
}
|
|
4931
|
+
function detectShell() {
|
|
4932
|
+
if (process.platform === "win32")
|
|
4933
|
+
return findExecutable("pwsh.exe") ? "pwsh" : "powershell";
|
|
4934
|
+
const shell = process.env.SHELL ?? "";
|
|
4935
|
+
return basename2(shell) === "bash" ? "bash" : "zsh";
|
|
4936
|
+
}
|
|
4937
|
+
function normalizeShell(shell) {
|
|
4938
|
+
if (!shell) return detectShell();
|
|
4939
|
+
const normalized = shell.toLowerCase();
|
|
4940
|
+
if (normalized === "zsh" || normalized === "bash" || normalized === "pwsh" || normalized === "powershell") {
|
|
4941
|
+
return normalized;
|
|
4942
|
+
}
|
|
4943
|
+
throw new Error(
|
|
4944
|
+
`Unsupported shell: ${shell}. Expected zsh, bash, pwsh, or powershell.`
|
|
4945
|
+
);
|
|
4946
|
+
}
|
|
4947
|
+
function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
|
|
4948
|
+
if (results.length === 0) return null;
|
|
4949
|
+
const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
|
|
4950
|
+
const runnable = results.find(
|
|
4951
|
+
(result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
|
|
4952
|
+
);
|
|
4953
|
+
return runnable ?? results[0];
|
|
4954
|
+
}
|
|
4955
|
+
function stripSurroundingQuotes(command) {
|
|
4956
|
+
const trimmed = command.trim();
|
|
4957
|
+
if (trimmed.length < 2) return trimmed;
|
|
4958
|
+
const first = trimmed[0];
|
|
4959
|
+
const last = trimmed[trimmed.length - 1];
|
|
4960
|
+
if (first === '"' && last === '"' || first === "'" && last === "'") {
|
|
4961
|
+
return trimmed.slice(1, -1);
|
|
4962
|
+
}
|
|
4963
|
+
return trimmed;
|
|
4964
|
+
}
|
|
4965
|
+
function executableExists(path) {
|
|
4966
|
+
if (!existsSync11(path)) return false;
|
|
4967
|
+
if (process.platform === "win32") return true;
|
|
4968
|
+
try {
|
|
4969
|
+
accessSync(path, constants.X_OK);
|
|
4970
|
+
return true;
|
|
4971
|
+
} catch {
|
|
4972
|
+
return false;
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
function windowsExecutableNames(command) {
|
|
4976
|
+
if (process.platform !== "win32") return [command];
|
|
4977
|
+
if (extname(command)) return [command];
|
|
4978
|
+
const extensions = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => ext.trim()).filter(Boolean);
|
|
4979
|
+
return extensions.map((ext) => `${command}${ext}`);
|
|
4980
|
+
}
|
|
4981
|
+
function hasDirectoryPart(command) {
|
|
4982
|
+
return isAbsolute(command) || command.includes("/") || command.includes("\\");
|
|
4983
|
+
}
|
|
4984
|
+
function findExecutable(command) {
|
|
4985
|
+
const normalized = stripSurroundingQuotes(command);
|
|
4986
|
+
if (!normalized) return null;
|
|
4987
|
+
const matches = [];
|
|
4988
|
+
if (hasDirectoryPart(normalized)) {
|
|
4989
|
+
for (const candidate of windowsExecutableNames(normalized)) {
|
|
4990
|
+
if (executableExists(candidate)) matches.push(candidate);
|
|
4991
|
+
}
|
|
4992
|
+
return process.platform === "win32" ? selectWindowsExecutable(matches) : matches[0] ?? null;
|
|
4993
|
+
}
|
|
4994
|
+
const pathEntries = (process.env.PATH ?? "").split(delimiter).map((entry) => entry.trim()).filter(Boolean);
|
|
4995
|
+
for (const entry of pathEntries) {
|
|
4996
|
+
for (const name of windowsExecutableNames(normalized)) {
|
|
4997
|
+
const candidate = join11(entry, name);
|
|
4998
|
+
if (executableExists(candidate)) matches.push(candidate);
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
return process.platform === "win32" ? selectWindowsExecutable(matches) : matches[0] ?? null;
|
|
5002
|
+
}
|
|
5003
|
+
function resolveZamInvocation(shell) {
|
|
5004
|
+
const installed = findExecutable("zam");
|
|
5005
|
+
if (installed) {
|
|
5006
|
+
return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
|
|
5007
|
+
}
|
|
5008
|
+
const projectRoot = join11(import.meta.dirname, "..", "..");
|
|
5009
|
+
const cliSource = join11(projectRoot, "src/cli/index.ts");
|
|
5010
|
+
if (isPowerShellShell(shell)) {
|
|
5011
|
+
return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
|
|
5012
|
+
}
|
|
5013
|
+
return `npx --prefix ${JSON.stringify(projectRoot)} tsx ${JSON.stringify(cliSource)}`;
|
|
5014
|
+
}
|
|
5015
|
+
function buildShellSetupCommand(dir, shell, command) {
|
|
5016
|
+
if (isPowerShellShell(shell)) {
|
|
5017
|
+
return [`Set-Location -LiteralPath ${psSingleQuoted2(dir)}`, command].join(
|
|
5018
|
+
"; "
|
|
5019
|
+
);
|
|
5020
|
+
}
|
|
5021
|
+
return `cd ${JSON.stringify(dir)} && ${command}`;
|
|
5022
|
+
}
|
|
5023
|
+
function isItermRunning() {
|
|
5024
|
+
try {
|
|
5025
|
+
const result = execSync4(
|
|
5026
|
+
`osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"' 2>/dev/null`,
|
|
5027
|
+
{ encoding: "utf-8" }
|
|
5028
|
+
).trim();
|
|
5029
|
+
return result === "true";
|
|
5030
|
+
} catch {
|
|
5031
|
+
return false;
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
function openMacTerminal(shellSetup, label, dir) {
|
|
5035
|
+
const useIterm = isItermRunning();
|
|
5036
|
+
const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
5037
|
+
const appleScript = useIterm ? `tell application "iTerm2"
|
|
5038
|
+
activate
|
|
5039
|
+
set newWindow to (create window with default profile)
|
|
5040
|
+
tell current session of newWindow
|
|
5041
|
+
write text "${escaped}"
|
|
5042
|
+
end tell
|
|
5043
|
+
end tell` : `tell application "Terminal"
|
|
5044
|
+
activate
|
|
5045
|
+
do script "${escaped}"
|
|
5046
|
+
end tell`;
|
|
5047
|
+
const tmpFile = join11(tmpdir(), `zam-terminal-${label}.scpt`);
|
|
5048
|
+
try {
|
|
5049
|
+
writeFileSync6(tmpFile, appleScript);
|
|
5050
|
+
execSync4(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
|
|
5051
|
+
console.log(
|
|
5052
|
+
`Opened ${useIterm ? "iTerm2" : "Terminal.app"} window (${label})`
|
|
5053
|
+
);
|
|
5054
|
+
console.log(` Directory: ${dir}`);
|
|
5055
|
+
} catch (err) {
|
|
5056
|
+
console.error(`Failed to open terminal: ${err.message}`);
|
|
5057
|
+
console.log(`
|
|
5058
|
+
Run this manually in a new terminal:
|
|
5059
|
+
`);
|
|
5060
|
+
console.log(` ${shellSetup}`);
|
|
5061
|
+
} finally {
|
|
5062
|
+
try {
|
|
5063
|
+
unlinkSync(tmpFile);
|
|
5064
|
+
} catch {
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
function openWindowsPowerShell(shellSetup, label, dir, requestedShell) {
|
|
5069
|
+
const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
|
|
5070
|
+
const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
|
|
5071
|
+
const startCommand = [
|
|
5072
|
+
"Start-Process",
|
|
5073
|
+
`-FilePath ${psSingleQuoted2(executable)}`,
|
|
5074
|
+
`-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
|
|
5075
|
+
].join(" ");
|
|
5076
|
+
const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
|
|
5077
|
+
try {
|
|
5078
|
+
execFileSync2(
|
|
5079
|
+
launcher,
|
|
5080
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
|
|
5081
|
+
{
|
|
5082
|
+
stdio: "ignore"
|
|
5083
|
+
}
|
|
5084
|
+
);
|
|
5085
|
+
console.log(
|
|
5086
|
+
`Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window (${label})`
|
|
5087
|
+
);
|
|
5088
|
+
console.log(` Directory: ${dir}`);
|
|
5089
|
+
} catch (err) {
|
|
5090
|
+
console.error(`Failed to open PowerShell: ${err.message}`);
|
|
5091
|
+
console.log(`
|
|
5092
|
+
Run this manually in a new PowerShell terminal:
|
|
5093
|
+
`);
|
|
5094
|
+
console.log(` ${shellSetup}`);
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
function openTerminalWindow(opts) {
|
|
5098
|
+
const { shellSetup, label, dir, shell } = opts;
|
|
5099
|
+
if (process.platform === "darwin" && !isPowerShellShell(shell)) {
|
|
5100
|
+
openMacTerminal(shellSetup, label, dir);
|
|
5101
|
+
return;
|
|
5102
|
+
}
|
|
5103
|
+
if (process.platform === "win32" && isPowerShellShell(shell)) {
|
|
5104
|
+
openWindowsPowerShell(shellSetup, label, dir, shell);
|
|
5105
|
+
return;
|
|
5106
|
+
}
|
|
5107
|
+
console.log(`Run this in a new terminal:
|
|
5108
|
+
`);
|
|
5109
|
+
console.log(` ${shellSetup}
|
|
5110
|
+
`);
|
|
5111
|
+
console.log(
|
|
5112
|
+
`(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
|
|
5113
|
+
);
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
// src/cli/agent-harness.ts
|
|
5117
|
+
var AGENT_HARNESSES = [
|
|
5118
|
+
{ id: "claude-code", label: "Claude Code", kind: "cli", command: "claude" },
|
|
5119
|
+
{ id: "codex", label: "Codex", kind: "cli", command: "codex" },
|
|
5120
|
+
{ id: "opencode", label: "opencode", kind: "cli", command: "opencode" },
|
|
5121
|
+
{
|
|
5122
|
+
id: "cursor",
|
|
5123
|
+
label: "Cursor",
|
|
5124
|
+
kind: "app",
|
|
5125
|
+
command: "cursor",
|
|
5126
|
+
candidatePaths: {
|
|
5127
|
+
win32: [
|
|
5128
|
+
join12(homedir8(), "AppData", "Local", "Programs", "cursor", "Cursor.exe")
|
|
5129
|
+
],
|
|
5130
|
+
darwin: ["/Applications/Cursor.app/Contents/MacOS/Cursor"]
|
|
5131
|
+
}
|
|
5132
|
+
},
|
|
5133
|
+
{ id: "copilot", label: "GitHub Copilot", kind: "app", command: "copilot" },
|
|
5134
|
+
{
|
|
5135
|
+
id: "antigravity",
|
|
5136
|
+
label: "Antigravity",
|
|
5137
|
+
kind: "app",
|
|
5138
|
+
command: "antigravity"
|
|
5139
|
+
}
|
|
5140
|
+
];
|
|
5141
|
+
function getHarness(id) {
|
|
5142
|
+
return AGENT_HARNESSES.find((h) => h.id === id);
|
|
5143
|
+
}
|
|
5144
|
+
function resolveHarnessExecutable(harness, overrideCommand, deps = {}) {
|
|
5145
|
+
const find = deps.find ?? findExecutable;
|
|
5146
|
+
const exists = deps.exists ?? existsSync12;
|
|
5147
|
+
const platform = deps.platform ?? process.platform;
|
|
5148
|
+
const found = find(overrideCommand || harness.command);
|
|
5149
|
+
if (found) return found;
|
|
5150
|
+
if (harness.kind === "app") {
|
|
5151
|
+
for (const candidate of harness.candidatePaths?.[platform] ?? []) {
|
|
5152
|
+
if (exists(candidate)) return candidate;
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
return null;
|
|
5156
|
+
}
|
|
5157
|
+
function planHarnessLaunch(harness, opts) {
|
|
5158
|
+
if (harness.kind === "cli") {
|
|
5159
|
+
const invocation = isPowerShellShell(opts.shell) ? `& ${psSingleQuoted2(opts.executable)}` : JSON.stringify(opts.executable);
|
|
5160
|
+
return {
|
|
5161
|
+
kind: "cli",
|
|
5162
|
+
shell: opts.shell,
|
|
5163
|
+
shellSetup: buildShellSetupCommand(
|
|
5164
|
+
opts.workspace,
|
|
5165
|
+
opts.shell,
|
|
5166
|
+
invocation
|
|
5167
|
+
)
|
|
5168
|
+
};
|
|
5169
|
+
}
|
|
5170
|
+
return { kind: "app", executable: opts.executable, args: [opts.workspace] };
|
|
5171
|
+
}
|
|
5172
|
+
function launchHarness(harness, opts) {
|
|
5173
|
+
const plan = planHarnessLaunch(harness, opts);
|
|
5174
|
+
if (plan.kind === "cli") {
|
|
5175
|
+
openTerminalWindow({
|
|
5176
|
+
shellSetup: plan.shellSetup,
|
|
5177
|
+
label: `agent-${harness.id}`,
|
|
5178
|
+
dir: opts.workspace,
|
|
5179
|
+
shell: plan.shell
|
|
5180
|
+
});
|
|
5181
|
+
return;
|
|
5182
|
+
}
|
|
5183
|
+
spawn(plan.executable, plan.args, {
|
|
5184
|
+
detached: true,
|
|
5185
|
+
stdio: "ignore",
|
|
5186
|
+
windowsHide: true
|
|
5187
|
+
}).unref();
|
|
5188
|
+
console.log(`Launched ${harness.label} in ${opts.workspace}`);
|
|
5189
|
+
}
|
|
5190
|
+
|
|
4882
5191
|
// src/cli/commands/agent.ts
|
|
4883
5192
|
var C = {
|
|
4884
5193
|
reset: "\x1B[0m",
|
|
@@ -4890,7 +5199,7 @@ var C = {
|
|
|
4890
5199
|
};
|
|
4891
5200
|
var SUPPORTED_AGENTS = ["opencode"];
|
|
4892
5201
|
function agentsMdPresent(cwd = process.cwd()) {
|
|
4893
|
-
return
|
|
5202
|
+
return existsSync13(join13(cwd, "AGENTS.md"));
|
|
4894
5203
|
}
|
|
4895
5204
|
function printStatus() {
|
|
4896
5205
|
const installed = hasCommand("opencode");
|
|
@@ -4928,19 +5237,87 @@ var installCmd = new Command("install").description("Download and install the de
|
|
|
4928
5237
|
console.log(` Start it with: ${C.cyan}opencode${C.reset}`);
|
|
4929
5238
|
});
|
|
4930
5239
|
var statusCmd = new Command("status").description("Show whether the agent is installed and wired to ZAM").action(printStatus);
|
|
4931
|
-
var
|
|
5240
|
+
var listCmd = new Command("list").description("List known agent harnesses and whether each is detected").action(() => {
|
|
5241
|
+
console.log(`${C.bold}Agent harnesses${C.reset}`);
|
|
5242
|
+
for (const h of AGENT_HARNESSES) {
|
|
5243
|
+
const status = resolveHarnessExecutable(h) ? `${C.green}detected${C.reset}` : `${C.yellow}not found${C.reset}`;
|
|
5244
|
+
console.log(
|
|
5245
|
+
` ${h.id.padEnd(13)} ${status} ${C.dim}${h.label} (${h.kind})${C.reset}`
|
|
5246
|
+
);
|
|
5247
|
+
}
|
|
5248
|
+
console.log(`
|
|
5249
|
+
${C.dim}Open one: zam agent open --id <id>${C.reset}`);
|
|
5250
|
+
console.log(
|
|
5251
|
+
` ${C.dim}Point ZAM at a custom path: zam settings set agent.<id>.command <path>${C.reset}`
|
|
5252
|
+
);
|
|
5253
|
+
});
|
|
5254
|
+
var openCmd = new Command("open").description("Open an agent harness in the workspace, ready to drive ZAM").option(
|
|
5255
|
+
"--id <id>",
|
|
5256
|
+
"Harness id (default: agent.default setting, else first detected)"
|
|
5257
|
+
).option("--dir <path>", "Workspace directory (defaults to cwd)").option(
|
|
5258
|
+
"--shell <type>",
|
|
5259
|
+
"Shell type: zsh | bash | pwsh | powershell (auto-detected)"
|
|
5260
|
+
).action(async (opts) => {
|
|
5261
|
+
let shell;
|
|
5262
|
+
try {
|
|
5263
|
+
shell = normalizeShell(opts.shell);
|
|
5264
|
+
} catch (err) {
|
|
5265
|
+
console.error(`Error: ${err.message}`);
|
|
5266
|
+
process.exit(1);
|
|
5267
|
+
}
|
|
5268
|
+
let id = opts.id;
|
|
5269
|
+
let override;
|
|
5270
|
+
let db;
|
|
5271
|
+
try {
|
|
5272
|
+
db = await openDatabase();
|
|
5273
|
+
if (!id) {
|
|
5274
|
+
id = await getSetting(db, "agent.default") || void 0;
|
|
5275
|
+
}
|
|
5276
|
+
if (!id) {
|
|
5277
|
+
id = AGENT_HARNESSES.find((h) => resolveHarnessExecutable(h))?.id;
|
|
5278
|
+
}
|
|
5279
|
+
if (id) {
|
|
5280
|
+
override = await getSetting(db, `agent.${id}.command`) || void 0;
|
|
5281
|
+
}
|
|
5282
|
+
} finally {
|
|
5283
|
+
await db?.close();
|
|
5284
|
+
}
|
|
5285
|
+
if (!id) {
|
|
5286
|
+
console.error(
|
|
5287
|
+
"No agent harness configured or detected. Install one (e.g. zam agent install), then set a default: zam settings set agent.default <id>."
|
|
5288
|
+
);
|
|
5289
|
+
process.exit(1);
|
|
5290
|
+
}
|
|
5291
|
+
const harness = getHarness(id);
|
|
5292
|
+
if (!harness) {
|
|
5293
|
+
console.error(
|
|
5294
|
+
`Unknown harness: ${id}. Known: ${AGENT_HARNESSES.map((h) => h.id).join(", ")}.`
|
|
5295
|
+
);
|
|
5296
|
+
process.exit(1);
|
|
5297
|
+
}
|
|
5298
|
+
const executable = resolveHarnessExecutable(harness, override);
|
|
5299
|
+
if (!executable) {
|
|
5300
|
+
console.error(
|
|
5301
|
+
`${harness.label} was not detected. Install it, or point ZAM at it: zam settings set agent.${harness.id}.command <path>.`
|
|
5302
|
+
);
|
|
5303
|
+
process.exit(1);
|
|
5304
|
+
}
|
|
5305
|
+
const workspace = opts.dir ?? process.cwd();
|
|
5306
|
+
launchHarness(harness, { executable, workspace, shell });
|
|
5307
|
+
});
|
|
5308
|
+
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
5309
|
|
|
4933
5310
|
// src/cli/commands/bridge.ts
|
|
4934
|
-
import { execFileSync as
|
|
5311
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
4935
5312
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
4936
5313
|
import { readdirSync as readdirSync2, readFileSync as readFileSync10, rmSync as rmSync2 } from "fs";
|
|
4937
|
-
import { homedir as
|
|
4938
|
-
import { join as
|
|
4939
|
-
import { Command as
|
|
5314
|
+
import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
|
|
5315
|
+
import { join as join16, resolve as resolve3 } from "path";
|
|
5316
|
+
import { Command as Command3 } from "commander";
|
|
4940
5317
|
|
|
4941
5318
|
// src/cli/llm/client.ts
|
|
4942
|
-
import { spawn } from "child_process";
|
|
4943
|
-
import { existsSync as
|
|
5319
|
+
import { spawn as spawn2 } from "child_process";
|
|
5320
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4944
5321
|
var DEFAULT_LLM_URL = "http://localhost:8000/v1";
|
|
4945
5322
|
var DEFAULT_LLM_MODEL = "qwen3.5:4b";
|
|
4946
5323
|
var DEFAULT_LLM_API_KEY = "sk-none";
|
|
@@ -4953,16 +5330,108 @@ async function getLlmConfig(db) {
|
|
|
4953
5330
|
locale: await getSetting(db, "system.locale") || "en"
|
|
4954
5331
|
};
|
|
4955
5332
|
}
|
|
4956
|
-
|
|
5333
|
+
function getCloudModelRecommendation(url) {
|
|
5334
|
+
const lowercase = url.toLowerCase();
|
|
5335
|
+
if (lowercase.includes("openrouter.ai")) {
|
|
5336
|
+
return "openrouter/free";
|
|
5337
|
+
}
|
|
5338
|
+
if (lowercase.includes("openai.com") || lowercase.includes("api.openai")) {
|
|
5339
|
+
return "gpt-5-mini";
|
|
5340
|
+
}
|
|
5341
|
+
if (lowercase.includes("googleapis.com") || lowercase.includes("google")) {
|
|
5342
|
+
return "gemini-3.5-flash";
|
|
5343
|
+
}
|
|
5344
|
+
if (lowercase.includes("deepseek.com")) {
|
|
5345
|
+
return "deepseek-v4-flash";
|
|
5346
|
+
}
|
|
5347
|
+
if (lowercase.includes("mimo")) {
|
|
5348
|
+
return "mimo-v2.5";
|
|
5349
|
+
}
|
|
5350
|
+
return null;
|
|
5351
|
+
}
|
|
5352
|
+
function inferApiFlavor(url) {
|
|
5353
|
+
try {
|
|
5354
|
+
return new URL(url).hostname.toLowerCase().endsWith("anthropic.com") ? "anthropic-messages" : "chat-completions";
|
|
5355
|
+
} catch {
|
|
5356
|
+
return "chat-completions";
|
|
5357
|
+
}
|
|
5358
|
+
}
|
|
5359
|
+
async function readJsonSetting(db, key) {
|
|
5360
|
+
const raw = await getSetting(db, key);
|
|
5361
|
+
if (!raw) return null;
|
|
5362
|
+
try {
|
|
5363
|
+
return JSON.parse(raw);
|
|
5364
|
+
} catch {
|
|
5365
|
+
return null;
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
function resolveProviderApiKey(rec) {
|
|
5369
|
+
if (rec.apiKey) return rec.apiKey;
|
|
5370
|
+
if (rec.apiKeyRef) {
|
|
5371
|
+
const key = getProviderApiKey(rec.apiKeyRef);
|
|
5372
|
+
if (key) return key;
|
|
5373
|
+
}
|
|
5374
|
+
return DEFAULT_LLM_API_KEY;
|
|
5375
|
+
}
|
|
5376
|
+
async function getProviderForRole(db, role) {
|
|
5377
|
+
const enabled = role === "vision" ? await getSetting(db, "llm.vision.enabled") === "true" : await getSetting(db, "llm.enabled") === "true";
|
|
5378
|
+
const base = await getLegacyRoleConfig(db, role, enabled);
|
|
5379
|
+
const providers = await readJsonSetting(db, "llm.providers");
|
|
5380
|
+
const roles = await readJsonSetting(db, "llm.roles");
|
|
5381
|
+
const binding = roles?.[role];
|
|
5382
|
+
if (providers && binding?.primary && providers[binding.primary]) {
|
|
5383
|
+
const primary = materializeProvider(providers[binding.primary], base, role);
|
|
5384
|
+
const fallback = binding.fallback && providers[binding.fallback] ? materializeProvider(providers[binding.fallback], base, role) : void 0;
|
|
5385
|
+
return { ...primary, fallback };
|
|
5386
|
+
}
|
|
5387
|
+
return base;
|
|
5388
|
+
}
|
|
5389
|
+
function materializeProvider(rec, base, role) {
|
|
5390
|
+
const url = rec.url || base.url;
|
|
5391
|
+
return {
|
|
5392
|
+
enabled: base.enabled,
|
|
5393
|
+
url,
|
|
5394
|
+
model: rec.model || base.model,
|
|
5395
|
+
apiKey: resolveProviderApiKey(rec),
|
|
5396
|
+
apiFlavor: rec.apiFlavor || inferApiFlavor(url),
|
|
5397
|
+
locale: base.locale,
|
|
5398
|
+
...role === "vision" ? { maxFrames: base.maxFrames } : {}
|
|
5399
|
+
};
|
|
5400
|
+
}
|
|
5401
|
+
async function getLegacyRoleConfig(db, role, enabled) {
|
|
4957
5402
|
const base = await getLlmConfig(db);
|
|
5403
|
+
if (role === "vision") {
|
|
5404
|
+
const maxFramesStr = await getSetting(db, "llm.vision.max_frames");
|
|
5405
|
+
const parsed = maxFramesStr ? parseInt(maxFramesStr, 10) : 100;
|
|
5406
|
+
const url = await getSetting(db, "llm.vision.url") || base.url;
|
|
5407
|
+
let model = await getSetting(db, "llm.vision.model");
|
|
5408
|
+
if (!model) model = getCloudModelRecommendation(url) || base.model;
|
|
5409
|
+
return {
|
|
5410
|
+
enabled,
|
|
5411
|
+
url,
|
|
5412
|
+
model,
|
|
5413
|
+
apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
|
|
5414
|
+
apiFlavor: inferApiFlavor(url),
|
|
5415
|
+
locale: base.locale,
|
|
5416
|
+
maxFrames: Number.isNaN(parsed) ? 100 : parsed
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
4958
5419
|
return {
|
|
4959
|
-
enabled
|
|
4960
|
-
url:
|
|
4961
|
-
model:
|
|
4962
|
-
apiKey:
|
|
5420
|
+
enabled,
|
|
5421
|
+
url: base.url,
|
|
5422
|
+
model: base.model,
|
|
5423
|
+
apiKey: base.apiKey,
|
|
5424
|
+
apiFlavor: inferApiFlavor(base.url),
|
|
4963
5425
|
locale: base.locale
|
|
4964
5426
|
};
|
|
4965
5427
|
}
|
|
5428
|
+
function assertChatCompletions(cfg) {
|
|
5429
|
+
if (cfg.apiFlavor !== "chat-completions") {
|
|
5430
|
+
throw new Error(
|
|
5431
|
+
`This role is configured for a "${cfg.apiFlavor}" provider, which is not supported here yet. Use a chat-completions provider for the recall role.`
|
|
5432
|
+
);
|
|
5433
|
+
}
|
|
5434
|
+
}
|
|
4966
5435
|
var LANGUAGE_NAMES = {
|
|
4967
5436
|
en: "English",
|
|
4968
5437
|
de: "German",
|
|
@@ -5003,10 +5472,11 @@ async function readChatContent(res, label) {
|
|
|
5003
5472
|
return content.trim();
|
|
5004
5473
|
}
|
|
5005
5474
|
async function generateQuestionViaLLM(db, input8) {
|
|
5006
|
-
const cfg = await
|
|
5475
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5007
5476
|
if (!cfg.enabled) {
|
|
5008
5477
|
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5009
5478
|
}
|
|
5479
|
+
assertChatCompletions(cfg);
|
|
5010
5480
|
const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
|
|
5011
5481
|
const verb = BLOOM_VERBS2[bloom];
|
|
5012
5482
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
@@ -5046,10 +5516,11 @@ Active-Recall Question:`;
|
|
|
5046
5516
|
return readChatContent(res, "LLM request");
|
|
5047
5517
|
}
|
|
5048
5518
|
async function evaluateAnswerViaLLM(db, input8) {
|
|
5049
|
-
const cfg = await
|
|
5519
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5050
5520
|
if (!cfg.enabled) {
|
|
5051
5521
|
throw new Error("LLM integration is disabled in settings (llm.enabled)");
|
|
5052
5522
|
}
|
|
5523
|
+
assertChatCompletions(cfg);
|
|
5053
5524
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
5054
5525
|
const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
|
|
5055
5526
|
const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
|
|
@@ -5099,10 +5570,11 @@ Evaluation:`;
|
|
|
5099
5570
|
return readChatContent(res, "LLM evaluation");
|
|
5100
5571
|
}
|
|
5101
5572
|
async function translateQuestionViaLLM(db, question) {
|
|
5102
|
-
const cfg = await
|
|
5573
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5103
5574
|
if (!cfg.enabled) {
|
|
5104
5575
|
throw new Error("LLM integration is disabled in settings");
|
|
5105
5576
|
}
|
|
5577
|
+
assertChatCompletions(cfg);
|
|
5106
5578
|
const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
5107
5579
|
const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
|
|
5108
5580
|
Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
|
|
@@ -5156,34 +5628,82 @@ async function getAvailableModels(url, apiKey = DEFAULT_LLM_API_KEY) {
|
|
|
5156
5628
|
return [];
|
|
5157
5629
|
}
|
|
5158
5630
|
}
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
(candidate) => candidate.toLowerCase() === model.toLowerCase()
|
|
5172
|
-
);
|
|
5173
|
-
}
|
|
5631
|
+
function isLocalEndpoint(url) {
|
|
5632
|
+
return url.includes("localhost") || url.includes("127.0.0.1") || url.includes("[::1]") || url.includes("::1");
|
|
5633
|
+
}
|
|
5634
|
+
async function checkProviderEndpoint(endpoint) {
|
|
5635
|
+
const online = await isLlmOnline(endpoint.url);
|
|
5636
|
+
if (!online) {
|
|
5637
|
+
return {
|
|
5638
|
+
endpoint,
|
|
5639
|
+
online: false,
|
|
5640
|
+
availableModels: [],
|
|
5641
|
+
modelAvailable: false
|
|
5642
|
+
};
|
|
5174
5643
|
}
|
|
5644
|
+
const availableModels = await getAvailableModels(
|
|
5645
|
+
endpoint.url,
|
|
5646
|
+
endpoint.apiKey
|
|
5647
|
+
);
|
|
5648
|
+
const modelAvailable = availableModels.length === 0 || availableModels.some(
|
|
5649
|
+
(candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
|
|
5650
|
+
);
|
|
5651
|
+
return { endpoint, online, availableModels, modelAvailable };
|
|
5652
|
+
}
|
|
5653
|
+
function isEndpointUsable(readiness) {
|
|
5654
|
+
return readiness.online && readiness.modelAvailable;
|
|
5655
|
+
}
|
|
5656
|
+
function providerChain(primary) {
|
|
5657
|
+
return [primary, ...primary.fallback ? [primary.fallback] : []];
|
|
5658
|
+
}
|
|
5659
|
+
async function checkProviderChain(primary) {
|
|
5660
|
+
let first;
|
|
5661
|
+
for (const endpoint of providerChain(primary)) {
|
|
5662
|
+
const readiness = await checkProviderEndpoint(endpoint);
|
|
5663
|
+
first ??= readiness;
|
|
5664
|
+
if (isEndpointUsable(readiness)) {
|
|
5665
|
+
return { primary: first, firstUsable: readiness };
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
5668
|
+
return { primary: first };
|
|
5669
|
+
}
|
|
5670
|
+
async function isVisionProviderModelExplicit(db, active) {
|
|
5671
|
+
if (await getSetting(db, "llm.vision.model")) return true;
|
|
5672
|
+
const providers = await readJsonSetting(db, "llm.providers");
|
|
5673
|
+
const roles = await readJsonSetting(db, "llm.roles");
|
|
5674
|
+
const binding = roles?.vision;
|
|
5675
|
+
if (!providers || !binding) return false;
|
|
5676
|
+
return [binding.primary, binding.fallback].some((id) => {
|
|
5677
|
+
if (!id) return false;
|
|
5678
|
+
const rec = providers[id];
|
|
5679
|
+
return rec?.model === active.model && (rec.url === void 0 || rec.url === active.url);
|
|
5680
|
+
});
|
|
5681
|
+
}
|
|
5682
|
+
async function checkVisionReadiness(db) {
|
|
5683
|
+
const cfg = await getProviderForRole(db, "vision");
|
|
5684
|
+
const chain = cfg.enabled ? await checkProviderChain(cfg) : void 0;
|
|
5685
|
+
const selected = chain?.firstUsable ?? chain?.primary;
|
|
5686
|
+
const active = selected?.endpoint ?? cfg;
|
|
5687
|
+
const online = selected?.online ?? false;
|
|
5688
|
+
const availableModels = selected?.availableModels ?? [];
|
|
5689
|
+
const modelAvailable = selected?.modelAvailable ?? false;
|
|
5690
|
+
const visionModelExplicit = await isVisionProviderModelExplicit(db, active);
|
|
5175
5691
|
let warning;
|
|
5176
|
-
if (enabled && online && modelAvailable && !visionModelExplicit) {
|
|
5177
|
-
|
|
5692
|
+
if (cfg.enabled && online && modelAvailable && !visionModelExplicit) {
|
|
5693
|
+
const cloudRec = getCloudModelRecommendation(active.url);
|
|
5694
|
+
if (cloudRec && active.model === cloudRec) {
|
|
5695
|
+
} else {
|
|
5696
|
+
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>`;
|
|
5697
|
+
}
|
|
5178
5698
|
}
|
|
5179
5699
|
return {
|
|
5180
|
-
enabled,
|
|
5700
|
+
enabled: cfg.enabled,
|
|
5181
5701
|
online,
|
|
5182
|
-
url,
|
|
5183
|
-
model,
|
|
5702
|
+
url: active.url,
|
|
5703
|
+
model: active.model,
|
|
5184
5704
|
modelAvailable,
|
|
5185
5705
|
availableModels,
|
|
5186
|
-
usable: enabled && online && modelAvailable,
|
|
5706
|
+
usable: cfg.enabled && online && modelAvailable,
|
|
5187
5707
|
visionModelExplicit,
|
|
5188
5708
|
warning
|
|
5189
5709
|
};
|
|
@@ -5208,16 +5728,16 @@ function spawnLocalRunner(url, model) {
|
|
|
5208
5728
|
const { runner, port } = detectRunner(url, model);
|
|
5209
5729
|
try {
|
|
5210
5730
|
if (runner === "fastflowlm") {
|
|
5211
|
-
const flmExe =
|
|
5731
|
+
const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
|
|
5212
5732
|
if (!hasCommand("flm") && flmExe === "flm") return;
|
|
5213
|
-
|
|
5733
|
+
spawn2(flmExe, ["serve", model, "--port", port], {
|
|
5214
5734
|
detached: true,
|
|
5215
5735
|
stdio: "ignore",
|
|
5216
5736
|
windowsHide: true
|
|
5217
5737
|
}).unref();
|
|
5218
5738
|
} else if (runner === "ollama" || runner === "generic") {
|
|
5219
5739
|
if (!hasCommand("ollama")) return;
|
|
5220
|
-
|
|
5740
|
+
spawn2("ollama", ["serve"], {
|
|
5221
5741
|
detached: true,
|
|
5222
5742
|
stdio: "ignore",
|
|
5223
5743
|
windowsHide: true
|
|
@@ -5229,7 +5749,7 @@ function spawnLocalRunner(url, model) {
|
|
|
5229
5749
|
async function startLocalRunner(url, model, locale) {
|
|
5230
5750
|
const { runner, port } = detectRunner(url, model);
|
|
5231
5751
|
if (runner === "fastflowlm") {
|
|
5232
|
-
const flmExe =
|
|
5752
|
+
const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
|
|
5233
5753
|
if (!hasCommand("flm") && flmExe === "flm") {
|
|
5234
5754
|
console.warn(
|
|
5235
5755
|
"\x1B[31m\u2717 FastFlowLM is configured but could not be found on the system.\x1B[0m"
|
|
@@ -5242,7 +5762,7 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5242
5762
|
`\x1B[36mStarting FastFlowLM serve process: ${flmExe} ${args.join(" ")}\x1B[0m`
|
|
5243
5763
|
);
|
|
5244
5764
|
try {
|
|
5245
|
-
|
|
5765
|
+
spawn2(flmExe, args, {
|
|
5246
5766
|
detached: true,
|
|
5247
5767
|
stdio: "ignore",
|
|
5248
5768
|
windowsHide: true
|
|
@@ -5263,7 +5783,7 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5263
5783
|
}
|
|
5264
5784
|
console.log("\x1B[36mStarting Ollama serve process: ollama serve\x1B[0m");
|
|
5265
5785
|
try {
|
|
5266
|
-
|
|
5786
|
+
spawn2("ollama", ["serve"], {
|
|
5267
5787
|
detached: true,
|
|
5268
5788
|
stdio: "ignore",
|
|
5269
5789
|
windowsHide: true
|
|
@@ -5286,7 +5806,7 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5286
5806
|
let attempts = 0;
|
|
5287
5807
|
const dotsPerLine = 30;
|
|
5288
5808
|
while (true) {
|
|
5289
|
-
await new Promise((
|
|
5809
|
+
await new Promise((resolve6) => setTimeout(resolve6, 500));
|
|
5290
5810
|
if (await isLlmOnline(url)) {
|
|
5291
5811
|
if (attempts > 0) process.stdout.write("\n");
|
|
5292
5812
|
return true;
|
|
@@ -5311,12 +5831,18 @@ async function startLocalRunner(url, model, locale) {
|
|
|
5311
5831
|
}
|
|
5312
5832
|
}
|
|
5313
5833
|
async function ensureLocalLlmRunning(db) {
|
|
5314
|
-
const cfg = await
|
|
5834
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5315
5835
|
if (!cfg.enabled) {
|
|
5316
5836
|
return { usable: false, reason: "disabled" };
|
|
5317
5837
|
}
|
|
5838
|
+
if (cfg.apiFlavor !== "chat-completions") {
|
|
5839
|
+
console.warn(
|
|
5840
|
+
`\x1B[31m\u2717 Recall is configured for "${cfg.apiFlavor}", but active recall currently supports chat-completions providers only.\x1B[0m`
|
|
5841
|
+
);
|
|
5842
|
+
return { usable: false, reason: "unsupported-provider" };
|
|
5843
|
+
}
|
|
5318
5844
|
const { url, model, apiKey, locale } = cfg;
|
|
5319
|
-
const isLocal =
|
|
5845
|
+
const isLocal = isLocalEndpoint(url);
|
|
5320
5846
|
console.log(`Checking if local LLM server is online at ${url}...`);
|
|
5321
5847
|
let online = await isLlmOnline(url);
|
|
5322
5848
|
if (!online && isLocal) {
|
|
@@ -5350,8 +5876,9 @@ async function ensureLocalLlmRunning(db) {
|
|
|
5350
5876
|
}
|
|
5351
5877
|
async function ensureLlmReadyHeadless(db, opts = {}) {
|
|
5352
5878
|
const timeoutMs = opts.timeoutMs ?? 25e3;
|
|
5353
|
-
const
|
|
5354
|
-
|
|
5879
|
+
const cfg = await getProviderForRole(db, "recall");
|
|
5880
|
+
const { url, model, apiKey } = cfg;
|
|
5881
|
+
if (!cfg.enabled) {
|
|
5355
5882
|
return {
|
|
5356
5883
|
usable: false,
|
|
5357
5884
|
reason: "disabled",
|
|
@@ -5360,7 +5887,16 @@ async function ensureLlmReadyHeadless(db, opts = {}) {
|
|
|
5360
5887
|
availableModels: []
|
|
5361
5888
|
};
|
|
5362
5889
|
}
|
|
5363
|
-
|
|
5890
|
+
if (cfg.apiFlavor !== "chat-completions") {
|
|
5891
|
+
return {
|
|
5892
|
+
usable: false,
|
|
5893
|
+
reason: "unsupported-provider",
|
|
5894
|
+
online: false,
|
|
5895
|
+
model,
|
|
5896
|
+
availableModels: []
|
|
5897
|
+
};
|
|
5898
|
+
}
|
|
5899
|
+
const isLocal = isLocalEndpoint(url);
|
|
5364
5900
|
let online = await isLlmOnline(url);
|
|
5365
5901
|
if (!online && isLocal) {
|
|
5366
5902
|
spawnLocalRunner(url, model);
|
|
@@ -5425,8 +5961,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
|
5425
5961
|
const dotsPerLine = 30;
|
|
5426
5962
|
while (true) {
|
|
5427
5963
|
let timeoutId;
|
|
5428
|
-
const timeoutPromise = new Promise((
|
|
5429
|
-
timeoutId = setTimeout(() =>
|
|
5964
|
+
const timeoutPromise = new Promise((resolve6) => {
|
|
5965
|
+
timeoutId = setTimeout(() => resolve6("timeout"), timeoutMs);
|
|
5430
5966
|
});
|
|
5431
5967
|
const dotsInterval = setInterval(() => {
|
|
5432
5968
|
process.stdout.write(".");
|
|
@@ -5497,10 +6033,10 @@ async function ensureHighQualityQuestion(db, token) {
|
|
|
5497
6033
|
}
|
|
5498
6034
|
|
|
5499
6035
|
// src/cli/llm/vision.ts
|
|
5500
|
-
import { readFileSync as readFileSync9 } from "fs";
|
|
5501
6036
|
import { randomBytes } from "crypto";
|
|
5502
|
-
import {
|
|
5503
|
-
import {
|
|
6037
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
6038
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
6039
|
+
import { basename as basename3, join as join14 } from "path";
|
|
5504
6040
|
var LANGUAGE_NAMES2 = {
|
|
5505
6041
|
en: "English",
|
|
5506
6042
|
de: "German",
|
|
@@ -5527,7 +6063,7 @@ var ACTION_TYPES2 = /* @__PURE__ */ new Set([
|
|
|
5527
6063
|
"window-change"
|
|
5528
6064
|
]);
|
|
5529
6065
|
async function observeUiSnapshotViaLLM(db, input8) {
|
|
5530
|
-
const cfg = await
|
|
6066
|
+
const cfg = await getProviderForRole(db, "vision");
|
|
5531
6067
|
if (!cfg.enabled) {
|
|
5532
6068
|
throw new Error(
|
|
5533
6069
|
"Vision observation is disabled in settings (llm.vision.enabled)"
|
|
@@ -5536,34 +6072,42 @@ async function observeUiSnapshotViaLLM(db, input8) {
|
|
|
5536
6072
|
const imageUrls = [];
|
|
5537
6073
|
const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
|
|
5538
6074
|
if (isVideo) {
|
|
5539
|
-
const { mkdirSync:
|
|
5540
|
-
const { execSync:
|
|
5541
|
-
const tempDir =
|
|
5542
|
-
|
|
6075
|
+
const { mkdirSync: mkdirSync13, readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
|
|
6076
|
+
const { execSync: execSync6 } = await import("child_process");
|
|
6077
|
+
const tempDir = join14(
|
|
6078
|
+
tmpdir2(),
|
|
6079
|
+
`zam-frames-${randomBytes(4).toString("hex")}`
|
|
6080
|
+
);
|
|
6081
|
+
mkdirSync13(tempDir, { recursive: true });
|
|
5543
6082
|
try {
|
|
5544
|
-
|
|
6083
|
+
execSync6(
|
|
5545
6084
|
`ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
|
|
5546
6085
|
{ stdio: "ignore" }
|
|
5547
6086
|
);
|
|
5548
6087
|
let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
|
|
5549
6088
|
if (files.length === 0) {
|
|
5550
|
-
|
|
6089
|
+
execSync6(
|
|
5551
6090
|
`ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
|
|
5552
6091
|
{ stdio: "ignore" }
|
|
5553
6092
|
);
|
|
5554
6093
|
files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
|
|
5555
6094
|
}
|
|
6095
|
+
const maxFrames = cfg.maxFrames ?? 100;
|
|
5556
6096
|
let sampledFiles = files;
|
|
5557
|
-
if (files.length >
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
const
|
|
5562
|
-
sampledFiles
|
|
6097
|
+
if (files.length > maxFrames) {
|
|
6098
|
+
if (maxFrames <= 1) {
|
|
6099
|
+
sampledFiles = [files[0]];
|
|
6100
|
+
} else {
|
|
6101
|
+
const step = (files.length - 1) / (maxFrames - 1);
|
|
6102
|
+
sampledFiles = [];
|
|
6103
|
+
for (let i = 0; i < maxFrames; i++) {
|
|
6104
|
+
const index = Math.round(i * step);
|
|
6105
|
+
sampledFiles.push(files[index]);
|
|
6106
|
+
}
|
|
5563
6107
|
}
|
|
5564
6108
|
}
|
|
5565
6109
|
for (const file of sampledFiles) {
|
|
5566
|
-
const bytes = readFileSync9(
|
|
6110
|
+
const bytes = readFileSync9(join14(tempDir, file));
|
|
5567
6111
|
imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
|
|
5568
6112
|
}
|
|
5569
6113
|
} finally {
|
|
@@ -5581,42 +6125,92 @@ async function observeUiSnapshotViaLLM(db, input8) {
|
|
|
5581
6125
|
if (imageUrls.length === 0) {
|
|
5582
6126
|
throw new Error("No image data available for vision analysis");
|
|
5583
6127
|
}
|
|
5584
|
-
const
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
"Vision model returned output that could not be parsed as JSON."
|
|
5600
|
-
);
|
|
6128
|
+
const endpoints = [
|
|
6129
|
+
{
|
|
6130
|
+
url: cfg.url,
|
|
6131
|
+
apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
|
|
6132
|
+
model: input8.model ?? cfg.model,
|
|
6133
|
+
apiFlavor: cfg.apiFlavor
|
|
6134
|
+
}
|
|
6135
|
+
];
|
|
6136
|
+
if (cfg.fallback) {
|
|
6137
|
+
endpoints.push({
|
|
6138
|
+
url: cfg.fallback.url,
|
|
6139
|
+
apiKey: cfg.fallback.apiKey || DEFAULT_LLM_API_KEY,
|
|
6140
|
+
model: cfg.fallback.model,
|
|
6141
|
+
apiFlavor: cfg.fallback.apiFlavor
|
|
6142
|
+
});
|
|
5601
6143
|
}
|
|
5602
|
-
|
|
5603
|
-
|
|
6144
|
+
let lastRequestError;
|
|
6145
|
+
let sawUnparseableDraft = false;
|
|
6146
|
+
let sawInvalidDraft = false;
|
|
6147
|
+
for (const endpoint of endpoints) {
|
|
6148
|
+
let content;
|
|
6149
|
+
try {
|
|
6150
|
+
content = await requestVisionDraft({
|
|
6151
|
+
...endpoint,
|
|
6152
|
+
locale: cfg.locale,
|
|
6153
|
+
imageUrls,
|
|
6154
|
+
input: input8
|
|
6155
|
+
});
|
|
6156
|
+
} catch (err) {
|
|
6157
|
+
lastRequestError = err;
|
|
6158
|
+
continue;
|
|
6159
|
+
}
|
|
6160
|
+
let draft;
|
|
6161
|
+
try {
|
|
6162
|
+
draft = extractDraft(content);
|
|
6163
|
+
} catch {
|
|
6164
|
+
sawUnparseableDraft = true;
|
|
6165
|
+
continue;
|
|
6166
|
+
}
|
|
6167
|
+
const report = buildReport(input8, draft);
|
|
6168
|
+
if (isUiObservationReport(report)) {
|
|
6169
|
+
return report;
|
|
6170
|
+
}
|
|
6171
|
+
sawInvalidDraft = true;
|
|
6172
|
+
}
|
|
6173
|
+
if (lastRequestError && !sawUnparseableDraft && !sawInvalidDraft) {
|
|
6174
|
+
throw lastRequestError;
|
|
6175
|
+
}
|
|
6176
|
+
if (sawInvalidDraft) {
|
|
5604
6177
|
return uncertainReport(
|
|
5605
6178
|
input8,
|
|
5606
6179
|
"Vision model returned an invalid UI report draft."
|
|
5607
6180
|
);
|
|
5608
6181
|
}
|
|
5609
|
-
return
|
|
6182
|
+
return uncertainReport(
|
|
6183
|
+
input8,
|
|
6184
|
+
"Vision model returned output that could not be parsed as JSON."
|
|
6185
|
+
);
|
|
5610
6186
|
}
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
6187
|
+
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.";
|
|
6188
|
+
function visionSchema(language) {
|
|
6189
|
+
return `{
|
|
5614
6190
|
"kind": "progress | step-completed | error | help-seeking | uncertain",
|
|
5615
6191
|
"summary": "short factual UI summary in ${language}",
|
|
5616
6192
|
"actions": [{"type": "click | shortcut | typing | scroll | window-change", "target": "optional UI target", "result": "optional visible result"}],
|
|
5617
6193
|
"candidateTokens": [{"slug": "optional-kebab-case-skill-token", "confidence": 0.0, "rationale": "why this token may matter"}],
|
|
5618
6194
|
"confidence": 0.0
|
|
5619
6195
|
}`;
|
|
6196
|
+
}
|
|
6197
|
+
function visionUserText(args, language) {
|
|
6198
|
+
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.";
|
|
6199
|
+
return `${intro}
|
|
6200
|
+
Application process: ${args.input.application.processName}
|
|
6201
|
+
Window title: ${args.input.application.windowTitle ?? "(unknown)"}
|
|
6202
|
+
|
|
6203
|
+
Return this JSON draft only:
|
|
6204
|
+
${visionSchema(language)}`;
|
|
6205
|
+
}
|
|
6206
|
+
async function requestVisionDraft(args) {
|
|
6207
|
+
if (args.apiFlavor === "anthropic-messages") {
|
|
6208
|
+
return requestAnthropicVisionDraft(args);
|
|
6209
|
+
}
|
|
6210
|
+
return requestChatCompletionsVisionDraft(args);
|
|
6211
|
+
}
|
|
6212
|
+
async function requestChatCompletionsVisionDraft(args) {
|
|
6213
|
+
const language = LANGUAGE_NAMES2[args.locale] ?? "English";
|
|
5620
6214
|
const res = await fetchWithInteractiveTimeout(
|
|
5621
6215
|
`${args.url}/chat/completions`,
|
|
5622
6216
|
{
|
|
@@ -5628,27 +6222,11 @@ async function requestVisionDraft(args) {
|
|
|
5628
6222
|
body: JSON.stringify({
|
|
5629
6223
|
model: args.model,
|
|
5630
6224
|
messages: [
|
|
5631
|
-
{
|
|
5632
|
-
role: "system",
|
|
5633
|
-
content: "You are ZAM's UI observer. Return only strict JSON. Do not include markdown, prose, or fields outside the requested schema."
|
|
5634
|
-
},
|
|
6225
|
+
{ role: "system", content: VISION_SYSTEM_PROMPT },
|
|
5635
6226
|
{
|
|
5636
6227
|
role: "user",
|
|
5637
6228
|
content: [
|
|
5638
|
-
{
|
|
5639
|
-
type: "text",
|
|
5640
|
-
text: args.imageUrls.length > 1 ? `Observe this sequence of Windows/macOS application snapshots showing a task performed over time.
|
|
5641
|
-
Application process: ${args.input.application.processName}
|
|
5642
|
-
Window title: ${args.input.application.windowTitle ?? "(unknown)"}
|
|
5643
|
-
|
|
5644
|
-
Return this JSON draft only:
|
|
5645
|
-
${schema}` : `Observe this Windows/macOS application snapshot for a learning session.
|
|
5646
|
-
Application process: ${args.input.application.processName}
|
|
5647
|
-
Window title: ${args.input.application.windowTitle ?? "(unknown)"}
|
|
5648
|
-
|
|
5649
|
-
Return this JSON draft only:
|
|
5650
|
-
${schema}`
|
|
5651
|
-
},
|
|
6229
|
+
{ type: "text", text: visionUserText(args, language) },
|
|
5652
6230
|
...args.imageUrls.map((url) => ({
|
|
5653
6231
|
type: "image_url",
|
|
5654
6232
|
image_url: { url }
|
|
@@ -5657,7 +6235,8 @@ ${schema}`
|
|
|
5657
6235
|
}
|
|
5658
6236
|
],
|
|
5659
6237
|
temperature: 0,
|
|
5660
|
-
max_tokens: args.input.maxTokens ?? 450
|
|
6238
|
+
max_tokens: args.input.maxTokens ?? 450,
|
|
6239
|
+
...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
|
|
5661
6240
|
}),
|
|
5662
6241
|
locale: args.locale,
|
|
5663
6242
|
hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
|
|
@@ -5690,6 +6269,64 @@ ${schema}`
|
|
|
5690
6269
|
}
|
|
5691
6270
|
return content.trim();
|
|
5692
6271
|
}
|
|
6272
|
+
function dataUrlToAnthropicImage(dataUrl) {
|
|
6273
|
+
const match = dataUrl.match(/^data:(image\/[a-zA-Z]+);base64,(.+)$/);
|
|
6274
|
+
if (!match) {
|
|
6275
|
+
throw new Error("Unsupported image data for Anthropic vision request");
|
|
6276
|
+
}
|
|
6277
|
+
return {
|
|
6278
|
+
type: "image",
|
|
6279
|
+
source: { type: "base64", media_type: match[1], data: match[2] }
|
|
6280
|
+
};
|
|
6281
|
+
}
|
|
6282
|
+
async function requestAnthropicVisionDraft(args) {
|
|
6283
|
+
const language = LANGUAGE_NAMES2[args.locale] ?? "English";
|
|
6284
|
+
const base = args.url.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
6285
|
+
const res = await fetchWithInteractiveTimeout(`${base}/v1/messages`, {
|
|
6286
|
+
method: "POST",
|
|
6287
|
+
headers: {
|
|
6288
|
+
"Content-Type": "application/json",
|
|
6289
|
+
"x-api-key": args.apiKey,
|
|
6290
|
+
"anthropic-version": "2023-06-01"
|
|
6291
|
+
},
|
|
6292
|
+
body: JSON.stringify({
|
|
6293
|
+
model: args.model,
|
|
6294
|
+
max_tokens: args.input.maxTokens ?? 450,
|
|
6295
|
+
system: VISION_SYSTEM_PROMPT,
|
|
6296
|
+
messages: [
|
|
6297
|
+
{
|
|
6298
|
+
role: "user",
|
|
6299
|
+
content: [
|
|
6300
|
+
{ type: "text", text: visionUserText(args, language) },
|
|
6301
|
+
...args.imageUrls.map(dataUrlToAnthropicImage)
|
|
6302
|
+
]
|
|
6303
|
+
}
|
|
6304
|
+
]
|
|
6305
|
+
}),
|
|
6306
|
+
locale: args.locale,
|
|
6307
|
+
hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
|
|
6308
|
+
});
|
|
6309
|
+
if (!res.ok) {
|
|
6310
|
+
const errorText = await res.text().catch(() => "");
|
|
6311
|
+
throw new Error(
|
|
6312
|
+
`Vision LLM request failed: ${res.statusText} (${res.status}) - ${errorText}`
|
|
6313
|
+
);
|
|
6314
|
+
}
|
|
6315
|
+
const data = await res.json();
|
|
6316
|
+
if (data.error !== void 0) {
|
|
6317
|
+
throw new Error(`Vision model failed: ${formatModelError(data.error)}`);
|
|
6318
|
+
}
|
|
6319
|
+
if (data.stop_reason === "refusal") {
|
|
6320
|
+
throw new Error("Vision model refused the request (safety classifier).");
|
|
6321
|
+
}
|
|
6322
|
+
const text = data.content?.find(
|
|
6323
|
+
(b) => b.type === "text" && typeof b.text === "string"
|
|
6324
|
+
)?.text;
|
|
6325
|
+
if (!text) {
|
|
6326
|
+
throw new Error("Empty response from vision model");
|
|
6327
|
+
}
|
|
6328
|
+
return text.trim();
|
|
6329
|
+
}
|
|
5693
6330
|
function extractDraft(content) {
|
|
5694
6331
|
const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
5695
6332
|
const candidate = (fenced?.[1] ?? content).trim();
|
|
@@ -5718,7 +6355,7 @@ function buildReport(input8, draft) {
|
|
|
5718
6355
|
evidence: [
|
|
5719
6356
|
{
|
|
5720
6357
|
type: "keyframe",
|
|
5721
|
-
ref: input8.evidenceRef ??
|
|
6358
|
+
ref: input8.evidenceRef ?? basename3(input8.imagePath),
|
|
5722
6359
|
redacted: input8.redacted ?? false
|
|
5723
6360
|
}
|
|
5724
6361
|
],
|
|
@@ -5740,7 +6377,7 @@ function uncertainReport(input8, summary) {
|
|
|
5740
6377
|
evidence: [
|
|
5741
6378
|
{
|
|
5742
6379
|
type: "keyframe",
|
|
5743
|
-
ref: input8.evidenceRef ??
|
|
6380
|
+
ref: input8.evidenceRef ?? basename3(input8.imagePath),
|
|
5744
6381
|
redacted: input8.redacted ?? false
|
|
5745
6382
|
}
|
|
5746
6383
|
],
|
|
@@ -5850,6 +6487,206 @@ function jsonOut(data) {
|
|
|
5850
6487
|
console.log(JSON.stringify(data, null, 2));
|
|
5851
6488
|
}
|
|
5852
6489
|
|
|
6490
|
+
// src/cli/commands/workspace.ts
|
|
6491
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
6492
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
6493
|
+
import { homedir as homedir9 } from "os";
|
|
6494
|
+
import { join as join15 } from "path";
|
|
6495
|
+
import { confirm, input } from "@inquirer/prompts";
|
|
6496
|
+
import { Command as Command2 } from "commander";
|
|
6497
|
+
function runGit(cwd, args) {
|
|
6498
|
+
try {
|
|
6499
|
+
return execFileSync3("git", args, {
|
|
6500
|
+
cwd,
|
|
6501
|
+
stdio: "pipe",
|
|
6502
|
+
encoding: "utf8"
|
|
6503
|
+
}).trim();
|
|
6504
|
+
} catch (err) {
|
|
6505
|
+
throw new Error(`Git command failed: ${err.message}`);
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
function ghRepoCreateArgs(repoName, repoVisibility) {
|
|
6509
|
+
return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
|
|
6510
|
+
}
|
|
6511
|
+
function gitRemoteArgs(githubUrl, hasOrigin) {
|
|
6512
|
+
return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
|
|
6513
|
+
}
|
|
6514
|
+
var workspaceCommand = new Command2("workspace").description(
|
|
6515
|
+
"Manage your ZAM learning workspace"
|
|
6516
|
+
);
|
|
6517
|
+
workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
|
|
6518
|
+
let db;
|
|
6519
|
+
let workspaceDir = "";
|
|
6520
|
+
try {
|
|
6521
|
+
db = await openDatabase();
|
|
6522
|
+
workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
|
|
6523
|
+
await db.close();
|
|
6524
|
+
} catch {
|
|
6525
|
+
await db?.close();
|
|
6526
|
+
}
|
|
6527
|
+
if (!workspaceDir) {
|
|
6528
|
+
console.error(
|
|
6529
|
+
"\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
|
|
6530
|
+
);
|
|
6531
|
+
process.exit(1);
|
|
6532
|
+
}
|
|
6533
|
+
if (!existsSync15(workspaceDir)) {
|
|
6534
|
+
console.error(
|
|
6535
|
+
`\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
|
|
6536
|
+
);
|
|
6537
|
+
process.exit(1);
|
|
6538
|
+
}
|
|
6539
|
+
console.log(`
|
|
6540
|
+
Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
|
|
6541
|
+
if (!hasCommand("git")) {
|
|
6542
|
+
console.error(
|
|
6543
|
+
"\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
|
|
6544
|
+
);
|
|
6545
|
+
process.exit(1);
|
|
6546
|
+
}
|
|
6547
|
+
const gitignorePath = join15(workspaceDir, ".gitignore");
|
|
6548
|
+
if (!existsSync15(gitignorePath)) {
|
|
6549
|
+
writeFileSync7(
|
|
6550
|
+
gitignorePath,
|
|
6551
|
+
"node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
|
|
6552
|
+
"utf8"
|
|
6553
|
+
);
|
|
6554
|
+
}
|
|
6555
|
+
const hasGitRepo = existsSync15(join15(workspaceDir, ".git"));
|
|
6556
|
+
if (!hasGitRepo) {
|
|
6557
|
+
console.log("Initializing local Git repository...");
|
|
6558
|
+
runGit(workspaceDir, ["init", "-b", "main"]);
|
|
6559
|
+
runGit(workspaceDir, ["add", "."]);
|
|
6560
|
+
runGit(workspaceDir, [
|
|
6561
|
+
"commit",
|
|
6562
|
+
"-m",
|
|
6563
|
+
"chore: initial workspace sandbox bootstrap"
|
|
6564
|
+
]);
|
|
6565
|
+
console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
|
|
6566
|
+
} else {
|
|
6567
|
+
console.log("Git repository is already initialized.");
|
|
6568
|
+
}
|
|
6569
|
+
const repoName = await input({
|
|
6570
|
+
message: "Choose a name for your GitHub repository:",
|
|
6571
|
+
default: "zam-personal"
|
|
6572
|
+
});
|
|
6573
|
+
const isPrivate = await confirm({
|
|
6574
|
+
message: "Should the repository be private?",
|
|
6575
|
+
default: true
|
|
6576
|
+
});
|
|
6577
|
+
const repoVisibility = isPrivate ? "--private" : "--public";
|
|
6578
|
+
if (hasCommand("gh")) {
|
|
6579
|
+
console.log("GitHub CLI detected! Automating repository creation...");
|
|
6580
|
+
const proceedGh = await confirm({
|
|
6581
|
+
message: "Would you like ZAM to create the repository using the GitHub CLI?",
|
|
6582
|
+
default: true
|
|
6583
|
+
});
|
|
6584
|
+
if (proceedGh) {
|
|
6585
|
+
try {
|
|
6586
|
+
console.log(`Creating GitHub repository ${repoName}...`);
|
|
6587
|
+
execFileSync3("gh", ghRepoCreateArgs(repoName, repoVisibility), {
|
|
6588
|
+
cwd: workspaceDir,
|
|
6589
|
+
stdio: "inherit"
|
|
6590
|
+
});
|
|
6591
|
+
console.log(
|
|
6592
|
+
"\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
|
|
6593
|
+
);
|
|
6594
|
+
process.exit(0);
|
|
6595
|
+
} catch (err) {
|
|
6596
|
+
console.warn(
|
|
6597
|
+
`\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
|
|
6598
|
+
);
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
console.log(
|
|
6603
|
+
"\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
|
|
6604
|
+
);
|
|
6605
|
+
console.log(" 1. Go to https://github.com/new");
|
|
6606
|
+
console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
|
|
6607
|
+
console.log(
|
|
6608
|
+
` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
|
|
6609
|
+
);
|
|
6610
|
+
console.log(
|
|
6611
|
+
" 4. Do NOT initialize it with README, .gitignore, or license"
|
|
6612
|
+
);
|
|
6613
|
+
console.log(" 5. Click 'Create repository'\n");
|
|
6614
|
+
const githubUrl = await input({
|
|
6615
|
+
message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
|
|
6616
|
+
});
|
|
6617
|
+
if (githubUrl) {
|
|
6618
|
+
try {
|
|
6619
|
+
console.log("Linking remote repository and pushing...");
|
|
6620
|
+
let hasOrigin = false;
|
|
6621
|
+
try {
|
|
6622
|
+
runGit(workspaceDir, ["remote", "get-url", "origin"]);
|
|
6623
|
+
hasOrigin = true;
|
|
6624
|
+
} catch {
|
|
6625
|
+
}
|
|
6626
|
+
runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
|
|
6627
|
+
runGit(workspaceDir, ["push", "-u", "origin", "main"]);
|
|
6628
|
+
console.log(
|
|
6629
|
+
"\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
|
|
6630
|
+
);
|
|
6631
|
+
} catch (err) {
|
|
6632
|
+
console.error(
|
|
6633
|
+
`\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
|
|
6634
|
+
);
|
|
6635
|
+
console.log(
|
|
6636
|
+
"You can push manually later using: git push -u origin main"
|
|
6637
|
+
);
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6640
|
+
});
|
|
6641
|
+
async function backupDatabaseTo(db, targetDir) {
|
|
6642
|
+
const backupDir = join15(targetDir, "zam-backups");
|
|
6643
|
+
mkdirSync8(backupDir, { recursive: true });
|
|
6644
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6645
|
+
const dest = join15(backupDir, `zam-${stamp}.db`);
|
|
6646
|
+
await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
6647
|
+
return dest;
|
|
6648
|
+
}
|
|
6649
|
+
workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
|
|
6650
|
+
const dir = join15(homedir9(), ".zam");
|
|
6651
|
+
console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
|
|
6652
|
+
});
|
|
6653
|
+
workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
|
|
6654
|
+
"--dir <path>",
|
|
6655
|
+
"Target directory (default: workspace dir, else ~/Documents/zam)"
|
|
6656
|
+
).option("--json", "Output as JSON").action(async (opts) => {
|
|
6657
|
+
const target = getDatabaseTargetInfo();
|
|
6658
|
+
if (target.kind !== "local") {
|
|
6659
|
+
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.`;
|
|
6660
|
+
if (opts.json) {
|
|
6661
|
+
console.log(JSON.stringify({ ok: false, reason }));
|
|
6662
|
+
return;
|
|
6663
|
+
}
|
|
6664
|
+
console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
|
|
6665
|
+
process.exit(1);
|
|
6666
|
+
}
|
|
6667
|
+
let db;
|
|
6668
|
+
try {
|
|
6669
|
+
db = await openDatabase();
|
|
6670
|
+
const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join15(homedir9(), "Documents", "zam");
|
|
6671
|
+
const dest = await backupDatabaseTo(db, workspaceDir);
|
|
6672
|
+
if (opts.json) {
|
|
6673
|
+
console.log(JSON.stringify({ ok: true, path: dest }));
|
|
6674
|
+
} else {
|
|
6675
|
+
console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
|
|
6676
|
+
}
|
|
6677
|
+
} catch (err) {
|
|
6678
|
+
const reason = err.message;
|
|
6679
|
+
if (opts.json) {
|
|
6680
|
+
console.log(JSON.stringify({ ok: false, reason }));
|
|
6681
|
+
} else {
|
|
6682
|
+
console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
|
|
6683
|
+
}
|
|
6684
|
+
process.exit(1);
|
|
6685
|
+
} finally {
|
|
6686
|
+
await db?.close();
|
|
6687
|
+
}
|
|
6688
|
+
});
|
|
6689
|
+
|
|
5853
6690
|
// src/cli/commands/bridge.ts
|
|
5854
6691
|
var isServeMode = false;
|
|
5855
6692
|
function jsonOut2(data) {
|
|
@@ -5906,7 +6743,7 @@ function parseTokenUpdates(opts) {
|
|
|
5906
6743
|
}
|
|
5907
6744
|
return updates;
|
|
5908
6745
|
}
|
|
5909
|
-
var bridgeCommand = new
|
|
6746
|
+
var bridgeCommand = new Command3("bridge").description(
|
|
5910
6747
|
"Machine-readable JSON protocol for AI integration"
|
|
5911
6748
|
);
|
|
5912
6749
|
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) => {
|
|
@@ -5934,6 +6771,45 @@ bridgeCommand.command("check-due").description("Check due cards for a user (JSON
|
|
|
5934
6771
|
});
|
|
5935
6772
|
});
|
|
5936
6773
|
});
|
|
6774
|
+
bridgeCommand.command("backup-db").description("Back up the local database into the workspace (JSON)").option(
|
|
6775
|
+
"--dir <path>",
|
|
6776
|
+
"Target directory (default: workspace dir, else ~/Documents/zam)"
|
|
6777
|
+
).action(async (opts) => {
|
|
6778
|
+
const target = getDatabaseTargetInfo();
|
|
6779
|
+
if (target.kind !== "local") {
|
|
6780
|
+
jsonOut2({
|
|
6781
|
+
ok: false,
|
|
6782
|
+
reason: "remote",
|
|
6783
|
+
target: target.kind,
|
|
6784
|
+
location: target.location
|
|
6785
|
+
});
|
|
6786
|
+
return;
|
|
6787
|
+
}
|
|
6788
|
+
await withDb2(async (db) => {
|
|
6789
|
+
const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir10(), "Documents", "zam");
|
|
6790
|
+
const path = await backupDatabaseTo(db, workspaceDir);
|
|
6791
|
+
jsonOut2({ ok: true, path });
|
|
6792
|
+
});
|
|
6793
|
+
});
|
|
6794
|
+
bridgeCommand.command("workspace-info").description("Report the workspace dir, its default, and the data dir (JSON)").action(async () => {
|
|
6795
|
+
await withDb2(async (db) => {
|
|
6796
|
+
const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
|
|
6797
|
+
jsonOut2({
|
|
6798
|
+
workspaceDir,
|
|
6799
|
+
defaultWorkspaceDir: join16(homedir10(), "Documents", "zam"),
|
|
6800
|
+
dataDir: join16(homedir10(), ".zam")
|
|
6801
|
+
});
|
|
6802
|
+
});
|
|
6803
|
+
});
|
|
6804
|
+
bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
|
|
6805
|
+
const raw = String(opts.dir ?? "").trim();
|
|
6806
|
+
if (!raw) jsonError("A non-empty --dir is required");
|
|
6807
|
+
const dir = resolve3(raw);
|
|
6808
|
+
await withDb2(async (db) => {
|
|
6809
|
+
await setSetting(db, "personal.workspace_dir", dir);
|
|
6810
|
+
jsonOut2({ ok: true, workspaceDir: dir });
|
|
6811
|
+
});
|
|
6812
|
+
});
|
|
5937
6813
|
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) => {
|
|
5938
6814
|
await withDb2(async (db) => {
|
|
5939
6815
|
const userId = await resolveUser(opts, db, { json: true });
|
|
@@ -6292,7 +7168,7 @@ bridgeCommand.command("discover-skills").description(
|
|
|
6292
7168
|
"20"
|
|
6293
7169
|
).action(async (opts) => {
|
|
6294
7170
|
try {
|
|
6295
|
-
const monitorDir =
|
|
7171
|
+
const monitorDir = join16(homedir10(), ".zam", "monitor");
|
|
6296
7172
|
let files;
|
|
6297
7173
|
try {
|
|
6298
7174
|
files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -6305,7 +7181,7 @@ bridgeCommand.command("discover-skills").description(
|
|
|
6305
7181
|
return;
|
|
6306
7182
|
}
|
|
6307
7183
|
const limit = Number(opts.limit);
|
|
6308
|
-
const sorted = files.map((f) => ({ name: f, path:
|
|
7184
|
+
const sorted = files.map((f) => ({ name: f, path: join16(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
|
|
6309
7185
|
const sessionCommands = /* @__PURE__ */ new Map();
|
|
6310
7186
|
for (const file of sorted) {
|
|
6311
7187
|
const sessionId = file.name.replace(".jsonl", "");
|
|
@@ -6417,7 +7293,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
|
|
|
6417
7293
|
});
|
|
6418
7294
|
function resolveWindowsPowerShell() {
|
|
6419
7295
|
try {
|
|
6420
|
-
|
|
7296
|
+
execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
|
|
6421
7297
|
return "pwsh";
|
|
6422
7298
|
} catch {
|
|
6423
7299
|
return "powershell";
|
|
@@ -6432,7 +7308,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
|
|
|
6432
7308
|
throw new Error(`Invalid process name format: ${processName}`);
|
|
6433
7309
|
}
|
|
6434
7310
|
if (platform === "win32") {
|
|
6435
|
-
const stdout =
|
|
7311
|
+
const stdout = execFileSync4(
|
|
6436
7312
|
resolveWindowsPowerShell(),
|
|
6437
7313
|
[
|
|
6438
7314
|
"-NoProfile",
|
|
@@ -6745,13 +7621,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
|
|
|
6745
7621
|
} else if (platform === "darwin") {
|
|
6746
7622
|
if (hwnd) {
|
|
6747
7623
|
const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
|
|
6748
|
-
|
|
7624
|
+
execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
|
|
6749
7625
|
stdio: "pipe"
|
|
6750
7626
|
});
|
|
6751
7627
|
return { method: "screencapture-window", target: null };
|
|
6752
7628
|
} else if (processName) {
|
|
6753
7629
|
try {
|
|
6754
|
-
const windowId =
|
|
7630
|
+
const windowId = execFileSync4(
|
|
6755
7631
|
"osascript",
|
|
6756
7632
|
[
|
|
6757
7633
|
"-e",
|
|
@@ -6760,17 +7636,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
|
|
|
6760
7636
|
{ encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
|
|
6761
7637
|
).trim();
|
|
6762
7638
|
if (windowId && /^\d+$/.test(windowId)) {
|
|
6763
|
-
|
|
7639
|
+
execFileSync4("screencapture", ["-l", windowId, outputPath], {
|
|
6764
7640
|
stdio: "pipe"
|
|
6765
7641
|
});
|
|
6766
7642
|
return { method: "screencapture-window", target: null };
|
|
6767
7643
|
}
|
|
6768
7644
|
} catch {
|
|
6769
7645
|
}
|
|
6770
|
-
|
|
7646
|
+
execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
|
|
6771
7647
|
return { method: "screencapture-full", target: null };
|
|
6772
7648
|
} else {
|
|
6773
|
-
|
|
7649
|
+
execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
|
|
6774
7650
|
return { method: "screencapture-full", target: null };
|
|
6775
7651
|
}
|
|
6776
7652
|
} else {
|
|
@@ -6807,7 +7683,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
|
|
|
6807
7683
|
return;
|
|
6808
7684
|
}
|
|
6809
7685
|
}
|
|
6810
|
-
const outputPath = opts.image ?? opts.output ??
|
|
7686
|
+
const outputPath = opts.image ?? opts.output ?? join16(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
|
|
6811
7687
|
const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
|
|
6812
7688
|
if (!isProvided) {
|
|
6813
7689
|
const post = decidePostCapture(policy, {
|
|
@@ -6851,21 +7727,22 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
|
|
|
6851
7727
|
});
|
|
6852
7728
|
});
|
|
6853
7729
|
});
|
|
6854
|
-
bridgeCommand.command("start-recording").description("Start screen recording in the background (
|
|
7730
|
+
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) => {
|
|
6855
7731
|
const platform = process.platform;
|
|
6856
|
-
if (platform !== "darwin") {
|
|
7732
|
+
if (platform !== "darwin" && platform !== "win32") {
|
|
6857
7733
|
jsonOut2({
|
|
6858
7734
|
sessionId: opts.session,
|
|
6859
7735
|
started: false,
|
|
6860
|
-
error: "Screen recording is only supported on macOS (darwin)"
|
|
7736
|
+
error: "Screen recording is only supported on macOS (darwin) and Windows (win32)"
|
|
6861
7737
|
});
|
|
6862
7738
|
return;
|
|
6863
7739
|
}
|
|
6864
7740
|
const sessionId = opts.session;
|
|
6865
|
-
const statePath =
|
|
6866
|
-
const
|
|
6867
|
-
const
|
|
6868
|
-
|
|
7741
|
+
const statePath = join16(tmpdir3(), `zam-recording-${sessionId}.json`);
|
|
7742
|
+
const defaultExt = platform === "win32" ? ".mkv" : ".mov";
|
|
7743
|
+
const outputPath = opts.output ?? join16(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
|
|
7744
|
+
const { existsSync: existsSync24, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
|
|
7745
|
+
if (existsSync24(statePath)) {
|
|
6869
7746
|
jsonOut2({
|
|
6870
7747
|
sessionId,
|
|
6871
7748
|
started: false,
|
|
@@ -6873,7 +7750,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
6873
7750
|
});
|
|
6874
7751
|
return;
|
|
6875
7752
|
}
|
|
6876
|
-
const logPath =
|
|
7753
|
+
const logPath = join16(tmpdir3(), `zam-recording-${sessionId}.log`);
|
|
6877
7754
|
let logFd;
|
|
6878
7755
|
try {
|
|
6879
7756
|
logFd = openSync(logPath, "w");
|
|
@@ -6885,26 +7762,34 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
6885
7762
|
});
|
|
6886
7763
|
return;
|
|
6887
7764
|
}
|
|
6888
|
-
const { spawn:
|
|
6889
|
-
const
|
|
6890
|
-
"
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
7765
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
7766
|
+
const ffmpegArgs = platform === "darwin" ? [
|
|
7767
|
+
"-y",
|
|
7768
|
+
"-f",
|
|
7769
|
+
"avfoundation",
|
|
7770
|
+
"-r",
|
|
7771
|
+
"5",
|
|
7772
|
+
"-i",
|
|
7773
|
+
"0",
|
|
7774
|
+
"-pix_fmt",
|
|
7775
|
+
"yuv420p",
|
|
7776
|
+
outputPath
|
|
7777
|
+
] : [
|
|
7778
|
+
"-y",
|
|
7779
|
+
"-f",
|
|
7780
|
+
"gdigrab",
|
|
7781
|
+
"-framerate",
|
|
7782
|
+
"5",
|
|
7783
|
+
"-i",
|
|
7784
|
+
"desktop",
|
|
7785
|
+
"-pix_fmt",
|
|
7786
|
+
"yuv420p",
|
|
7787
|
+
outputPath
|
|
7788
|
+
];
|
|
7789
|
+
const child = spawn4("ffmpeg", ffmpegArgs, {
|
|
7790
|
+
detached: true,
|
|
7791
|
+
stdio: ["pipe", logFd, logFd]
|
|
7792
|
+
});
|
|
6908
7793
|
try {
|
|
6909
7794
|
closeSync(logFd);
|
|
6910
7795
|
} catch {
|
|
@@ -6935,21 +7820,21 @@ bridgeCommand.command("start-recording").description("Start screen recording in
|
|
|
6935
7820
|
}
|
|
6936
7821
|
});
|
|
6937
7822
|
bridgeCommand.command("stop-recording").description(
|
|
6938
|
-
"Stop active screen recording and apply idle-frame compression (
|
|
7823
|
+
"Stop active screen recording and apply idle-frame compression (JSON)"
|
|
6939
7824
|
).requiredOption("--session <id>", "ZAM session ID").action(async (opts) => {
|
|
6940
7825
|
const platform = process.platform;
|
|
6941
|
-
if (platform !== "darwin") {
|
|
7826
|
+
if (platform !== "darwin" && platform !== "win32") {
|
|
6942
7827
|
jsonOut2({
|
|
6943
7828
|
sessionId: opts.session,
|
|
6944
7829
|
stopped: false,
|
|
6945
|
-
error: "Screen recording is only supported on macOS (darwin)"
|
|
7830
|
+
error: "Screen recording is only supported on macOS (darwin) and Windows (win32)"
|
|
6946
7831
|
});
|
|
6947
7832
|
return;
|
|
6948
7833
|
}
|
|
6949
7834
|
const sessionId = opts.session;
|
|
6950
|
-
const statePath =
|
|
6951
|
-
const { existsSync:
|
|
6952
|
-
if (!
|
|
7835
|
+
const statePath = join16(tmpdir3(), `zam-recording-${sessionId}.json`);
|
|
7836
|
+
const { existsSync: existsSync24, readFileSync: readFileSync15, rmSync: rmSync3 } = await import("fs");
|
|
7837
|
+
if (!existsSync24(statePath)) {
|
|
6953
7838
|
jsonOut2({
|
|
6954
7839
|
sessionId,
|
|
6955
7840
|
stopped: false,
|
|
@@ -6961,32 +7846,32 @@ bridgeCommand.command("stop-recording").description(
|
|
|
6961
7846
|
const { pid, outputPath } = state;
|
|
6962
7847
|
try {
|
|
6963
7848
|
process.kill(pid, "SIGINT");
|
|
6964
|
-
} catch (
|
|
7849
|
+
} catch (_e) {
|
|
6965
7850
|
}
|
|
6966
7851
|
const isProcessRunning = (pId) => {
|
|
6967
7852
|
try {
|
|
6968
7853
|
process.kill(pId, 0);
|
|
6969
7854
|
return true;
|
|
6970
|
-
} catch (
|
|
7855
|
+
} catch (_e) {
|
|
6971
7856
|
return false;
|
|
6972
7857
|
}
|
|
6973
7858
|
};
|
|
6974
7859
|
let attempts = 0;
|
|
6975
7860
|
while (isProcessRunning(pid) && attempts < 20) {
|
|
6976
|
-
await new Promise((
|
|
7861
|
+
await new Promise((resolve6) => setTimeout(resolve6, 250));
|
|
6977
7862
|
attempts++;
|
|
6978
7863
|
}
|
|
6979
7864
|
if (isProcessRunning(pid)) {
|
|
6980
7865
|
try {
|
|
6981
7866
|
process.kill(pid, "SIGKILL");
|
|
6982
|
-
} catch (
|
|
7867
|
+
} catch (_e) {
|
|
6983
7868
|
}
|
|
6984
7869
|
}
|
|
6985
7870
|
try {
|
|
6986
7871
|
rmSync3(statePath, { force: true });
|
|
6987
7872
|
} catch {
|
|
6988
7873
|
}
|
|
6989
|
-
if (!
|
|
7874
|
+
if (!existsSync24(outputPath)) {
|
|
6990
7875
|
jsonOut2({
|
|
6991
7876
|
sessionId,
|
|
6992
7877
|
stopped: false,
|
|
@@ -6995,9 +7880,9 @@ bridgeCommand.command("stop-recording").description(
|
|
|
6995
7880
|
return;
|
|
6996
7881
|
}
|
|
6997
7882
|
const decimatedPath = outputPath.replace(/\.[^.]+$/, "-decimated.mp4");
|
|
6998
|
-
const { execSync:
|
|
7883
|
+
const { execSync: execSync6 } = await import("child_process");
|
|
6999
7884
|
try {
|
|
7000
|
-
|
|
7885
|
+
execSync6(
|
|
7001
7886
|
`ffmpeg -y -i "${outputPath}" -vf "mpdecimate,setpts=N/FRAME_RATE/TB" -an -pix_fmt yuv420p "${decimatedPath}"`,
|
|
7002
7887
|
{ stdio: "ignore" }
|
|
7003
7888
|
);
|
|
@@ -7050,11 +7935,13 @@ bridgeCommand.command("sync-observer-policy").description(
|
|
|
7050
7935
|
});
|
|
7051
7936
|
bridgeCommand.command("check-llm").description("Check if LLM is enabled and online (JSON)").action(async () => {
|
|
7052
7937
|
await withDb2(async (db) => {
|
|
7053
|
-
const
|
|
7938
|
+
const provider = await getProviderForRole(db, "recall");
|
|
7939
|
+
const { enabled, url, model, apiKey } = provider;
|
|
7940
|
+
const unsupportedProvider = provider.apiFlavor !== "chat-completions";
|
|
7054
7941
|
let online = false;
|
|
7055
7942
|
let availableModels = [];
|
|
7056
7943
|
let modelAvailable = false;
|
|
7057
|
-
if (enabled) {
|
|
7944
|
+
if (enabled && !unsupportedProvider) {
|
|
7058
7945
|
online = await isLlmOnline(url);
|
|
7059
7946
|
if (online) {
|
|
7060
7947
|
availableModels = await getAvailableModels(url, apiKey);
|
|
@@ -7069,7 +7956,9 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
|
|
|
7069
7956
|
url,
|
|
7070
7957
|
model,
|
|
7071
7958
|
modelAvailable,
|
|
7072
|
-
availableModels
|
|
7959
|
+
availableModels,
|
|
7960
|
+
apiFlavor: provider.apiFlavor,
|
|
7961
|
+
unsupportedProvider
|
|
7073
7962
|
});
|
|
7074
7963
|
});
|
|
7075
7964
|
});
|
|
@@ -7385,8 +8274,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
|
|
|
7385
8274
|
});
|
|
7386
8275
|
|
|
7387
8276
|
// src/cli/commands/card.ts
|
|
7388
|
-
import { Command as
|
|
7389
|
-
var cardCommand = new
|
|
8277
|
+
import { Command as Command4 } from "commander";
|
|
8278
|
+
var cardCommand = new Command4("card").description(
|
|
7390
8279
|
"Manage spaced-repetition cards"
|
|
7391
8280
|
);
|
|
7392
8281
|
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) => {
|
|
@@ -7584,9 +8473,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
|
|
|
7584
8473
|
});
|
|
7585
8474
|
|
|
7586
8475
|
// src/cli/commands/connector.ts
|
|
7587
|
-
import { input, password } from "@inquirer/prompts";
|
|
7588
|
-
import { Command as
|
|
7589
|
-
var connectorCommand = new
|
|
8476
|
+
import { input as input2, password } from "@inquirer/prompts";
|
|
8477
|
+
import { Command as Command5 } from "commander";
|
|
8478
|
+
var connectorCommand = new Command5("connector").description(
|
|
7590
8479
|
"Manage external service connectors"
|
|
7591
8480
|
);
|
|
7592
8481
|
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(
|
|
@@ -7605,10 +8494,10 @@ connectorCommand.command("setup").description("Configure a connector").argument(
|
|
|
7605
8494
|
process.exit(1);
|
|
7606
8495
|
}
|
|
7607
8496
|
try {
|
|
7608
|
-
const orgUrl = await
|
|
8497
|
+
const orgUrl = await input2({
|
|
7609
8498
|
message: "Organization URL (e.g. https://dev.azure.com/myorg):"
|
|
7610
8499
|
});
|
|
7611
|
-
const project = await
|
|
8500
|
+
const project = await input2({
|
|
7612
8501
|
message: "Project name:"
|
|
7613
8502
|
});
|
|
7614
8503
|
const pat = await password({
|
|
@@ -7699,7 +8588,7 @@ connectorCommand.command("sync").description("Verify the Turso cloud database co
|
|
|
7699
8588
|
async function setupTurso(urlArg, tokenArg, mode) {
|
|
7700
8589
|
let db;
|
|
7701
8590
|
try {
|
|
7702
|
-
const url = urlArg ?? await
|
|
8591
|
+
const url = urlArg ?? await input2({
|
|
7703
8592
|
message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
|
|
7704
8593
|
});
|
|
7705
8594
|
const token = tokenArg ?? await password({
|
|
@@ -7728,27 +8617,27 @@ async function setupTurso(urlArg, tokenArg, mode) {
|
|
|
7728
8617
|
}
|
|
7729
8618
|
|
|
7730
8619
|
// src/cli/commands/git-sync.ts
|
|
7731
|
-
import { execSync as
|
|
7732
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
7733
|
-
import { join as
|
|
7734
|
-
import { Command as
|
|
8620
|
+
import { execSync as execSync5 } from "child_process";
|
|
8621
|
+
import { chmodSync as chmodSync2, existsSync as existsSync16, writeFileSync as writeFileSync8 } from "fs";
|
|
8622
|
+
import { join as join17 } from "path";
|
|
8623
|
+
import { Command as Command6 } from "commander";
|
|
7735
8624
|
function installHook2() {
|
|
7736
|
-
const gitDir =
|
|
7737
|
-
if (!
|
|
8625
|
+
const gitDir = join17(process.cwd(), ".git");
|
|
8626
|
+
if (!existsSync16(gitDir)) {
|
|
7738
8627
|
console.error(
|
|
7739
8628
|
"Error: Current directory is not the root of a Git repository."
|
|
7740
8629
|
);
|
|
7741
8630
|
process.exit(1);
|
|
7742
8631
|
}
|
|
7743
|
-
const hooksDir =
|
|
7744
|
-
const hookPath =
|
|
8632
|
+
const hooksDir = join17(gitDir, "hooks");
|
|
8633
|
+
const hookPath = join17(hooksDir, "post-commit");
|
|
7745
8634
|
const hookContent = `#!/bin/sh
|
|
7746
8635
|
# ZAM Spaced Repetition Auto-Stale Hook
|
|
7747
8636
|
# Triggered automatically on git commits to decay modified concept cards.
|
|
7748
8637
|
zam git-sync --commit HEAD --quiet
|
|
7749
8638
|
`;
|
|
7750
8639
|
try {
|
|
7751
|
-
|
|
8640
|
+
writeFileSync8(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
|
|
7752
8641
|
try {
|
|
7753
8642
|
chmodSync2(hookPath, "755");
|
|
7754
8643
|
} catch (_e) {
|
|
@@ -7761,7 +8650,7 @@ zam git-sync --commit HEAD --quiet
|
|
|
7761
8650
|
process.exit(1);
|
|
7762
8651
|
}
|
|
7763
8652
|
}
|
|
7764
|
-
var gitSyncCommand = new
|
|
8653
|
+
var gitSyncCommand = new Command6("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) => {
|
|
7765
8654
|
if (opts.install) {
|
|
7766
8655
|
installHook2();
|
|
7767
8656
|
return;
|
|
@@ -7770,7 +8659,7 @@ var gitSyncCommand = new Command5("git-sync").description("Sync learning cards w
|
|
|
7770
8659
|
const userId = await resolveUser(opts, db);
|
|
7771
8660
|
let changedFiles = [];
|
|
7772
8661
|
try {
|
|
7773
|
-
const output =
|
|
8662
|
+
const output = execSync5(
|
|
7774
8663
|
`git diff-tree --no-commit-id --name-only -r ${opts.commit}`,
|
|
7775
8664
|
{
|
|
7776
8665
|
encoding: "utf-8",
|
|
@@ -7850,10 +8739,10 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
|
|
|
7850
8739
|
});
|
|
7851
8740
|
|
|
7852
8741
|
// src/cli/commands/goal.ts
|
|
7853
|
-
import { existsSync as
|
|
7854
|
-
import { resolve as
|
|
7855
|
-
import { input as
|
|
7856
|
-
import { Command as
|
|
8742
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
|
|
8743
|
+
import { resolve as resolve4 } from "path";
|
|
8744
|
+
import { input as input3 } from "@inquirer/prompts";
|
|
8745
|
+
import { Command as Command7 } from "commander";
|
|
7857
8746
|
async function resolveGoalsDir() {
|
|
7858
8747
|
let goalsDir;
|
|
7859
8748
|
let db;
|
|
@@ -7864,9 +8753,9 @@ async function resolveGoalsDir() {
|
|
|
7864
8753
|
} finally {
|
|
7865
8754
|
await db?.close();
|
|
7866
8755
|
}
|
|
7867
|
-
return goalsDir ?
|
|
8756
|
+
return goalsDir ? resolve4(goalsDir) : resolve4("goals");
|
|
7868
8757
|
}
|
|
7869
|
-
var goalCommand = new
|
|
8758
|
+
var goalCommand = new Command7("goal").description(
|
|
7870
8759
|
"Manage learning goals (markdown files)"
|
|
7871
8760
|
);
|
|
7872
8761
|
goalCommand.command("list").description("List all goals").option(
|
|
@@ -7874,7 +8763,7 @@ goalCommand.command("list").description("List all goals").option(
|
|
|
7874
8763
|
"Filter by status (active, completed, paused, abandoned)"
|
|
7875
8764
|
).option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action(async (opts) => {
|
|
7876
8765
|
const goalsDir = await resolveGoalsDir();
|
|
7877
|
-
if (!
|
|
8766
|
+
if (!existsSync17(goalsDir)) {
|
|
7878
8767
|
console.error(`Goals directory not found: ${goalsDir}`);
|
|
7879
8768
|
console.error(
|
|
7880
8769
|
"Set it with: zam settings set personal.goals_dir /path/to/goals"
|
|
@@ -7975,8 +8864,8 @@ ${"\u2500".repeat(50)}`);
|
|
|
7975
8864
|
});
|
|
7976
8865
|
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) => {
|
|
7977
8866
|
const goalsDir = await resolveGoalsDir();
|
|
7978
|
-
if (!
|
|
7979
|
-
|
|
8867
|
+
if (!existsSync17(goalsDir)) {
|
|
8868
|
+
mkdirSync9(goalsDir, { recursive: true });
|
|
7980
8869
|
}
|
|
7981
8870
|
let slug = opts.slug;
|
|
7982
8871
|
let title = opts.title;
|
|
@@ -7985,11 +8874,11 @@ goalCommand.command("create").description("Create a new goal").option("--slug <s
|
|
|
7985
8874
|
if (!slug || !title) {
|
|
7986
8875
|
try {
|
|
7987
8876
|
if (!title) {
|
|
7988
|
-
title = await
|
|
8877
|
+
title = await input3({ message: "Goal title:" });
|
|
7989
8878
|
}
|
|
7990
8879
|
if (!slug) {
|
|
7991
8880
|
const suggested = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
7992
|
-
slug = await
|
|
8881
|
+
slug = await input3({
|
|
7993
8882
|
message: "Goal slug (filename):",
|
|
7994
8883
|
default: suggested
|
|
7995
8884
|
});
|
|
@@ -8035,22 +8924,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
|
|
|
8035
8924
|
});
|
|
8036
8925
|
|
|
8037
8926
|
// src/cli/commands/init.ts
|
|
8038
|
-
import { existsSync as
|
|
8039
|
-
import { homedir as
|
|
8040
|
-
import { join as
|
|
8041
|
-
import { confirm, input as
|
|
8042
|
-
import { Command as
|
|
8043
|
-
var HOME2 =
|
|
8927
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
8928
|
+
import { homedir as homedir11 } from "os";
|
|
8929
|
+
import { join as join18 } from "path";
|
|
8930
|
+
import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
|
|
8931
|
+
import { Command as Command8 } from "commander";
|
|
8932
|
+
var HOME2 = homedir11();
|
|
8044
8933
|
function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
|
|
8045
8934
|
console.log(`${color}${char.repeat(len)}\x1B[0m`);
|
|
8046
8935
|
}
|
|
8047
8936
|
function bootstrapSandboxWorkspace(workspaceDir) {
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
const worldviewFile =
|
|
8052
|
-
if (!
|
|
8053
|
-
|
|
8937
|
+
mkdirSync10(join18(workspaceDir, "beliefs"), { recursive: true });
|
|
8938
|
+
mkdirSync10(join18(workspaceDir, "goals"), { recursive: true });
|
|
8939
|
+
mkdirSync10(join18(workspaceDir, "skills"), { recursive: true });
|
|
8940
|
+
const worldviewFile = join18(workspaceDir, "beliefs", "worldview.md");
|
|
8941
|
+
if (!existsSync18(worldviewFile)) {
|
|
8942
|
+
writeFileSync9(
|
|
8054
8943
|
worldviewFile,
|
|
8055
8944
|
`# Personal Worldview
|
|
8056
8945
|
|
|
@@ -8062,9 +8951,9 @@ Here, I declare the core concepts and principles I want to master.
|
|
|
8062
8951
|
"utf8"
|
|
8063
8952
|
);
|
|
8064
8953
|
}
|
|
8065
|
-
const goalsFile =
|
|
8066
|
-
if (!
|
|
8067
|
-
|
|
8954
|
+
const goalsFile = join18(workspaceDir, "goals", "goals.md");
|
|
8955
|
+
if (!existsSync18(goalsFile)) {
|
|
8956
|
+
writeFileSync9(
|
|
8068
8957
|
goalsFile,
|
|
8069
8958
|
`# Personal Goals
|
|
8070
8959
|
|
|
@@ -8076,7 +8965,7 @@ Here, I declare the core concepts and principles I want to master.
|
|
|
8076
8965
|
);
|
|
8077
8966
|
}
|
|
8078
8967
|
}
|
|
8079
|
-
var initCommand = new
|
|
8968
|
+
var initCommand = new Command8("init").description("Launch the guided interactive onboarding wizard").action(async () => {
|
|
8080
8969
|
printLine();
|
|
8081
8970
|
console.log(
|
|
8082
8971
|
"\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
|
|
@@ -8086,8 +8975,8 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
|
|
|
8086
8975
|
);
|
|
8087
8976
|
printLine();
|
|
8088
8977
|
console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
|
|
8089
|
-
const defaultWorkspace =
|
|
8090
|
-
const workspacePath = await
|
|
8978
|
+
const defaultWorkspace = join18(HOME2, "Documents", "zam");
|
|
8979
|
+
const workspacePath = await input4({
|
|
8091
8980
|
message: "Choose your ZAM workspace directory:",
|
|
8092
8981
|
default: defaultWorkspace
|
|
8093
8982
|
});
|
|
@@ -8123,7 +9012,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
|
|
|
8123
9012
|
\x1B[1mRecommendation:\x1B[0m ZAM suggests installing \x1B[32m${runnerLabel}\x1B[0m with \x1B[36m${profile.recommendedModel}\x1B[0m.`
|
|
8124
9013
|
);
|
|
8125
9014
|
console.log("\n\x1B[1m[3/5] Setting up Local LLM Runner\x1B[0m");
|
|
8126
|
-
const proceedInstall = await
|
|
9015
|
+
const proceedInstall = await confirm2({
|
|
8127
9016
|
message: `Would you like ZAM to install and configure ${runnerLabel} automatically?`,
|
|
8128
9017
|
default: true
|
|
8129
9018
|
});
|
|
@@ -8191,7 +9080,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
|
|
|
8191
9080
|
console.log(
|
|
8192
9081
|
"\n\x1B[1m[5/5] Wiring Developer Agents & Terminal Helpers\x1B[0m"
|
|
8193
9082
|
);
|
|
8194
|
-
const proceedHooks = await
|
|
9083
|
+
const proceedHooks = await confirm2({
|
|
8195
9084
|
message: "Distribute ZAM skills and install optional monitored-session shell helpers?",
|
|
8196
9085
|
default: true
|
|
8197
9086
|
});
|
|
@@ -8242,8 +9131,8 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
|
|
|
8242
9131
|
});
|
|
8243
9132
|
|
|
8244
9133
|
// src/cli/commands/learn.ts
|
|
8245
|
-
import { input as
|
|
8246
|
-
import { Command as
|
|
9134
|
+
import { input as input6 } from "@inquirer/prompts";
|
|
9135
|
+
import { Command as Command9 } from "commander";
|
|
8247
9136
|
|
|
8248
9137
|
// src/cli/learn-format.ts
|
|
8249
9138
|
var BLOOM_VERBS3 = {
|
|
@@ -8292,7 +9181,7 @@ function formatReveal(input8) {
|
|
|
8292
9181
|
}
|
|
8293
9182
|
|
|
8294
9183
|
// src/cli/review-actions.ts
|
|
8295
|
-
import { confirm as
|
|
9184
|
+
import { confirm as confirm3, input as input5, select } from "@inquirer/prompts";
|
|
8296
9185
|
async function runInteractiveReviewAction(inputData) {
|
|
8297
9186
|
let currentItem = { ...inputData.item };
|
|
8298
9187
|
while (true) {
|
|
@@ -8407,7 +9296,7 @@ async function runInteractiveReviewAction(inputData) {
|
|
|
8407
9296
|
continue;
|
|
8408
9297
|
}
|
|
8409
9298
|
if (choice === "deprecate-token") {
|
|
8410
|
-
const approved = await
|
|
9299
|
+
const approved = await confirm3({
|
|
8411
9300
|
message: `Deprecate ${currentItem.slug} so it stops appearing in review queues?`,
|
|
8412
9301
|
default: false
|
|
8413
9302
|
});
|
|
@@ -8438,7 +9327,7 @@ async function runInteractiveReviewAction(inputData) {
|
|
|
8438
9327
|
console.log(` Session steps: ${impact.session_steps}`);
|
|
8439
9328
|
console.log(` Sessions touched: ${impact.sessions_touched}`);
|
|
8440
9329
|
console.log(` Agent skills updated: ${impact.agent_skills}`);
|
|
8441
|
-
const approved = await
|
|
9330
|
+
const approved = await confirm3({
|
|
8442
9331
|
message: "Permanently delete this token and its dependent learning data?",
|
|
8443
9332
|
default: false
|
|
8444
9333
|
});
|
|
@@ -8463,7 +9352,7 @@ async function runInteractiveReviewAction(inputData) {
|
|
|
8463
9352
|
);
|
|
8464
9353
|
console.log(`Delete your card for ${currentItem.slug}?`);
|
|
8465
9354
|
console.log(` Review logs removed: ${impact.review_logs}`);
|
|
8466
|
-
const approved = await
|
|
9355
|
+
const approved = await confirm3({
|
|
8467
9356
|
message: "Delete only your card for this token?",
|
|
8468
9357
|
default: false
|
|
8469
9358
|
});
|
|
@@ -8501,21 +9390,21 @@ async function promptTokenEdit(token) {
|
|
|
8501
9390
|
}
|
|
8502
9391
|
switch (field) {
|
|
8503
9392
|
case "concept": {
|
|
8504
|
-
const concept = await
|
|
9393
|
+
const concept = await input5({
|
|
8505
9394
|
message: "Concept:",
|
|
8506
9395
|
default: token.concept
|
|
8507
9396
|
});
|
|
8508
9397
|
return concept === token.concept ? null : { concept };
|
|
8509
9398
|
}
|
|
8510
9399
|
case "domain": {
|
|
8511
|
-
const domain = await
|
|
9400
|
+
const domain = await input5({
|
|
8512
9401
|
message: "Domain (blank allowed):",
|
|
8513
9402
|
default: token.domain
|
|
8514
9403
|
});
|
|
8515
9404
|
return domain === token.domain ? null : { domain };
|
|
8516
9405
|
}
|
|
8517
9406
|
case "context": {
|
|
8518
|
-
const context = await
|
|
9407
|
+
const context = await input5({
|
|
8519
9408
|
message: "Context (blank allowed):",
|
|
8520
9409
|
default: token.context
|
|
8521
9410
|
});
|
|
@@ -8556,14 +9445,14 @@ async function promptTokenEdit(token) {
|
|
|
8556
9445
|
return mode === token.symbiosis_mode ? null : { symbiosis_mode: mode };
|
|
8557
9446
|
}
|
|
8558
9447
|
case "source_link": {
|
|
8559
|
-
const link = await
|
|
9448
|
+
const link = await input5({
|
|
8560
9449
|
message: "Source link (blank allowed):",
|
|
8561
9450
|
default: token.source_link ?? ""
|
|
8562
9451
|
});
|
|
8563
9452
|
return link === token.source_link ? null : { source_link: link || null };
|
|
8564
9453
|
}
|
|
8565
9454
|
case "question": {
|
|
8566
|
-
const question = await
|
|
9455
|
+
const question = await input5({
|
|
8567
9456
|
message: "Recall question (blank allowed):",
|
|
8568
9457
|
default: token.question ?? ""
|
|
8569
9458
|
});
|
|
@@ -8581,7 +9470,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
|
|
|
8581
9470
|
function isExitPrompt(err) {
|
|
8582
9471
|
return err instanceof Error && err.name === "ExitPromptError";
|
|
8583
9472
|
}
|
|
8584
|
-
var learnCommand = new
|
|
9473
|
+
var learnCommand = new Command9("learn").description(
|
|
8585
9474
|
"Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
|
|
8586
9475
|
).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) => {
|
|
8587
9476
|
let db;
|
|
@@ -8661,7 +9550,7 @@ ${"\u2500".repeat(50)}`);
|
|
|
8661
9550
|
${prompt.question}`);
|
|
8662
9551
|
let answer;
|
|
8663
9552
|
try {
|
|
8664
|
-
answer = await
|
|
9553
|
+
answer = await input6({
|
|
8665
9554
|
message: t(locale, "prompt_answer")
|
|
8666
9555
|
});
|
|
8667
9556
|
} catch (err) {
|
|
@@ -8781,36 +9670,52 @@ ${"\u2550".repeat(50)}`);
|
|
|
8781
9670
|
process.exit(1);
|
|
8782
9671
|
}
|
|
8783
9672
|
});
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
import { tmpdir as tmpdir3 } from "os";
|
|
8789
|
-
import { basename as basename3, join as join16 } from "path";
|
|
8790
|
-
import { Command as Command9 } from "commander";
|
|
8791
|
-
function isPowerShellShell(shell) {
|
|
8792
|
-
return shell === "pwsh" || shell === "powershell";
|
|
9673
|
+
function buildLearnCommand(shell, userId) {
|
|
9674
|
+
const zamInvocation = resolveZamInvocation(shell);
|
|
9675
|
+
const learnArgs = userId ? ` learn --user ${userId}` : " learn";
|
|
9676
|
+
return `${zamInvocation}${learnArgs}`;
|
|
8793
9677
|
}
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
9678
|
+
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(
|
|
9679
|
+
"--shell <type>",
|
|
9680
|
+
"Shell type: zsh | bash | pwsh | powershell (auto-detected)"
|
|
9681
|
+
).action(async (opts) => {
|
|
9682
|
+
let shell;
|
|
9683
|
+
try {
|
|
9684
|
+
shell = normalizeShell(opts.shell);
|
|
9685
|
+
} catch (err) {
|
|
9686
|
+
console.error(`Error: ${err.message}`);
|
|
9687
|
+
process.exit(1);
|
|
8799
9688
|
}
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
}
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
9689
|
+
let db;
|
|
9690
|
+
let userId = opts.user;
|
|
9691
|
+
try {
|
|
9692
|
+
db = await openDatabase();
|
|
9693
|
+
if (!userId) {
|
|
9694
|
+
userId = await resolveUser(opts, db);
|
|
9695
|
+
}
|
|
9696
|
+
if (!await getSetting(db, "review_method")) {
|
|
9697
|
+
await setSetting(db, "review_method", "console");
|
|
9698
|
+
}
|
|
9699
|
+
} catch (err) {
|
|
9700
|
+
console.error(`Error: ${err.message}`);
|
|
9701
|
+
process.exit(1);
|
|
9702
|
+
} finally {
|
|
9703
|
+
await db?.close();
|
|
9704
|
+
}
|
|
9705
|
+
const dir = opts.dir ?? process.cwd();
|
|
9706
|
+
const learnCommandLine = buildLearnCommand(shell, userId);
|
|
9707
|
+
const shellSetup = buildShellSetupCommand(dir, shell, learnCommandLine);
|
|
9708
|
+
openTerminalWindow({
|
|
9709
|
+
shellSetup,
|
|
9710
|
+
label: "learn",
|
|
9711
|
+
dir,
|
|
9712
|
+
shell
|
|
9713
|
+
});
|
|
9714
|
+
});
|
|
9715
|
+
|
|
9716
|
+
// src/cli/commands/monitor.ts
|
|
9717
|
+
import { Command as Command10 } from "commander";
|
|
9718
|
+
var monitorCommand = new Command10("monitor").description(
|
|
8814
9719
|
"Shell observation for real-time task monitoring"
|
|
8815
9720
|
);
|
|
8816
9721
|
monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
|
|
@@ -8937,39 +9842,6 @@ monitorCommand.command("status").description("Show monitoring status for a sessi
|
|
|
8937
9842
|
console.log(` To: ${result.timeSpan.end}`);
|
|
8938
9843
|
}
|
|
8939
9844
|
});
|
|
8940
|
-
function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
|
|
8941
|
-
if (results.length === 0) return null;
|
|
8942
|
-
const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
|
|
8943
|
-
const runnable = results.find(
|
|
8944
|
-
(result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
|
|
8945
|
-
);
|
|
8946
|
-
return runnable ?? results[0];
|
|
8947
|
-
}
|
|
8948
|
-
function findExecutable(command) {
|
|
8949
|
-
try {
|
|
8950
|
-
const lookup = process.platform === "win32" ? `where.exe ${command}` : `command -v ${command}`;
|
|
8951
|
-
const results = execSync5(lookup, { encoding: "utf-8" }).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
8952
|
-
if (results.length === 0) return null;
|
|
8953
|
-
if (process.platform === "win32") {
|
|
8954
|
-
return selectWindowsExecutable(results);
|
|
8955
|
-
}
|
|
8956
|
-
return results[0];
|
|
8957
|
-
} catch {
|
|
8958
|
-
return null;
|
|
8959
|
-
}
|
|
8960
|
-
}
|
|
8961
|
-
function resolveZamInvocation(shell) {
|
|
8962
|
-
const installed = findExecutable("zam");
|
|
8963
|
-
if (installed) {
|
|
8964
|
-
return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
|
|
8965
|
-
}
|
|
8966
|
-
const projectRoot = join16(import.meta.dirname, "..", "..", "..");
|
|
8967
|
-
const cliSource = join16(projectRoot, "src/cli/index.ts");
|
|
8968
|
-
if (isPowerShellShell(shell)) {
|
|
8969
|
-
return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
|
|
8970
|
-
}
|
|
8971
|
-
return `npx --prefix ${JSON.stringify(projectRoot)} tsx ${JSON.stringify(cliSource)}`;
|
|
8972
|
-
}
|
|
8973
9845
|
function buildMonitorSetupCommand(dir, sessionId, shell) {
|
|
8974
9846
|
const zamInvocation = resolveZamInvocation(shell);
|
|
8975
9847
|
if (isPowerShellShell(shell)) {
|
|
@@ -8982,17 +9854,6 @@ function buildMonitorSetupCommand(dir, sessionId, shell) {
|
|
|
8982
9854
|
}
|
|
8983
9855
|
return `cd ${JSON.stringify(dir)} && eval "$(${zamInvocation} monitor start --session ${sessionId} --shell ${shell})"`;
|
|
8984
9856
|
}
|
|
8985
|
-
function isItermRunning() {
|
|
8986
|
-
try {
|
|
8987
|
-
const result = execSync5(
|
|
8988
|
-
`osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"' 2>/dev/null`,
|
|
8989
|
-
{ encoding: "utf-8" }
|
|
8990
|
-
).trim();
|
|
8991
|
-
return result === "true";
|
|
8992
|
-
} catch {
|
|
8993
|
-
return false;
|
|
8994
|
-
}
|
|
8995
|
-
}
|
|
8996
9857
|
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(
|
|
8997
9858
|
"--shell <type>",
|
|
8998
9859
|
"Shell type: zsh | bash | pwsh | powershell (auto-detected)"
|
|
@@ -9025,89 +9886,19 @@ monitorCommand.command("open").description("Open a new monitored terminal window
|
|
|
9025
9886
|
} finally {
|
|
9026
9887
|
await db?.close();
|
|
9027
9888
|
}
|
|
9028
|
-
const dir = opts.dir ?? process.cwd();
|
|
9029
|
-
const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
console.log(` ${shellSetup}
|
|
9038
|
-
`);
|
|
9039
|
-
console.log(
|
|
9040
|
-
`(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
|
|
9041
|
-
);
|
|
9042
|
-
}
|
|
9043
|
-
});
|
|
9044
|
-
function openMacTerminal(shellSetup, sessionId, dir) {
|
|
9045
|
-
const useIterm = isItermRunning();
|
|
9046
|
-
const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
9047
|
-
const appleScript = useIterm ? `tell application "iTerm2"
|
|
9048
|
-
activate
|
|
9049
|
-
set newWindow to (create window with default profile)
|
|
9050
|
-
tell current session of newWindow
|
|
9051
|
-
write text "${escaped}"
|
|
9052
|
-
end tell
|
|
9053
|
-
end tell` : `tell application "Terminal"
|
|
9054
|
-
activate
|
|
9055
|
-
do script "${escaped}"
|
|
9056
|
-
end tell`;
|
|
9057
|
-
const tmpFile = join16(tmpdir3(), `zam-monitor-${sessionId}.scpt`);
|
|
9058
|
-
try {
|
|
9059
|
-
writeFileSync8(tmpFile, appleScript);
|
|
9060
|
-
execSync5(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
|
|
9061
|
-
console.log(
|
|
9062
|
-
`Opened ${useIterm ? "iTerm2" : "Terminal.app"} window with monitoring for session ${sessionId}`
|
|
9063
|
-
);
|
|
9064
|
-
console.log(` Directory: ${dir}`);
|
|
9065
|
-
} catch (err) {
|
|
9066
|
-
console.error(`Failed to open terminal: ${err.message}`);
|
|
9067
|
-
console.log(`
|
|
9068
|
-
Run this manually in a new terminal:
|
|
9069
|
-
`);
|
|
9070
|
-
console.log(` ${shellSetup}`);
|
|
9071
|
-
} finally {
|
|
9072
|
-
try {
|
|
9073
|
-
unlinkSync(tmpFile);
|
|
9074
|
-
} catch {
|
|
9075
|
-
}
|
|
9076
|
-
}
|
|
9077
|
-
}
|
|
9078
|
-
function openWindowsPowerShell(shellSetup, sessionId, dir, requestedShell) {
|
|
9079
|
-
const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
|
|
9080
|
-
const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
|
|
9081
|
-
const startCommand = [
|
|
9082
|
-
"Start-Process",
|
|
9083
|
-
`-FilePath ${psSingleQuoted2(executable)}`,
|
|
9084
|
-
`-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
|
|
9085
|
-
].join(" ");
|
|
9086
|
-
const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
|
|
9087
|
-
try {
|
|
9088
|
-
execFileSync3(
|
|
9089
|
-
launcher,
|
|
9090
|
-
["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
|
|
9091
|
-
{
|
|
9092
|
-
stdio: "ignore"
|
|
9093
|
-
}
|
|
9094
|
-
);
|
|
9095
|
-
console.log(
|
|
9096
|
-
`Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window with monitoring for session ${sessionId}`
|
|
9097
|
-
);
|
|
9098
|
-
console.log(` Directory: ${dir}`);
|
|
9099
|
-
} catch (err) {
|
|
9100
|
-
console.error(`Failed to open PowerShell: ${err.message}`);
|
|
9101
|
-
console.log(`
|
|
9102
|
-
Run this manually in a new PowerShell terminal:
|
|
9103
|
-
`);
|
|
9104
|
-
console.log(` ${shellSetup}`);
|
|
9105
|
-
}
|
|
9106
|
-
}
|
|
9889
|
+
const dir = opts.dir ?? process.cwd();
|
|
9890
|
+
const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
|
|
9891
|
+
openTerminalWindow({
|
|
9892
|
+
shellSetup,
|
|
9893
|
+
label: `monitor-${opts.session}`,
|
|
9894
|
+
dir,
|
|
9895
|
+
shell
|
|
9896
|
+
});
|
|
9897
|
+
});
|
|
9107
9898
|
|
|
9108
9899
|
// src/cli/commands/observer.ts
|
|
9109
|
-
import { Command as
|
|
9110
|
-
var observerCommand = new
|
|
9900
|
+
import { Command as Command11 } from "commander";
|
|
9901
|
+
var observerCommand = new Command11("observer").description(
|
|
9111
9902
|
"Configure what the UI observer may capture (Layer 2 policy)"
|
|
9112
9903
|
);
|
|
9113
9904
|
function applyObserverListChange(current, entry, op) {
|
|
@@ -9169,9 +9960,9 @@ observerCommand.command("revoke <process>").description("Remove a process from o
|
|
|
9169
9960
|
});
|
|
9170
9961
|
|
|
9171
9962
|
// src/cli/commands/profile.ts
|
|
9172
|
-
import { homedir as
|
|
9173
|
-
import { dirname as dirname5, join as
|
|
9174
|
-
import { Command as
|
|
9963
|
+
import { homedir as homedir12 } from "os";
|
|
9964
|
+
import { dirname as dirname5, join as join19, resolve as resolve5 } from "path";
|
|
9965
|
+
import { Command as Command12 } from "commander";
|
|
9175
9966
|
var C2 = {
|
|
9176
9967
|
reset: "\x1B[0m",
|
|
9177
9968
|
bold: "\x1B[1m",
|
|
@@ -9180,7 +9971,7 @@ var C2 = {
|
|
|
9180
9971
|
green: "\x1B[32m"
|
|
9181
9972
|
};
|
|
9182
9973
|
function defaultPersonalDir() {
|
|
9183
|
-
return
|
|
9974
|
+
return join19(homedir12(), "Documents", "zam");
|
|
9184
9975
|
}
|
|
9185
9976
|
function render(profile) {
|
|
9186
9977
|
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}`;
|
|
@@ -9191,7 +9982,7 @@ function render(profile) {
|
|
|
9191
9982
|
console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
|
|
9192
9983
|
console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
|
|
9193
9984
|
}
|
|
9194
|
-
var profileCommand = new
|
|
9985
|
+
var profileCommand = new Command12("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) => {
|
|
9195
9986
|
if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
|
|
9196
9987
|
console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
|
|
9197
9988
|
process.exit(1);
|
|
@@ -9201,7 +9992,7 @@ var profileCommand = new Command11("profile").description("Show or change this m
|
|
|
9201
9992
|
if (opts.mode) setInstallMode(opts.mode);
|
|
9202
9993
|
db = await openDatabaseWithSync({ initialize: true });
|
|
9203
9994
|
if (opts.dir) {
|
|
9204
|
-
await setSetting(db, "personal.workspace_dir",
|
|
9995
|
+
await setSetting(db, "personal.workspace_dir", resolve5(opts.dir));
|
|
9205
9996
|
}
|
|
9206
9997
|
const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
|
|
9207
9998
|
await db.close();
|
|
@@ -9226,9 +10017,276 @@ var profileCommand = new Command11("profile").description("Show or change this m
|
|
|
9226
10017
|
}
|
|
9227
10018
|
});
|
|
9228
10019
|
|
|
10020
|
+
// src/cli/commands/provider.ts
|
|
10021
|
+
import { password as password2 } from "@inquirer/prompts";
|
|
10022
|
+
import { Command as Command13 } from "commander";
|
|
10023
|
+
var VALID_API_FLAVORS = [
|
|
10024
|
+
"chat-completions",
|
|
10025
|
+
"anthropic-messages"
|
|
10026
|
+
];
|
|
10027
|
+
var VALID_ROLES = ["vision", "recall", "text"];
|
|
10028
|
+
function upsertProviderRecord(providers, name, patch) {
|
|
10029
|
+
const merged = { ...providers[name] ?? {} };
|
|
10030
|
+
if (patch.url !== void 0) merged.url = patch.url;
|
|
10031
|
+
if (patch.model !== void 0) merged.model = patch.model;
|
|
10032
|
+
if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
|
|
10033
|
+
if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
|
|
10034
|
+
return { ...providers, [name]: merged };
|
|
10035
|
+
}
|
|
10036
|
+
function removeProviderRecord(providers, name) {
|
|
10037
|
+
if (!(name in providers)) return { providers, removed: false };
|
|
10038
|
+
const next = { ...providers };
|
|
10039
|
+
delete next[name];
|
|
10040
|
+
return { providers: next, removed: true };
|
|
10041
|
+
}
|
|
10042
|
+
function rolesReferencing(roles, name) {
|
|
10043
|
+
return VALID_ROLES.filter((role) => {
|
|
10044
|
+
const binding = roles[role];
|
|
10045
|
+
return binding?.primary === name || binding?.fallback === name;
|
|
10046
|
+
});
|
|
10047
|
+
}
|
|
10048
|
+
function bindRoleProviders(roles, role, primary, fallback) {
|
|
10049
|
+
const binding = { primary };
|
|
10050
|
+
if (fallback) binding.fallback = fallback;
|
|
10051
|
+
return { ...roles, [role]: binding };
|
|
10052
|
+
}
|
|
10053
|
+
function maskSecret(key) {
|
|
10054
|
+
return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
|
|
10055
|
+
}
|
|
10056
|
+
function buildProviderListing(providers, hasKey) {
|
|
10057
|
+
return Object.entries(providers).map(([name, rec]) => {
|
|
10058
|
+
const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
|
|
10059
|
+
let keyState;
|
|
10060
|
+
if (!rec.apiKeyRef) keyState = "none";
|
|
10061
|
+
else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
|
|
10062
|
+
return {
|
|
10063
|
+
name,
|
|
10064
|
+
url: rec.url,
|
|
10065
|
+
model: rec.model,
|
|
10066
|
+
apiFlavor,
|
|
10067
|
+
apiKeyRef: rec.apiKeyRef,
|
|
10068
|
+
keyState
|
|
10069
|
+
};
|
|
10070
|
+
});
|
|
10071
|
+
}
|
|
10072
|
+
function findOrphanKeyRefs(storedRefs, providers) {
|
|
10073
|
+
const used = new Set(
|
|
10074
|
+
Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
|
|
10075
|
+
);
|
|
10076
|
+
return storedRefs.filter((ref) => !used.has(ref));
|
|
10077
|
+
}
|
|
10078
|
+
async function readJson(db, key, fallback) {
|
|
10079
|
+
const raw = await getSetting(db, key);
|
|
10080
|
+
if (!raw) return fallback;
|
|
10081
|
+
try {
|
|
10082
|
+
return JSON.parse(raw);
|
|
10083
|
+
} catch {
|
|
10084
|
+
return fallback;
|
|
10085
|
+
}
|
|
10086
|
+
}
|
|
10087
|
+
var readProviders = (db) => readJson(db, "llm.providers", {});
|
|
10088
|
+
var readRoles = (db) => readJson(db, "llm.roles", {});
|
|
10089
|
+
async function writeProviders(db, p) {
|
|
10090
|
+
await setSetting(db, "llm.providers", JSON.stringify(p));
|
|
10091
|
+
}
|
|
10092
|
+
async function writeRoles(db, r) {
|
|
10093
|
+
await setSetting(db, "llm.roles", JSON.stringify(r));
|
|
10094
|
+
}
|
|
10095
|
+
var providerCommand = new Command13("provider").description(
|
|
10096
|
+
"Configure role-based AI providers (url/model/flavor/key, per role)"
|
|
10097
|
+
);
|
|
10098
|
+
providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").action(async (opts) => {
|
|
10099
|
+
await withDb(async (db) => {
|
|
10100
|
+
const providers = await readProviders(db);
|
|
10101
|
+
const roles = await readRoles(db);
|
|
10102
|
+
const rows = buildProviderListing(
|
|
10103
|
+
providers,
|
|
10104
|
+
(ref) => getProviderApiKey(ref) !== null
|
|
10105
|
+
);
|
|
10106
|
+
const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
|
|
10107
|
+
if (opts.json) {
|
|
10108
|
+
console.log(
|
|
10109
|
+
JSON.stringify({ providers: rows, roles, orphans }, null, 2)
|
|
10110
|
+
);
|
|
10111
|
+
return;
|
|
10112
|
+
}
|
|
10113
|
+
console.log("Providers (llm.providers):\n");
|
|
10114
|
+
if (rows.length === 0) {
|
|
10115
|
+
console.log(
|
|
10116
|
+
" (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
|
|
10117
|
+
);
|
|
10118
|
+
} else {
|
|
10119
|
+
for (const row of rows) {
|
|
10120
|
+
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";
|
|
10121
|
+
console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
|
|
10122
|
+
console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
|
|
10123
|
+
console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
|
|
10124
|
+
console.log(` flavor: ${row.apiFlavor}`);
|
|
10125
|
+
if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
|
|
10126
|
+
}
|
|
10127
|
+
}
|
|
10128
|
+
console.log("\nRoles (llm.roles):\n");
|
|
10129
|
+
for (const role of VALID_ROLES) {
|
|
10130
|
+
const binding = roles[role];
|
|
10131
|
+
if (!binding?.primary) {
|
|
10132
|
+
console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
|
|
10133
|
+
} else {
|
|
10134
|
+
const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
|
|
10135
|
+
console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
|
|
10136
|
+
}
|
|
10137
|
+
}
|
|
10138
|
+
if (orphans.length > 0) {
|
|
10139
|
+
console.log(
|
|
10140
|
+
`
|
|
10141
|
+
\x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
|
|
10142
|
+
);
|
|
10143
|
+
}
|
|
10144
|
+
});
|
|
10145
|
+
});
|
|
10146
|
+
providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option(
|
|
10147
|
+
"--flavor <flavor>",
|
|
10148
|
+
`Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
|
|
10149
|
+
).option(
|
|
10150
|
+
"--key-ref <ref>",
|
|
10151
|
+
"Credential reference for the API key (default: <name> when --key is given)"
|
|
10152
|
+
).option(
|
|
10153
|
+
"--key <value>",
|
|
10154
|
+
"Store this API key now (prefer `set-key` to keep it out of shell history)"
|
|
10155
|
+
).action(async (name, opts) => {
|
|
10156
|
+
let apiFlavor;
|
|
10157
|
+
if (opts.flavor) {
|
|
10158
|
+
if (!VALID_API_FLAVORS.includes(opts.flavor)) {
|
|
10159
|
+
console.error(
|
|
10160
|
+
`Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
|
|
10161
|
+
);
|
|
10162
|
+
process.exit(1);
|
|
10163
|
+
}
|
|
10164
|
+
apiFlavor = opts.flavor;
|
|
10165
|
+
}
|
|
10166
|
+
const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
|
|
10167
|
+
await withDb(async (db) => {
|
|
10168
|
+
const providers = await readProviders(db);
|
|
10169
|
+
const next = upsertProviderRecord(providers, name, {
|
|
10170
|
+
url: opts.url,
|
|
10171
|
+
model: opts.model,
|
|
10172
|
+
apiFlavor,
|
|
10173
|
+
apiKeyRef
|
|
10174
|
+
});
|
|
10175
|
+
await writeProviders(db, next);
|
|
10176
|
+
if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
|
|
10177
|
+
const rec = next[name];
|
|
10178
|
+
console.log(`Provider "${name}" saved:`);
|
|
10179
|
+
console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
|
|
10180
|
+
console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
|
|
10181
|
+
console.log(
|
|
10182
|
+
` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
|
|
10183
|
+
);
|
|
10184
|
+
console.log(` key-ref: ${rec.apiKeyRef ?? "(none \u2014 uses default key)"}`);
|
|
10185
|
+
if (opts.key) {
|
|
10186
|
+
console.log(` key: stored (${maskSecret(opts.key)})`);
|
|
10187
|
+
} else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
|
|
10188
|
+
console.log(
|
|
10189
|
+
`
|
|
10190
|
+
\u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
|
|
10191
|
+
);
|
|
10192
|
+
}
|
|
10193
|
+
if (!rec.url) {
|
|
10194
|
+
console.log(
|
|
10195
|
+
"\n \u26A0 No --url set; this provider inherits the base llm.url."
|
|
10196
|
+
);
|
|
10197
|
+
}
|
|
10198
|
+
console.log(
|
|
10199
|
+
`
|
|
10200
|
+
Bind it to a role: zam provider use recall --primary ${name}`
|
|
10201
|
+
);
|
|
10202
|
+
});
|
|
10203
|
+
});
|
|
10204
|
+
providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").action(async (name) => {
|
|
10205
|
+
await withDb(async (db) => {
|
|
10206
|
+
const providers = await readProviders(db);
|
|
10207
|
+
const { providers: next, removed } = removeProviderRecord(
|
|
10208
|
+
providers,
|
|
10209
|
+
name
|
|
10210
|
+
);
|
|
10211
|
+
if (!removed) {
|
|
10212
|
+
console.log(`No such provider: ${name}`);
|
|
10213
|
+
return;
|
|
10214
|
+
}
|
|
10215
|
+
await writeProviders(db, next);
|
|
10216
|
+
console.log(`Removed provider "${name}".`);
|
|
10217
|
+
const referencing = rolesReferencing(await readRoles(db), name);
|
|
10218
|
+
if (referencing.length > 0) {
|
|
10219
|
+
console.log(
|
|
10220
|
+
` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
|
|
10221
|
+
);
|
|
10222
|
+
}
|
|
10223
|
+
});
|
|
10224
|
+
});
|
|
10225
|
+
providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").action(async (role, opts) => {
|
|
10226
|
+
if (!VALID_ROLES.includes(role)) {
|
|
10227
|
+
console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
|
|
10228
|
+
process.exit(1);
|
|
10229
|
+
}
|
|
10230
|
+
if (!opts.primary) {
|
|
10231
|
+
console.error("--primary is required.");
|
|
10232
|
+
process.exit(1);
|
|
10233
|
+
}
|
|
10234
|
+
await withDb(async (db) => {
|
|
10235
|
+
const providers = await readProviders(db);
|
|
10236
|
+
await writeRoles(
|
|
10237
|
+
db,
|
|
10238
|
+
bindRoleProviders(
|
|
10239
|
+
await readRoles(db),
|
|
10240
|
+
role,
|
|
10241
|
+
opts.primary,
|
|
10242
|
+
opts.fallback
|
|
10243
|
+
)
|
|
10244
|
+
);
|
|
10245
|
+
const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
|
|
10246
|
+
console.log(`Role "${role}" \u2192 primary: ${opts.primary}${fb}`);
|
|
10247
|
+
for (const [label, ref] of [
|
|
10248
|
+
["primary", opts.primary],
|
|
10249
|
+
["fallback", opts.fallback]
|
|
10250
|
+
]) {
|
|
10251
|
+
if (ref && !(ref in providers)) {
|
|
10252
|
+
console.log(
|
|
10253
|
+
` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
|
|
10254
|
+
);
|
|
10255
|
+
}
|
|
10256
|
+
}
|
|
10257
|
+
});
|
|
10258
|
+
});
|
|
10259
|
+
providerCommand.command("set-key <ref>").description(
|
|
10260
|
+
"Store an API key for a provider reference (in credentials.json)"
|
|
10261
|
+
).option(
|
|
10262
|
+
"--key <value>",
|
|
10263
|
+
"The API key (omit to enter it interactively, hidden)"
|
|
10264
|
+
).action(async (ref, opts) => {
|
|
10265
|
+
try {
|
|
10266
|
+
const key = opts.key ?? await password2({ message: `API key for "${ref}":` });
|
|
10267
|
+
if (!key || key.trim().length === 0) {
|
|
10268
|
+
console.error("No key provided.");
|
|
10269
|
+
process.exit(1);
|
|
10270
|
+
}
|
|
10271
|
+
setProviderApiKey(ref, key.trim());
|
|
10272
|
+
console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
|
|
10273
|
+
} catch (err) {
|
|
10274
|
+
if (err.name === "ExitPromptError") {
|
|
10275
|
+
console.log("\nCancelled.");
|
|
10276
|
+
process.exit(0);
|
|
10277
|
+
}
|
|
10278
|
+
console.error("Error:", err.message);
|
|
10279
|
+
process.exit(1);
|
|
10280
|
+
}
|
|
10281
|
+
});
|
|
10282
|
+
providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
|
|
10283
|
+
clearProviderApiKey(ref);
|
|
10284
|
+
console.log(`Cleared API key for "${ref}".`);
|
|
10285
|
+
});
|
|
10286
|
+
|
|
9229
10287
|
// src/cli/commands/review.ts
|
|
9230
|
-
import { Command as
|
|
9231
|
-
var reviewCommand = new
|
|
10288
|
+
import { Command as Command14 } from "commander";
|
|
10289
|
+
var reviewCommand = new Command14("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) => {
|
|
9232
10290
|
let db;
|
|
9233
10291
|
try {
|
|
9234
10292
|
db = await openDatabase();
|
|
@@ -9342,9 +10400,9 @@ ${"\u2550".repeat(50)}`);
|
|
|
9342
10400
|
|
|
9343
10401
|
// src/cli/commands/session.ts
|
|
9344
10402
|
import { readFileSync as readFileSync11 } from "fs";
|
|
9345
|
-
import { input as
|
|
9346
|
-
import { Command as
|
|
9347
|
-
var sessionCommand = new
|
|
10403
|
+
import { input as input7, select as select2 } from "@inquirer/prompts";
|
|
10404
|
+
import { Command as Command15 } from "commander";
|
|
10405
|
+
var sessionCommand = new Command15("session").description(
|
|
9348
10406
|
"Manage learning sessions"
|
|
9349
10407
|
);
|
|
9350
10408
|
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(
|
|
@@ -9521,7 +10579,7 @@ async function selectTask() {
|
|
|
9521
10579
|
console.log("No active work items found in Azure DevOps.");
|
|
9522
10580
|
}
|
|
9523
10581
|
}
|
|
9524
|
-
return
|
|
10582
|
+
return input7({ message: "Task description:" });
|
|
9525
10583
|
}
|
|
9526
10584
|
var RATING_LABELS = {
|
|
9527
10585
|
1: "Again",
|
|
@@ -9723,9 +10781,9 @@ sessionCommand.command("end").description("End a session and show summary").requ
|
|
|
9723
10781
|
});
|
|
9724
10782
|
|
|
9725
10783
|
// src/cli/commands/settings.ts
|
|
9726
|
-
import { existsSync as
|
|
9727
|
-
import { Command as
|
|
9728
|
-
var settingsCommand = new
|
|
10784
|
+
import { existsSync as existsSync19 } from "fs";
|
|
10785
|
+
import { Command as Command16 } from "commander";
|
|
10786
|
+
var settingsCommand = new Command16("settings").description(
|
|
9729
10787
|
"Manage user settings"
|
|
9730
10788
|
);
|
|
9731
10789
|
var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
|
|
@@ -9888,7 +10946,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
|
|
|
9888
10946
|
console.log("\nValidation:");
|
|
9889
10947
|
for (const [name, path] of Object.entries(paths)) {
|
|
9890
10948
|
if (path) {
|
|
9891
|
-
const exists =
|
|
10949
|
+
const exists = existsSync19(path);
|
|
9892
10950
|
console.log(
|
|
9893
10951
|
` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
|
|
9894
10952
|
);
|
|
@@ -9900,41 +10958,41 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
|
|
|
9900
10958
|
});
|
|
9901
10959
|
|
|
9902
10960
|
// src/cli/commands/setup.ts
|
|
9903
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
9904
|
-
import { basename as basename4, dirname as dirname6, join as
|
|
10961
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
|
|
10962
|
+
import { basename as basename4, dirname as dirname6, join as join20 } from "path";
|
|
9905
10963
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9906
|
-
import { Command as
|
|
10964
|
+
import { Command as Command17 } from "commander";
|
|
9907
10965
|
var packageRoot = [
|
|
9908
10966
|
fileURLToPath2(new URL("../..", import.meta.url)),
|
|
9909
10967
|
fileURLToPath2(new URL("../../..", import.meta.url))
|
|
9910
|
-
].find((candidate) =>
|
|
10968
|
+
].find((candidate) => existsSync20(join20(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
|
|
9911
10969
|
var SKILL_PAIRS = [
|
|
9912
10970
|
{
|
|
9913
|
-
from:
|
|
9914
|
-
to:
|
|
10971
|
+
from: join20(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
|
|
10972
|
+
to: join20(".claude", "skills", "zam", "SKILL.md")
|
|
9915
10973
|
},
|
|
9916
10974
|
{
|
|
9917
|
-
from:
|
|
9918
|
-
to:
|
|
10975
|
+
from: join20(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
|
|
10976
|
+
to: join20(".agent", "skills", "zam", "SKILL.md")
|
|
9919
10977
|
},
|
|
9920
10978
|
{
|
|
9921
|
-
from:
|
|
9922
|
-
to:
|
|
10979
|
+
from: join20(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
|
|
10980
|
+
to: join20(".agents", "skills", "zam", "SKILL.md")
|
|
9923
10981
|
}
|
|
9924
10982
|
];
|
|
9925
10983
|
function copySkills(force, cwd = process.cwd()) {
|
|
9926
10984
|
let anyAction = false;
|
|
9927
10985
|
for (const { from, to } of SKILL_PAIRS) {
|
|
9928
|
-
const dest =
|
|
9929
|
-
if (!
|
|
10986
|
+
const dest = join20(cwd, to);
|
|
10987
|
+
if (!existsSync20(from)) {
|
|
9930
10988
|
console.warn(` warn source not found, skipping: ${from}`);
|
|
9931
10989
|
continue;
|
|
9932
10990
|
}
|
|
9933
|
-
if (
|
|
10991
|
+
if (existsSync20(dest) && !force) {
|
|
9934
10992
|
console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
|
|
9935
10993
|
continue;
|
|
9936
10994
|
}
|
|
9937
|
-
|
|
10995
|
+
mkdirSync11(dirname6(dest), { recursive: true });
|
|
9938
10996
|
copyFileSync2(from, dest);
|
|
9939
10997
|
console.log(` copy ${to}`);
|
|
9940
10998
|
anyAction = true;
|
|
@@ -9975,13 +11033,13 @@ async function initDatabase(skipInit) {
|
|
|
9975
11033
|
}
|
|
9976
11034
|
function writeClaudeMd(skipClaudeMd, cwd = process.cwd()) {
|
|
9977
11035
|
if (skipClaudeMd) return;
|
|
9978
|
-
const dest =
|
|
9979
|
-
if (
|
|
11036
|
+
const dest = join20(cwd, "CLAUDE.md");
|
|
11037
|
+
if (existsSync20(dest)) {
|
|
9980
11038
|
console.log(` skip CLAUDE.md (already present)`);
|
|
9981
11039
|
return;
|
|
9982
11040
|
}
|
|
9983
11041
|
const name = basename4(cwd);
|
|
9984
|
-
|
|
11042
|
+
writeFileSync10(
|
|
9985
11043
|
dest,
|
|
9986
11044
|
`# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
|
|
9987
11045
|
|
|
@@ -10009,13 +11067,13 @@ Use \`zam connector setup turso\` to store cloud credentials in
|
|
|
10009
11067
|
}
|
|
10010
11068
|
function writeAgentsMd(skipAgentsMd, cwd = process.cwd()) {
|
|
10011
11069
|
if (skipAgentsMd) return;
|
|
10012
|
-
const dest =
|
|
10013
|
-
if (
|
|
11070
|
+
const dest = join20(cwd, "AGENTS.md");
|
|
11071
|
+
if (existsSync20(dest)) {
|
|
10014
11072
|
console.log(` skip AGENTS.md (already present)`);
|
|
10015
11073
|
return;
|
|
10016
11074
|
}
|
|
10017
11075
|
const name = basename4(cwd);
|
|
10018
|
-
|
|
11076
|
+
writeFileSync10(
|
|
10019
11077
|
dest,
|
|
10020
11078
|
`# ZAM Personal Kernel - ${name}
|
|
10021
11079
|
|
|
@@ -10048,7 +11106,7 @@ Codex discovers repository skills under \`.agents/skills/\`. Run
|
|
|
10048
11106
|
);
|
|
10049
11107
|
console.log(` write AGENTS.md`);
|
|
10050
11108
|
}
|
|
10051
|
-
var setupCommand = new
|
|
11109
|
+
var setupCommand = new Command17("setup").description(
|
|
10052
11110
|
"Distribute ZAM skill files into this personal instance and initialize the database"
|
|
10053
11111
|
).option(
|
|
10054
11112
|
"--force",
|
|
@@ -10069,8 +11127,8 @@ var setupCommand = new Command15("setup").description(
|
|
|
10069
11127
|
);
|
|
10070
11128
|
|
|
10071
11129
|
// src/cli/commands/skill.ts
|
|
10072
|
-
import { Command as
|
|
10073
|
-
var skillCommand = new
|
|
11130
|
+
import { Command as Command18 } from "commander";
|
|
11131
|
+
var skillCommand = new Command18("skill").description(
|
|
10074
11132
|
"Manage agent skill entries (task recipes)"
|
|
10075
11133
|
);
|
|
10076
11134
|
skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
|
|
@@ -10155,10 +11213,10 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
|
|
|
10155
11213
|
});
|
|
10156
11214
|
|
|
10157
11215
|
// src/cli/commands/snapshot.ts
|
|
10158
|
-
import { existsSync as
|
|
10159
|
-
import { homedir as
|
|
10160
|
-
import { dirname as dirname7, join as
|
|
10161
|
-
import { Command as
|
|
11216
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
|
|
11217
|
+
import { homedir as homedir13 } from "os";
|
|
11218
|
+
import { dirname as dirname7, join as join21 } from "path";
|
|
11219
|
+
import { Command as Command19 } from "commander";
|
|
10162
11220
|
function defaultOutName() {
|
|
10163
11221
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
|
|
10164
11222
|
return `zam-snapshot-${stamp}.sql`;
|
|
@@ -10172,12 +11230,12 @@ function summarize(tables) {
|
|
|
10172
11230
|
}
|
|
10173
11231
|
return { total, nonEmpty };
|
|
10174
11232
|
}
|
|
10175
|
-
var exportCmd = new
|
|
11233
|
+
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) => {
|
|
10176
11234
|
let db;
|
|
10177
11235
|
try {
|
|
10178
11236
|
db = await openDatabaseWithSync({ initialize: true });
|
|
10179
11237
|
const snapshot = await exportSnapshot(db);
|
|
10180
|
-
const personalDir = await getSetting(db, "personal.workspace_dir") ||
|
|
11238
|
+
const personalDir = await getSetting(db, "personal.workspace_dir") || join21(homedir13(), "Documents", "zam");
|
|
10181
11239
|
await db.close();
|
|
10182
11240
|
db = void 0;
|
|
10183
11241
|
const manifest = verifySnapshot(snapshot);
|
|
@@ -10186,12 +11244,12 @@ var exportCmd = new Command17("export").description("Write a portable SQL-text s
|
|
|
10186
11244
|
process.stdout.write(snapshot);
|
|
10187
11245
|
return;
|
|
10188
11246
|
}
|
|
10189
|
-
const out = opts.out ??
|
|
11247
|
+
const out = opts.out ?? join21(personalDir, "snapshots", defaultOutName());
|
|
10190
11248
|
const dir = dirname7(out);
|
|
10191
|
-
if (dir && dir !== "." && !
|
|
10192
|
-
|
|
11249
|
+
if (dir && dir !== "." && !existsSync21(dir)) {
|
|
11250
|
+
mkdirSync12(dir, { recursive: true });
|
|
10193
11251
|
}
|
|
10194
|
-
|
|
11252
|
+
writeFileSync11(out, snapshot, "utf-8");
|
|
10195
11253
|
console.log(`Snapshot written: ${out}`);
|
|
10196
11254
|
console.log(
|
|
10197
11255
|
` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
|
|
@@ -10202,10 +11260,10 @@ var exportCmd = new Command17("export").description("Write a portable SQL-text s
|
|
|
10202
11260
|
process.exit(1);
|
|
10203
11261
|
}
|
|
10204
11262
|
});
|
|
10205
|
-
var importCmd = new
|
|
11263
|
+
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) => {
|
|
10206
11264
|
let db;
|
|
10207
11265
|
try {
|
|
10208
|
-
if (!
|
|
11266
|
+
if (!existsSync21(file)) {
|
|
10209
11267
|
console.error(`Error: Snapshot file not found: ${file}`);
|
|
10210
11268
|
process.exit(1);
|
|
10211
11269
|
}
|
|
@@ -10225,9 +11283,9 @@ var importCmd = new Command17("import").description("Restore a snapshot into the
|
|
|
10225
11283
|
process.exit(1);
|
|
10226
11284
|
}
|
|
10227
11285
|
});
|
|
10228
|
-
var verifyCmd = new
|
|
11286
|
+
var verifyCmd = new Command19("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
|
|
10229
11287
|
try {
|
|
10230
|
-
if (!
|
|
11288
|
+
if (!existsSync21(file)) {
|
|
10231
11289
|
console.error(`Error: Snapshot file not found: ${file}`);
|
|
10232
11290
|
process.exit(1);
|
|
10233
11291
|
}
|
|
@@ -10243,11 +11301,11 @@ var verifyCmd = new Command17("verify").description("Check a snapshot's manifest
|
|
|
10243
11301
|
process.exit(1);
|
|
10244
11302
|
}
|
|
10245
11303
|
});
|
|
10246
|
-
var snapshotCommand = new
|
|
11304
|
+
var snapshotCommand = new Command19("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
|
|
10247
11305
|
|
|
10248
11306
|
// src/cli/commands/stats.ts
|
|
10249
|
-
import { Command as
|
|
10250
|
-
var statsCommand = new
|
|
11307
|
+
import { Command as Command20 } from "commander";
|
|
11308
|
+
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) => {
|
|
10251
11309
|
await withDb(async (db) => {
|
|
10252
11310
|
const userId = await resolveUser(opts, db);
|
|
10253
11311
|
const stats = await getUserStats(db, userId);
|
|
@@ -10283,8 +11341,8 @@ var statsCommand = new Command18("stats").description("Show learning dashboard f
|
|
|
10283
11341
|
});
|
|
10284
11342
|
|
|
10285
11343
|
// src/cli/commands/token.ts
|
|
10286
|
-
import { Command as
|
|
10287
|
-
var tokenCommand = new
|
|
11344
|
+
import { Command as Command21 } from "commander";
|
|
11345
|
+
var tokenCommand = new Command21("token").description(
|
|
10288
11346
|
"Manage knowledge tokens"
|
|
10289
11347
|
);
|
|
10290
11348
|
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) => {
|
|
@@ -10570,12 +11628,12 @@ tokenCommand.command("status").description("Show full status of a token for a us
|
|
|
10570
11628
|
});
|
|
10571
11629
|
|
|
10572
11630
|
// src/cli/commands/ui.ts
|
|
10573
|
-
import { spawn as
|
|
10574
|
-
import { existsSync as
|
|
10575
|
-
import { homedir as
|
|
10576
|
-
import { dirname as dirname8, join as
|
|
11631
|
+
import { spawn as spawn3, spawnSync } from "child_process";
|
|
11632
|
+
import { existsSync as existsSync22 } from "fs";
|
|
11633
|
+
import { homedir as homedir14 } from "os";
|
|
11634
|
+
import { dirname as dirname8, join as join22 } from "path";
|
|
10577
11635
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10578
|
-
import { Command as
|
|
11636
|
+
import { Command as Command22 } from "commander";
|
|
10579
11637
|
var C3 = {
|
|
10580
11638
|
reset: "\x1B[0m",
|
|
10581
11639
|
red: "\x1B[31m",
|
|
@@ -10589,8 +11647,8 @@ function findDesktopDir() {
|
|
|
10589
11647
|
for (const start of starts) {
|
|
10590
11648
|
let dir = start;
|
|
10591
11649
|
for (let i = 0; i < 10; i++) {
|
|
10592
|
-
if (
|
|
10593
|
-
return
|
|
11650
|
+
if (existsSync22(join22(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
|
|
11651
|
+
return join22(dir, "desktop");
|
|
10594
11652
|
}
|
|
10595
11653
|
const parent = dirname8(dir);
|
|
10596
11654
|
if (parent === dir) break;
|
|
@@ -10600,30 +11658,30 @@ function findDesktopDir() {
|
|
|
10600
11658
|
return null;
|
|
10601
11659
|
}
|
|
10602
11660
|
function findBuiltApp(desktopDir) {
|
|
10603
|
-
const releaseDir =
|
|
11661
|
+
const releaseDir = join22(desktopDir, "src-tauri", "target", "release");
|
|
10604
11662
|
if (process.platform === "win32") {
|
|
10605
11663
|
for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
|
|
10606
|
-
const p =
|
|
10607
|
-
if (
|
|
11664
|
+
const p = join22(releaseDir, name);
|
|
11665
|
+
if (existsSync22(p)) return p;
|
|
10608
11666
|
}
|
|
10609
11667
|
} else if (process.platform === "darwin") {
|
|
10610
|
-
const app =
|
|
10611
|
-
if (
|
|
11668
|
+
const app = join22(releaseDir, "bundle", "macos", "ZAM.app");
|
|
11669
|
+
if (existsSync22(app)) return app;
|
|
10612
11670
|
} else {
|
|
10613
11671
|
for (const name of ["zam", "ZAM", "zam-desktop"]) {
|
|
10614
|
-
const p =
|
|
10615
|
-
if (
|
|
11672
|
+
const p = join22(releaseDir, name);
|
|
11673
|
+
if (existsSync22(p)) return p;
|
|
10616
11674
|
}
|
|
10617
11675
|
}
|
|
10618
11676
|
return null;
|
|
10619
11677
|
}
|
|
10620
11678
|
function findInstalledApp() {
|
|
10621
11679
|
const candidates = process.platform === "win32" ? [
|
|
10622
|
-
process.env.LOCALAPPDATA &&
|
|
10623
|
-
process.env.ProgramFiles &&
|
|
10624
|
-
process.env["ProgramFiles(x86)"] &&
|
|
10625
|
-
] : process.platform === "darwin" ? ["/Applications/ZAM.app",
|
|
10626
|
-
return candidates.find((candidate) => candidate &&
|
|
11680
|
+
process.env.LOCALAPPDATA && join22(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
|
|
11681
|
+
process.env.ProgramFiles && join22(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
|
|
11682
|
+
process.env["ProgramFiles(x86)"] && join22(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
|
|
11683
|
+
] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(homedir14(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
|
|
11684
|
+
return candidates.find((candidate) => candidate && existsSync22(candidate)) || null;
|
|
10627
11685
|
}
|
|
10628
11686
|
function runNpm(args, opts) {
|
|
10629
11687
|
const res = spawnSync("npm", args, {
|
|
@@ -10634,7 +11692,7 @@ function runNpm(args, opts) {
|
|
|
10634
11692
|
return res.status ?? 1;
|
|
10635
11693
|
}
|
|
10636
11694
|
function ensureDesktopDeps(desktopDir) {
|
|
10637
|
-
if (
|
|
11695
|
+
if (existsSync22(join22(desktopDir, "node_modules"))) return true;
|
|
10638
11696
|
console.log(
|
|
10639
11697
|
`${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
|
|
10640
11698
|
);
|
|
@@ -10660,13 +11718,13 @@ function requireRust() {
|
|
|
10660
11718
|
function hasMsvcBuildTools() {
|
|
10661
11719
|
if (process.platform !== "win32") return true;
|
|
10662
11720
|
const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
|
10663
|
-
const vswhere =
|
|
11721
|
+
const vswhere = join22(
|
|
10664
11722
|
pf86,
|
|
10665
11723
|
"Microsoft Visual Studio",
|
|
10666
11724
|
"Installer",
|
|
10667
11725
|
"vswhere.exe"
|
|
10668
11726
|
);
|
|
10669
|
-
if (!
|
|
11727
|
+
if (!existsSync22(vswhere)) return false;
|
|
10670
11728
|
const res = spawnSync(
|
|
10671
11729
|
vswhere,
|
|
10672
11730
|
[
|
|
@@ -10698,7 +11756,7 @@ function requireMsvcOnWindows() {
|
|
|
10698
11756
|
return false;
|
|
10699
11757
|
}
|
|
10700
11758
|
function warnIfCliMissing(repoRoot) {
|
|
10701
|
-
if (!
|
|
11759
|
+
if (!existsSync22(join22(repoRoot, "dist", "cli", "index.js"))) {
|
|
10702
11760
|
console.warn(
|
|
10703
11761
|
`${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}`
|
|
10704
11762
|
);
|
|
@@ -10707,13 +11765,13 @@ function warnIfCliMissing(repoRoot) {
|
|
|
10707
11765
|
function launchApp(appPath, workingDir) {
|
|
10708
11766
|
console.log(`${C3.green}\u2713 Launching ZAM Desktop...${C3.reset}`);
|
|
10709
11767
|
if (process.platform === "darwin" && appPath.endsWith(".app")) {
|
|
10710
|
-
|
|
11768
|
+
spawn3("open", [appPath], {
|
|
10711
11769
|
cwd: workingDir,
|
|
10712
11770
|
detached: true,
|
|
10713
11771
|
stdio: "ignore"
|
|
10714
11772
|
}).unref();
|
|
10715
11773
|
} else {
|
|
10716
|
-
|
|
11774
|
+
spawn3(appPath, [], {
|
|
10717
11775
|
cwd: workingDir,
|
|
10718
11776
|
detached: true,
|
|
10719
11777
|
stdio: "ignore",
|
|
@@ -10755,13 +11813,13 @@ function createShortcuts(appPath, repoRoot) {
|
|
|
10755
11813
|
console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
|
|
10756
11814
|
}
|
|
10757
11815
|
}
|
|
10758
|
-
var uiCommand = new
|
|
11816
|
+
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(
|
|
10759
11817
|
"--build",
|
|
10760
11818
|
"Build the native installer for your OS (needs Rust, one-time)"
|
|
10761
11819
|
).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
|
|
10762
11820
|
const installedApp = findInstalledApp();
|
|
10763
11821
|
if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
|
|
10764
|
-
launchApp(installedApp,
|
|
11822
|
+
launchApp(installedApp, homedir14());
|
|
10765
11823
|
return;
|
|
10766
11824
|
}
|
|
10767
11825
|
const desktopDir = findDesktopDir();
|
|
@@ -10782,7 +11840,7 @@ var uiCommand = new Command20("ui").description("Launch the ZAM Desktop GUI (Act
|
|
|
10782
11840
|
);
|
|
10783
11841
|
const code = runNpm(["run", "tauri", "build"], { cwd: desktopDir });
|
|
10784
11842
|
if (code === 0) {
|
|
10785
|
-
const bundle =
|
|
11843
|
+
const bundle = join22(
|
|
10786
11844
|
desktopDir,
|
|
10787
11845
|
"src-tauri",
|
|
10788
11846
|
"target",
|
|
@@ -10859,11 +11917,11 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
|
|
|
10859
11917
|
|
|
10860
11918
|
// src/cli/commands/update.ts
|
|
10861
11919
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
10862
|
-
import { existsSync as
|
|
10863
|
-
import { dirname as dirname9, join as
|
|
11920
|
+
import { existsSync as existsSync23, readFileSync as readFileSync13, realpathSync } from "fs";
|
|
11921
|
+
import { dirname as dirname9, join as join23 } from "path";
|
|
10864
11922
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
10865
|
-
import { confirm as
|
|
10866
|
-
import { Command as
|
|
11923
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
11924
|
+
import { Command as Command23 } from "commander";
|
|
10867
11925
|
var GITHUB_REPO = "zam-os/zam";
|
|
10868
11926
|
var CHANNELS = [
|
|
10869
11927
|
"developer",
|
|
@@ -10885,7 +11943,7 @@ function currentVersion() {
|
|
|
10885
11943
|
for (const up of ["..", "../..", "../../.."]) {
|
|
10886
11944
|
try {
|
|
10887
11945
|
const pkg2 = JSON.parse(
|
|
10888
|
-
readFileSync13(
|
|
11946
|
+
readFileSync13(join23(here, up, "package.json"), "utf-8")
|
|
10889
11947
|
);
|
|
10890
11948
|
if (pkg2.version) return pkg2.version;
|
|
10891
11949
|
} catch {
|
|
@@ -10896,7 +11954,7 @@ function currentVersion() {
|
|
|
10896
11954
|
function versionAt(dir) {
|
|
10897
11955
|
try {
|
|
10898
11956
|
const pkg2 = JSON.parse(
|
|
10899
|
-
readFileSync13(
|
|
11957
|
+
readFileSync13(join23(dir, "package.json"), "utf-8")
|
|
10900
11958
|
);
|
|
10901
11959
|
return pkg2.version ?? "unknown";
|
|
10902
11960
|
} catch {
|
|
@@ -10941,7 +11999,7 @@ function render2(decision) {
|
|
|
10941
11999
|
);
|
|
10942
12000
|
}
|
|
10943
12001
|
}
|
|
10944
|
-
var checkCmd = new
|
|
12002
|
+
var checkCmd = new Command23("check").description("Check whether a newer ZAM has been released").option(
|
|
10945
12003
|
"--latest <version>",
|
|
10946
12004
|
"Compare against this version instead of fetching"
|
|
10947
12005
|
).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
|
|
@@ -10976,13 +12034,13 @@ function findSourceRepo() {
|
|
|
10976
12034
|
let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
|
|
10977
12035
|
let parent = dirname9(dir);
|
|
10978
12036
|
while (parent !== dir) {
|
|
10979
|
-
if (
|
|
12037
|
+
if (existsSync23(join23(dir, ".git"))) return dir;
|
|
10980
12038
|
dir = parent;
|
|
10981
12039
|
parent = dirname9(dir);
|
|
10982
12040
|
}
|
|
10983
|
-
return
|
|
12041
|
+
return existsSync23(join23(dir, ".git")) ? dir : null;
|
|
10984
12042
|
}
|
|
10985
|
-
function
|
|
12043
|
+
function runGit2(cwd, args, capture) {
|
|
10986
12044
|
const res = spawnSync2("git", args, {
|
|
10987
12045
|
cwd,
|
|
10988
12046
|
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
@@ -11014,7 +12072,7 @@ function applyDeveloperUpdate(force) {
|
|
|
11014
12072
|
);
|
|
11015
12073
|
process.exit(1);
|
|
11016
12074
|
}
|
|
11017
|
-
const status =
|
|
12075
|
+
const status = runGit2(src, ["status", "--porcelain"], true);
|
|
11018
12076
|
if (status.ok && status.out && !force) {
|
|
11019
12077
|
console.error(
|
|
11020
12078
|
`${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
|
|
@@ -11026,7 +12084,7 @@ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
|
|
|
11026
12084
|
console.log(
|
|
11027
12085
|
`${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
|
|
11028
12086
|
);
|
|
11029
|
-
if (!
|
|
12087
|
+
if (!runGit2(src, ["pull", "--ff-only"], false).ok) {
|
|
11030
12088
|
console.error(
|
|
11031
12089
|
`${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
|
|
11032
12090
|
);
|
|
@@ -11045,7 +12103,7 @@ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
|
|
|
11045
12103
|
console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
|
|
11046
12104
|
const setup = spawnSync2(
|
|
11047
12105
|
process.execPath,
|
|
11048
|
-
[
|
|
12106
|
+
[join23(src, "dist", "cli", "index.js"), "setup", "--force"],
|
|
11049
12107
|
{ cwd: process.cwd(), stdio: "inherit" }
|
|
11050
12108
|
);
|
|
11051
12109
|
if (setup.status !== 0) {
|
|
@@ -11088,7 +12146,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
|
|
|
11088
12146
|
return;
|
|
11089
12147
|
}
|
|
11090
12148
|
if (!opts.yes) {
|
|
11091
|
-
const ok = await
|
|
12149
|
+
const ok = await confirm4({
|
|
11092
12150
|
message: "Apply this update?",
|
|
11093
12151
|
default: true
|
|
11094
12152
|
});
|
|
@@ -11108,7 +12166,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
|
|
|
11108
12166
|
process.exit(1);
|
|
11109
12167
|
}
|
|
11110
12168
|
}
|
|
11111
|
-
var updateCommand = new
|
|
12169
|
+
var updateCommand = new Command23("update").description(
|
|
11112
12170
|
"Update ZAM to the latest release (use `update check` to only check)"
|
|
11113
12171
|
).option("-y, --yes", "Apply without confirmation").option(
|
|
11114
12172
|
"--force",
|
|
@@ -11118,8 +12176,8 @@ var updateCommand = new Command21("update").description(
|
|
|
11118
12176
|
}).addCommand(checkCmd);
|
|
11119
12177
|
|
|
11120
12178
|
// src/cli/commands/whoami.ts
|
|
11121
|
-
import { Command as
|
|
11122
|
-
var whoamiCommand = new
|
|
12179
|
+
import { Command as Command24 } from "commander";
|
|
12180
|
+
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) => {
|
|
11123
12181
|
await withDb(async (db) => {
|
|
11124
12182
|
if (opts.set) {
|
|
11125
12183
|
await setSetting(db, "user.id", opts.set);
|
|
@@ -11154,163 +12212,12 @@ var whoamiCommand = new Command22("whoami").description("Show or set the default
|
|
|
11154
12212
|
});
|
|
11155
12213
|
});
|
|
11156
12214
|
|
|
11157
|
-
// src/cli/commands/workspace.ts
|
|
11158
|
-
import { execSync as execSync6 } from "child_process";
|
|
11159
|
-
import { existsSync as existsSync21, writeFileSync as writeFileSync11 } from "fs";
|
|
11160
|
-
import { join as join22 } from "path";
|
|
11161
|
-
import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
|
|
11162
|
-
import { Command as Command23 } from "commander";
|
|
11163
|
-
function runGit2(cwd, args) {
|
|
11164
|
-
try {
|
|
11165
|
-
return execSync6(`git ${args}`, {
|
|
11166
|
-
cwd,
|
|
11167
|
-
stdio: "pipe",
|
|
11168
|
-
encoding: "utf8"
|
|
11169
|
-
}).trim();
|
|
11170
|
-
} catch (err) {
|
|
11171
|
-
throw new Error(`Git command failed: ${err.message}`);
|
|
11172
|
-
}
|
|
11173
|
-
}
|
|
11174
|
-
var workspaceCommand = new Command23("workspace").description(
|
|
11175
|
-
"Manage your ZAM learning workspace"
|
|
11176
|
-
);
|
|
11177
|
-
workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
|
|
11178
|
-
let db;
|
|
11179
|
-
let workspaceDir = "";
|
|
11180
|
-
try {
|
|
11181
|
-
db = await openDatabase();
|
|
11182
|
-
workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
|
|
11183
|
-
await db.close();
|
|
11184
|
-
} catch {
|
|
11185
|
-
await db?.close();
|
|
11186
|
-
}
|
|
11187
|
-
if (!workspaceDir) {
|
|
11188
|
-
console.error(
|
|
11189
|
-
"\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
|
|
11190
|
-
);
|
|
11191
|
-
process.exit(1);
|
|
11192
|
-
}
|
|
11193
|
-
if (!existsSync21(workspaceDir)) {
|
|
11194
|
-
console.error(
|
|
11195
|
-
`\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
|
|
11196
|
-
);
|
|
11197
|
-
process.exit(1);
|
|
11198
|
-
}
|
|
11199
|
-
console.log(`
|
|
11200
|
-
Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
|
|
11201
|
-
if (!hasCommand("git")) {
|
|
11202
|
-
console.error(
|
|
11203
|
-
"\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
|
|
11204
|
-
);
|
|
11205
|
-
process.exit(1);
|
|
11206
|
-
}
|
|
11207
|
-
const gitignorePath = join22(workspaceDir, ".gitignore");
|
|
11208
|
-
if (!existsSync21(gitignorePath)) {
|
|
11209
|
-
writeFileSync11(
|
|
11210
|
-
gitignorePath,
|
|
11211
|
-
"node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
|
|
11212
|
-
"utf8"
|
|
11213
|
-
);
|
|
11214
|
-
}
|
|
11215
|
-
const hasGitRepo = existsSync21(join22(workspaceDir, ".git"));
|
|
11216
|
-
if (!hasGitRepo) {
|
|
11217
|
-
console.log("Initializing local Git repository...");
|
|
11218
|
-
runGit2(workspaceDir, "init -b main");
|
|
11219
|
-
runGit2(workspaceDir, "add .");
|
|
11220
|
-
runGit2(
|
|
11221
|
-
workspaceDir,
|
|
11222
|
-
'commit -m "chore: initial workspace sandbox bootstrap"'
|
|
11223
|
-
);
|
|
11224
|
-
console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
|
|
11225
|
-
} else {
|
|
11226
|
-
console.log("Git repository is already initialized.");
|
|
11227
|
-
}
|
|
11228
|
-
const repoName = await input7({
|
|
11229
|
-
message: "Choose a name for your GitHub repository:",
|
|
11230
|
-
default: "zam-personal"
|
|
11231
|
-
});
|
|
11232
|
-
const isPrivate = await confirm4({
|
|
11233
|
-
message: "Should the repository be private?",
|
|
11234
|
-
default: true
|
|
11235
|
-
});
|
|
11236
|
-
const repoVisibility = isPrivate ? "--private" : "--public";
|
|
11237
|
-
if (hasCommand("gh")) {
|
|
11238
|
-
console.log("GitHub CLI detected! Automating repository creation...");
|
|
11239
|
-
const proceedGh = await confirm4({
|
|
11240
|
-
message: "Would you like ZAM to create the repository using the GitHub CLI?",
|
|
11241
|
-
default: true
|
|
11242
|
-
});
|
|
11243
|
-
if (proceedGh) {
|
|
11244
|
-
try {
|
|
11245
|
-
console.log(`Creating GitHub repository ${repoName}...`);
|
|
11246
|
-
execSync6(
|
|
11247
|
-
`gh repo create ${repoName} ${repoVisibility} --source=. --push`,
|
|
11248
|
-
{
|
|
11249
|
-
cwd: workspaceDir,
|
|
11250
|
-
stdio: "inherit"
|
|
11251
|
-
}
|
|
11252
|
-
);
|
|
11253
|
-
console.log(
|
|
11254
|
-
"\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
|
|
11255
|
-
);
|
|
11256
|
-
process.exit(0);
|
|
11257
|
-
} catch (err) {
|
|
11258
|
-
console.warn(
|
|
11259
|
-
`\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
|
|
11260
|
-
);
|
|
11261
|
-
}
|
|
11262
|
-
}
|
|
11263
|
-
}
|
|
11264
|
-
console.log(
|
|
11265
|
-
"\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
|
|
11266
|
-
);
|
|
11267
|
-
console.log(" 1. Go to https://github.com/new");
|
|
11268
|
-
console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
|
|
11269
|
-
console.log(
|
|
11270
|
-
` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
|
|
11271
|
-
);
|
|
11272
|
-
console.log(
|
|
11273
|
-
" 4. Do NOT initialize it with README, .gitignore, or license"
|
|
11274
|
-
);
|
|
11275
|
-
console.log(" 5. Click 'Create repository'\n");
|
|
11276
|
-
const githubUrl = await input7({
|
|
11277
|
-
message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
|
|
11278
|
-
});
|
|
11279
|
-
if (githubUrl) {
|
|
11280
|
-
try {
|
|
11281
|
-
console.log("Linking remote repository and pushing...");
|
|
11282
|
-
let hasOrigin = false;
|
|
11283
|
-
try {
|
|
11284
|
-
runGit2(workspaceDir, "remote get-url origin");
|
|
11285
|
-
hasOrigin = true;
|
|
11286
|
-
} catch {
|
|
11287
|
-
}
|
|
11288
|
-
if (hasOrigin) {
|
|
11289
|
-
runGit2(workspaceDir, `remote set-url origin ${githubUrl}`);
|
|
11290
|
-
} else {
|
|
11291
|
-
runGit2(workspaceDir, `remote add origin ${githubUrl}`);
|
|
11292
|
-
}
|
|
11293
|
-
runGit2(workspaceDir, "push -u origin main");
|
|
11294
|
-
console.log(
|
|
11295
|
-
"\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
|
|
11296
|
-
);
|
|
11297
|
-
} catch (err) {
|
|
11298
|
-
console.error(
|
|
11299
|
-
`\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
|
|
11300
|
-
);
|
|
11301
|
-
console.log(
|
|
11302
|
-
"You can push manually later using: git push -u origin main"
|
|
11303
|
-
);
|
|
11304
|
-
}
|
|
11305
|
-
}
|
|
11306
|
-
});
|
|
11307
|
-
|
|
11308
12215
|
// src/cli/index.ts
|
|
11309
12216
|
var __dirname = dirname10(fileURLToPath5(import.meta.url));
|
|
11310
12217
|
var pkg = JSON.parse(
|
|
11311
|
-
readFileSync14(
|
|
12218
|
+
readFileSync14(join24(__dirname, "..", "..", "package.json"), "utf-8")
|
|
11312
12219
|
);
|
|
11313
|
-
var program = new
|
|
12220
|
+
var program = new Command25();
|
|
11314
12221
|
program.name("zam").description(
|
|
11315
12222
|
"The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
|
|
11316
12223
|
).version(pkg.version);
|
|
@@ -11330,6 +12237,7 @@ program.addCommand(observerCommand);
|
|
|
11330
12237
|
program.addCommand(settingsCommand);
|
|
11331
12238
|
program.addCommand(whoamiCommand);
|
|
11332
12239
|
program.addCommand(connectorCommand);
|
|
12240
|
+
program.addCommand(providerCommand);
|
|
11333
12241
|
program.addCommand(snapshotCommand);
|
|
11334
12242
|
program.addCommand(profileCommand);
|
|
11335
12243
|
program.addCommand(updateCommand);
|