balchemy 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -118
- package/dist/cli-options.d.ts +23 -0
- package/dist/cli-options.d.ts.map +1 -0
- package/dist/cli-options.js +87 -0
- package/dist/cli-options.js.map +1 -0
- package/dist/docker-gen.d.ts +20 -1
- package/dist/docker-gen.d.ts.map +1 -1
- package/dist/docker-gen.js +87 -25
- package/dist/docker-gen.js.map +1 -1
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +115 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +720 -270
- package/dist/index.js.map +1 -1
- package/dist/output.d.ts +35 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +80 -0
- package/dist/output.js.map +1 -0
- package/dist/tui/AgentBridge.d.ts +2 -2
- package/dist/tui/AgentBridge.d.ts.map +1 -1
- package/dist/tui/AgentBridge.js +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +47 -19
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/ChatAgent.d.ts +2 -1
- package/dist/tui/ChatAgent.d.ts.map +1 -1
- package/dist/tui/ChatAgent.js +15 -4
- package/dist/tui/ChatAgent.js.map +1 -1
- package/dist/tui/Wizard.d.ts.map +1 -1
- package/dist/tui/Wizard.js +58 -10
- package/dist/tui/Wizard.js.map +1 -1
- package/dist/tui/types.d.ts +9 -0
- package/dist/tui/types.d.ts.map +1 -1
- package/dist/wizard.js +2 -2
- package/dist/wizard.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -2,62 +2,27 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* balchemy CLI entry point.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - If no saved agent → run wizard
|
|
8
|
-
*
|
|
9
|
-
* Sub-commands:
|
|
10
|
-
* (no args) Resume cached agent or run wizard
|
|
11
|
-
* init / --init Force run wizard (ignore cache)
|
|
12
|
-
* start [config] Start from agent.config.yaml
|
|
13
|
-
* docker [outDir] Generate Docker files
|
|
14
|
-
* list List saved agents
|
|
15
|
-
*
|
|
16
|
-
* Flags:
|
|
17
|
-
* --help, -h Show help
|
|
18
|
-
* --version, -v Show version
|
|
19
|
-
* --no-color Disable colored output
|
|
5
|
+
* Public terminal surface for setup, local agent context, config validation,
|
|
6
|
+
* Docker file generation, and the Ink cockpit.
|
|
20
7
|
*/
|
|
21
|
-
import * as
|
|
22
|
-
import * as
|
|
23
|
-
import
|
|
24
|
-
import {
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import * as readline from "node:readline";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { clearAgent, getStorePath, listAgents, loadAgent, setActiveAgent, } from "./agent-store.js";
|
|
25
13
|
import { C, setNoColor } from "./colors.js";
|
|
26
|
-
|
|
14
|
+
import { commandKey, isNonInteractive, parseCliArgs, } from "./cli-options.js";
|
|
15
|
+
import { compactValue, createReporter, endpointHost, jsonEnvelope, } from "./output.js";
|
|
16
|
+
import { renderTerminalError, terminalErrorToJson, TerminalError, toTerminalError, } from "./errors.js";
|
|
27
17
|
const require = createRequire(import.meta.url);
|
|
28
18
|
const CLI_VERSION = require("../package.json").version ?? "unknown";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const positional = [];
|
|
33
|
-
for (const arg of rawArgs) {
|
|
34
|
-
if (arg === "--no-color") {
|
|
35
|
-
setNoColor();
|
|
36
|
-
flags.add(arg);
|
|
37
|
-
}
|
|
38
|
-
else if (arg === "--help" || arg === "-h")
|
|
39
|
-
flags.add("--help");
|
|
40
|
-
else if (arg === "--version" || arg === "-v")
|
|
41
|
-
flags.add("--version");
|
|
42
|
-
else if (arg === "--init")
|
|
43
|
-
positional.push("init");
|
|
44
|
-
else
|
|
45
|
-
positional.push(arg);
|
|
46
|
-
}
|
|
47
|
-
const cmd = positional[0];
|
|
48
|
-
const args = positional.slice(1);
|
|
49
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
50
|
-
function stderr(msg) {
|
|
51
|
-
process.stderr.write(msg);
|
|
52
|
-
}
|
|
53
|
-
function printSummaryBlock(title, rows) {
|
|
54
|
-
const maxLabel = rows.reduce((acc, row) => Math.max(acc, row.label.length), 0);
|
|
55
|
-
process.stdout.write(` ${C.T}${title}${C.R}\n`);
|
|
56
|
-
for (const row of rows) {
|
|
57
|
-
process.stdout.write(` ${C.D}${row.label.padEnd(maxLabel)}${C.R} ${row.value}\n`);
|
|
58
|
-
}
|
|
59
|
-
process.stdout.write(` ${C.D}${"-".repeat(54)}${C.R}\n`);
|
|
19
|
+
const parsed = parseCliArgs(process.argv.slice(2));
|
|
20
|
+
if (parsed.flags.noColor) {
|
|
21
|
+
setNoColor();
|
|
60
22
|
}
|
|
23
|
+
const reporter = createReporter(parsed.flags);
|
|
24
|
+
const cmd = commandKey(parsed.commandPath);
|
|
25
|
+
const args = parsed.args;
|
|
61
26
|
function ask(rl, question, defaultVal = "") {
|
|
62
27
|
return new Promise((resolve) => {
|
|
63
28
|
const hint = defaultVal ? ` ${C.D}[${defaultVal}]${C.R}` : "";
|
|
@@ -66,11 +31,6 @@ function ask(rl, question, defaultVal = "") {
|
|
|
66
31
|
});
|
|
67
32
|
});
|
|
68
33
|
}
|
|
69
|
-
function compactValue(value, head = 28, tail = 8) {
|
|
70
|
-
if (value.length <= head + tail + 3)
|
|
71
|
-
return value;
|
|
72
|
-
return `${value.slice(0, head)}...${value.slice(-tail)}`;
|
|
73
|
-
}
|
|
74
34
|
function normalizeChoice(value) {
|
|
75
35
|
const trimmed = value.trim().toLowerCase();
|
|
76
36
|
if (/^([0-9a-z])\1+$/.test(trimmed)) {
|
|
@@ -78,6 +38,14 @@ function normalizeChoice(value) {
|
|
|
78
38
|
}
|
|
79
39
|
return trimmed;
|
|
80
40
|
}
|
|
41
|
+
function printSummaryBlock(title, rows) {
|
|
42
|
+
const maxLabel = rows.reduce((acc, row) => Math.max(acc, row.label.length), 0);
|
|
43
|
+
reporter.write(` ${C.T}${title}${C.R}\n`);
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
reporter.write(` ${C.D}${row.label.padEnd(maxLabel)}${C.R} ${row.value}\n`);
|
|
46
|
+
}
|
|
47
|
+
reporter.write(` ${C.D}${"-".repeat(54)}${C.R}\n`);
|
|
48
|
+
}
|
|
81
49
|
function mostRecentAgent(agents) {
|
|
82
50
|
if (agents.length === 0)
|
|
83
51
|
return null;
|
|
@@ -87,8 +55,30 @@ function mostRecentAgent(agents) {
|
|
|
87
55
|
return (Number.isFinite(bTime) ? bTime : 0) - (Number.isFinite(aTime) ? aTime : 0);
|
|
88
56
|
})[0] ?? null;
|
|
89
57
|
}
|
|
90
|
-
|
|
58
|
+
function ensureInteractive(commandName) {
|
|
59
|
+
if (parsed.flags.json) {
|
|
60
|
+
throw new TerminalError({
|
|
61
|
+
code: "TERMINAL_UNSUPPORTED_TUI",
|
|
62
|
+
title: "Interactive terminal required",
|
|
63
|
+
cause: `${commandName} launches an interactive TUI and cannot run with --json.`,
|
|
64
|
+
fix: "Use a plain CLI command for automation, such as balchemy config validate --json or balchemy agent current --json.",
|
|
65
|
+
commandSuggestion: "balchemy doctor --json",
|
|
66
|
+
exitCode: 2,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (parsed.flags.ci || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
70
|
+
throw new TerminalError({
|
|
71
|
+
code: parsed.flags.ci ? "CI_PROMPT_BLOCKED" : "TERMINAL_UNSUPPORTED_TUI",
|
|
72
|
+
title: "Interactive terminal required",
|
|
73
|
+
cause: `${commandName} needs a TTY, but this session is non-interactive.`,
|
|
74
|
+
fix: "Run the command in a terminal, or use non-interactive validation commands in CI.",
|
|
75
|
+
commandSuggestion: "balchemy config validate --ci --json",
|
|
76
|
+
exitCode: 2,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
91
80
|
async function startSavedAgent(agent) {
|
|
81
|
+
ensureInteractive("balchemy start");
|
|
92
82
|
setActiveAgent(agent.publicId);
|
|
93
83
|
const { startTui } = await import("./tui/start.js");
|
|
94
84
|
await startTui({
|
|
@@ -108,7 +98,7 @@ async function startSavedAgent(agent) {
|
|
|
108
98
|
});
|
|
109
99
|
}
|
|
110
100
|
async function runWizardFromCwd() {
|
|
111
|
-
|
|
101
|
+
ensureInteractive("balchemy init");
|
|
112
102
|
const { startWizard } = await import("./tui/start-wizard.js");
|
|
113
103
|
await startWizard(process.cwd());
|
|
114
104
|
}
|
|
@@ -125,7 +115,6 @@ async function chooseSavedAgent(rl, agents) {
|
|
|
125
115
|
}
|
|
126
116
|
return agents.find((agent) => agent.publicId.toLowerCase() === answer) ?? null;
|
|
127
117
|
}
|
|
128
|
-
// ── Update check ─────────────────────────────────────────────────────────────
|
|
129
118
|
function parseSemver(value) {
|
|
130
119
|
const match = value.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
131
120
|
if (!match)
|
|
@@ -145,101 +134,124 @@ function isNewerVersion(latest, current) {
|
|
|
145
134
|
}
|
|
146
135
|
return false;
|
|
147
136
|
}
|
|
148
|
-
|
|
137
|
+
function shouldCheckForUpdate(flags) {
|
|
138
|
+
if (flags.json || flags.ci || flags.help || flags.version || isNonInteractive(flags))
|
|
139
|
+
return false;
|
|
140
|
+
return cmd === "" || cmd === "init" || cmd === "start";
|
|
141
|
+
}
|
|
142
|
+
async function checkForUpdate(flags) {
|
|
143
|
+
if (!shouldCheckForUpdate(flags))
|
|
144
|
+
return;
|
|
149
145
|
try {
|
|
150
146
|
const res = await fetch("https://registry.npmjs.org/balchemy/latest", {
|
|
151
147
|
signal: AbortSignal.timeout(3000),
|
|
152
148
|
});
|
|
153
149
|
if (!res.ok)
|
|
154
|
-
return
|
|
150
|
+
return;
|
|
155
151
|
const data = (await res.json());
|
|
156
152
|
const latest = data.version;
|
|
157
|
-
if (!latest)
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
162
|
-
const answer = await ask(rl, `${C.W}Update now?${C.R} (Y/n)`, "y");
|
|
163
|
-
rl.close();
|
|
164
|
-
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
165
|
-
// Detect if running via npx (no global binary to re-exec)
|
|
166
|
-
const isNpx = Boolean(process.env.npm_execpath?.includes("npx") ||
|
|
167
|
-
process.env._?.includes("npx") ||
|
|
168
|
-
process.env.npm_command === "exec");
|
|
169
|
-
stderr(` Updating to ${C.T}${latest}${C.R}...\n`);
|
|
170
|
-
const { execSync } = await import("child_process");
|
|
171
|
-
try {
|
|
172
|
-
execSync(`npm install -g balchemy@${latest}`, { stdio: "inherit" });
|
|
173
|
-
if (isNpx) {
|
|
174
|
-
stderr(`\n ${C.T}Updated!${C.R} Run ${C.W}balchemy${C.R} to use the new version.\n\n`);
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
stderr(`\n ${C.T}Updated!${C.R} Restarting...\n\n`);
|
|
178
|
-
const { execFileSync } = await import("child_process");
|
|
179
|
-
execFileSync("balchemy", process.argv.slice(2), { stdio: "inherit" });
|
|
180
|
-
process.exit(0);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
stderr(` ${C.D}Update failed. Continuing with ${CLI_VERSION}.${C.R}\n\n`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
stderr(` ${C.D}Skipped.${C.R}\n\n`);
|
|
189
|
-
}
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
return false;
|
|
153
|
+
if (!latest || !isNewerVersion(latest, CLI_VERSION))
|
|
154
|
+
return;
|
|
155
|
+
reporter.warn(`\n ${C.G}Update available${C.R} ${C.D}${CLI_VERSION}${C.R} → ${C.T}${latest}${C.R}\n`);
|
|
156
|
+
reporter.warn(` ${C.D}Run ${C.W}npm install -g balchemy@${latest}${C.D} yourself if you want to update.${C.R}\n\n`);
|
|
193
157
|
}
|
|
194
158
|
catch {
|
|
195
|
-
return
|
|
159
|
+
return;
|
|
196
160
|
}
|
|
197
161
|
}
|
|
198
|
-
// ── Help & version ───────────────────────────────────────────────────────────
|
|
199
162
|
function printHelp() {
|
|
200
|
-
|
|
163
|
+
reporter.write(`
|
|
201
164
|
${C.G}B${C.T}alchemy ${C.W}Agent CLI${C.R} ${C.D}v${CLI_VERSION}${C.R}
|
|
202
|
-
${C.D}
|
|
165
|
+
${C.D}Create, inspect, run, and safely supervise Balchemy agents from the terminal.${C.R}
|
|
203
166
|
|
|
204
167
|
${C.W}USAGE${C.R}
|
|
205
|
-
balchemy
|
|
206
|
-
balchemy init
|
|
207
|
-
balchemy start [config]
|
|
208
|
-
balchemy
|
|
209
|
-
balchemy
|
|
168
|
+
balchemy Resume saved agent or run setup wizard
|
|
169
|
+
balchemy init Run setup wizard
|
|
170
|
+
balchemy start [config] Start the live Ink cockpit
|
|
171
|
+
balchemy list List saved agents
|
|
172
|
+
balchemy docker [outDir] Generate Docker files
|
|
210
173
|
|
|
211
|
-
${C.W}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
174
|
+
${C.W}COMMANDS${C.R}
|
|
175
|
+
agent list List saved agents (alias: list)
|
|
176
|
+
agent current Show active agent context
|
|
177
|
+
agent use <publicId> Switch active saved agent
|
|
178
|
+
auth status Show local auth/context status
|
|
179
|
+
auth login Run interactive setup wizard (alias: init)
|
|
180
|
+
auth logout Clear active context; credential removal stays explicit
|
|
181
|
+
context current Show active agent context (alias: agent current)
|
|
182
|
+
config validate [config] Validate config and env references
|
|
183
|
+
config list [config] Show non-secret effective config
|
|
184
|
+
doctor [config] Run read-only local CLI checks
|
|
185
|
+
version Show CLI version
|
|
215
186
|
|
|
216
|
-
${C.W}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
187
|
+
${C.W}GLOBAL FLAGS${C.R}
|
|
188
|
+
-h, --help Show this help
|
|
189
|
+
-v, --version Show version
|
|
190
|
+
--json Emit valid JSON only; never enters TUI
|
|
191
|
+
-q, --quiet Suppress non-essential human output
|
|
192
|
+
--verbose Show expanded non-secret details
|
|
193
|
+
--debug Show redacted technical details
|
|
194
|
+
--ci Non-interactive mode; fail closed on prompts
|
|
195
|
+
--dry-run Preview file/action plans without writing
|
|
196
|
+
-y, --yes Approve safe non-overwrite prompts only
|
|
197
|
+
--force Allow explicit local file overwrites
|
|
198
|
+
--no-color Disable ANSI color
|
|
222
199
|
|
|
223
|
-
${C.W}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
200
|
+
${C.W}SAFETY${C.R}
|
|
201
|
+
--yes never approves live trades, wallet/key mutations, credential deletion, global installs, or file overwrites.
|
|
202
|
+
Use --dry-run before file-writing commands when reviewing changes.
|
|
203
|
+
|
|
204
|
+
${C.W}SHORTCUTS (cockpit)${C.R}
|
|
205
|
+
Ctrl+S Open settings
|
|
206
|
+
Ctrl+L Clear chat
|
|
207
|
+
Ctrl+N Switch agent
|
|
208
|
+
Ctrl+Q Quit
|
|
209
|
+
PgUp / PgDn Scroll chat history
|
|
210
|
+
Esc in TRADE CHECK Cancel; never approve
|
|
211
|
+
Esc Back, close overlay, or cancel
|
|
212
|
+
? Keyboard help overlay when available
|
|
227
213
|
|
|
228
214
|
`);
|
|
229
215
|
}
|
|
230
216
|
function printVersion() {
|
|
231
|
-
|
|
217
|
+
if (parsed.flags.json) {
|
|
218
|
+
reporter.json(jsonEnvelope({
|
|
219
|
+
ok: true,
|
|
220
|
+
command: "version",
|
|
221
|
+
version: CLI_VERSION,
|
|
222
|
+
data: { name: "balchemy", version: CLI_VERSION },
|
|
223
|
+
}));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
reporter.write(`balchemy ${CLI_VERSION}\n`);
|
|
232
227
|
}
|
|
233
|
-
|
|
234
|
-
|
|
228
|
+
const KNOWN_COMMANDS = [
|
|
229
|
+
"init",
|
|
230
|
+
"start",
|
|
231
|
+
"docker",
|
|
232
|
+
"list",
|
|
233
|
+
"agent list",
|
|
234
|
+
"agent current",
|
|
235
|
+
"agent use",
|
|
236
|
+
"auth status",
|
|
237
|
+
"config validate",
|
|
238
|
+
"config list",
|
|
239
|
+
"doctor",
|
|
240
|
+
"version",
|
|
241
|
+
"context current",
|
|
242
|
+
"context status",
|
|
243
|
+
"tui",
|
|
244
|
+
"auth login",
|
|
245
|
+
"auth logout",
|
|
246
|
+
];
|
|
235
247
|
function levenshtein(a, b) {
|
|
236
248
|
const matrix = [];
|
|
237
|
-
for (let i = 0; i <= a.length; i
|
|
249
|
+
for (let i = 0; i <= a.length; i += 1)
|
|
238
250
|
matrix[i] = [i];
|
|
239
|
-
for (let j = 0; j <= b.length; j
|
|
251
|
+
for (let j = 0; j <= b.length; j += 1)
|
|
240
252
|
matrix[0][j] = j;
|
|
241
|
-
for (let i = 1; i <= a.length; i
|
|
242
|
-
for (let j = 1; j <= b.length; j
|
|
253
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
254
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
243
255
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
244
256
|
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
245
257
|
}
|
|
@@ -249,183 +261,621 @@ function levenshtein(a, b) {
|
|
|
249
261
|
function suggestCommand(input) {
|
|
250
262
|
let best = null;
|
|
251
263
|
let bestDist = Infinity;
|
|
252
|
-
for (const
|
|
253
|
-
const dist = levenshtein(input.toLowerCase(),
|
|
254
|
-
if (dist < bestDist && dist <=
|
|
264
|
+
for (const knownCommand of KNOWN_COMMANDS) {
|
|
265
|
+
const dist = levenshtein(input.toLowerCase(), knownCommand);
|
|
266
|
+
if (dist < bestDist && dist <= 3) {
|
|
255
267
|
bestDist = dist;
|
|
256
|
-
best =
|
|
268
|
+
best = knownCommand;
|
|
257
269
|
}
|
|
258
270
|
}
|
|
259
271
|
return best;
|
|
260
272
|
}
|
|
261
|
-
|
|
262
|
-
|
|
273
|
+
function agentToJson(agent, activePublicId) {
|
|
274
|
+
return {
|
|
275
|
+
publicId: agent.publicId,
|
|
276
|
+
active: activePublicId === agent.publicId,
|
|
277
|
+
name: agent.name ?? null,
|
|
278
|
+
endpointHost: endpointHost(agent.mcpEndpoint),
|
|
279
|
+
mcpEndpoint: agent.mcpEndpoint,
|
|
280
|
+
llmProvider: agent.llmProvider,
|
|
281
|
+
llmModel: agent.llmModel ?? null,
|
|
282
|
+
mode: agent.shadowMode ? "shadow" : "live",
|
|
283
|
+
strategy: agent.strategy,
|
|
284
|
+
createdAt: agent.createdAt,
|
|
285
|
+
credentials: {
|
|
286
|
+
apiKey: "[redacted]",
|
|
287
|
+
llmApiKey: "[redacted]",
|
|
288
|
+
redacted: true,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function agentListData() {
|
|
293
|
+
const agents = listAgents();
|
|
294
|
+
const active = loadAgent();
|
|
295
|
+
return {
|
|
296
|
+
activePublicId: active?.publicId ?? null,
|
|
297
|
+
storePath: getStorePath(),
|
|
298
|
+
agents: agents.map((agent) => agentToJson(agent, active?.publicId ?? null)),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function printAgentList(commandName) {
|
|
263
302
|
const agents = listAgents();
|
|
264
303
|
const active = loadAgent();
|
|
304
|
+
if (parsed.flags.json) {
|
|
305
|
+
reporter.json(jsonEnvelope({
|
|
306
|
+
ok: true,
|
|
307
|
+
command: commandName,
|
|
308
|
+
version: CLI_VERSION,
|
|
309
|
+
data: agentListData(),
|
|
310
|
+
}));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
265
313
|
if (agents.length === 0) {
|
|
266
|
-
|
|
314
|
+
reporter.write(` ${C.D}No saved agents. Run ${C.W}balchemy init${C.D} to create one or ${C.W}balchemy auth status${C.D} to inspect context.${C.R}\n`);
|
|
267
315
|
return;
|
|
268
316
|
}
|
|
269
|
-
|
|
270
|
-
|
|
317
|
+
reporter.write(`\n ${C.T}Saved agents${C.R} ${C.D}(${agents.length})${C.R}\n`);
|
|
318
|
+
reporter.write(` ${C.D}${"-".repeat(54)}${C.R}\n`);
|
|
271
319
|
for (const agent of agents) {
|
|
272
320
|
const isActive = active?.publicId === agent.publicId;
|
|
273
|
-
const marker = isActive ? `${C.OK}
|
|
321
|
+
const marker = isActive ? `${C.OK}>${C.R}` : " ";
|
|
274
322
|
const id = compactValue(agent.publicId, 20, 8);
|
|
275
323
|
const endpoint = compactValue(agent.mcpEndpoint, 30, 10);
|
|
276
324
|
const model = agent.llmModel ?? "default";
|
|
277
|
-
const mode = agent.shadowMode ? `${C.G}shadow${C.R}` : `${C.OK}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
325
|
+
const mode = agent.shadowMode ? `${C.G}shadow${C.R}` : `${C.OK}LIVE${C.R}`;
|
|
326
|
+
reporter.write(` ${marker} ${C.W}${id}${C.R}\n`);
|
|
327
|
+
reporter.write(` ${C.D}Endpoint${C.R} ${endpoint}\n`);
|
|
328
|
+
reporter.write(` ${C.D}Host${C.R} ${endpointHost(agent.mcpEndpoint)}\n`);
|
|
329
|
+
reporter.write(` ${C.D}Model${C.R} ${model} ${mode}\n`);
|
|
330
|
+
reporter.write(` ${C.D}Created${C.R} ${agent.createdAt}\n`);
|
|
331
|
+
reporter.write(` ${C.D}${"-".repeat(54)}${C.R}\n`);
|
|
283
332
|
}
|
|
284
|
-
|
|
333
|
+
reporter.write(`\n ${C.D}Next${C.R} ${C.W}balchemy start${C.R} ${C.D}or${C.R} ${C.W}balchemy agent use <publicId>${C.R}\n\n`);
|
|
285
334
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
335
|
+
function printAgentCurrent() {
|
|
336
|
+
const active = loadAgent();
|
|
337
|
+
const data = {
|
|
338
|
+
active: active ? agentToJson(active, active.publicId) : null,
|
|
339
|
+
hasActiveAgent: Boolean(active),
|
|
340
|
+
storePath: getStorePath(),
|
|
341
|
+
};
|
|
342
|
+
if (parsed.flags.json) {
|
|
343
|
+
reporter.json(jsonEnvelope({ ok: true, command: "agent.current", version: CLI_VERSION, data }));
|
|
291
344
|
return;
|
|
292
345
|
}
|
|
293
|
-
if (
|
|
294
|
-
|
|
346
|
+
if (!active) {
|
|
347
|
+
reporter.write(` ${C.D}No active agent. Run ${C.W}balchemy list${C.D} or ${C.W}balchemy init${C.D}.${C.R}\n`);
|
|
295
348
|
return;
|
|
296
349
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
350
|
+
printSummaryBlock("Active agent", [
|
|
351
|
+
{ label: "Agent", value: active.publicId },
|
|
352
|
+
{ label: "Endpoint", value: compactValue(active.mcpEndpoint, 42, 12) },
|
|
353
|
+
{ label: "Host", value: endpointHost(active.mcpEndpoint) },
|
|
354
|
+
{ label: "Provider", value: active.llmProvider },
|
|
355
|
+
{ label: "Model", value: active.llmModel ?? "default" },
|
|
356
|
+
{ label: "Mode", value: active.shadowMode ? "Shadow" : "LIVE" },
|
|
357
|
+
{ label: "Saved", value: active.createdAt },
|
|
358
|
+
{ label: "Secrets", value: "stored encrypted locally, redacted in CLI output" },
|
|
359
|
+
]);
|
|
360
|
+
}
|
|
361
|
+
function useAgent(publicId) {
|
|
362
|
+
if (!publicId) {
|
|
363
|
+
throw new TerminalError({
|
|
364
|
+
code: "UNKNOWN_COMMAND",
|
|
365
|
+
title: "Missing agent publicId",
|
|
366
|
+
cause: "balchemy agent use requires a saved agent publicId.",
|
|
367
|
+
fix: "Run balchemy list, then pass the exact publicId to agent use.",
|
|
368
|
+
commandSuggestion: "balchemy list",
|
|
369
|
+
exitCode: 2,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (!setActiveAgent(publicId)) {
|
|
373
|
+
throw new TerminalError({
|
|
374
|
+
code: "UNKNOWN_COMMAND",
|
|
375
|
+
title: "Saved agent not found",
|
|
376
|
+
cause: `No saved agent matches ${publicId}.`,
|
|
377
|
+
fix: "Run balchemy list and choose one of the saved publicIds.",
|
|
378
|
+
commandSuggestion: "balchemy list",
|
|
379
|
+
exitCode: 2,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
const active = loadAgent();
|
|
383
|
+
if (parsed.flags.json) {
|
|
384
|
+
reporter.json(jsonEnvelope({
|
|
385
|
+
ok: true,
|
|
386
|
+
command: "agent.use",
|
|
387
|
+
version: CLI_VERSION,
|
|
388
|
+
data: { active: active ? agentToJson(active, active.publicId) : null },
|
|
389
|
+
}));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
reporter.write(` ${C.OK}Active agent:${C.R} ${C.W}${publicId}${C.R}\n`);
|
|
393
|
+
}
|
|
394
|
+
function authLogoutCommand() {
|
|
395
|
+
if (parsed.flags.force) {
|
|
396
|
+
clearAgent();
|
|
397
|
+
if (parsed.flags.json) {
|
|
398
|
+
reporter.json(jsonEnvelope({
|
|
399
|
+
ok: true,
|
|
400
|
+
command: "auth.logout",
|
|
401
|
+
version: CLI_VERSION,
|
|
402
|
+
data: { activeCleared: true, credentialsRemoved: false, storePath: getStorePath() },
|
|
403
|
+
}));
|
|
404
|
+
return;
|
|
304
405
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
406
|
+
reporter.write(` ${C.OK}Active agent cleared.${C.R} ${C.D}Saved encrypted credentials were not removed.${C.R}\n`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
throw new TerminalError({
|
|
410
|
+
code: "FILE_OVERWRITE_CONFIRMATION_REQUIRED",
|
|
411
|
+
title: "Logout confirmation required",
|
|
412
|
+
cause: "auth logout clears local active context and must be explicit.",
|
|
413
|
+
fix: "Run balchemy auth status to inspect context, then rerun with --force to clear only the active selection.",
|
|
414
|
+
commandSuggestion: "balchemy auth status",
|
|
415
|
+
docsHint: "Run balchemy auth logout --help for local credential safety notes.",
|
|
416
|
+
exitCode: 4,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function printAuthStatus() {
|
|
420
|
+
const agents = listAgents();
|
|
421
|
+
const active = loadAgent();
|
|
422
|
+
const data = {
|
|
423
|
+
authenticated: Boolean(active),
|
|
424
|
+
activePublicId: active?.publicId ?? null,
|
|
425
|
+
endpointHost: active ? endpointHost(active.mcpEndpoint) : null,
|
|
426
|
+
savedAgentCount: agents.length,
|
|
427
|
+
encryptedStorePath: getStorePath(),
|
|
428
|
+
credentialsStored: agents.length > 0,
|
|
429
|
+
secrets: {
|
|
430
|
+
apiKeys: agents.length > 0 ? "[redacted]" : null,
|
|
431
|
+
redacted: true,
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
if (parsed.flags.json) {
|
|
435
|
+
reporter.json(jsonEnvelope({ ok: true, command: "auth.status", version: CLI_VERSION, data }));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
printSummaryBlock("Auth status", [
|
|
439
|
+
{ label: "Active", value: active ? active.publicId : "none" },
|
|
440
|
+
{ label: "Host", value: active ? endpointHost(active.mcpEndpoint) : "none" },
|
|
441
|
+
{ label: "Saved", value: String(agents.length) },
|
|
442
|
+
{ label: "Store", value: getStorePath() },
|
|
443
|
+
{ label: "Secrets", value: agents.length > 0 ? "encrypted locally, redacted" : "none" },
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
446
|
+
async function loadConfigWithEnv(configPath) {
|
|
447
|
+
const resolvedPath = path.resolve(configPath);
|
|
448
|
+
const dotenv = await import("dotenv");
|
|
449
|
+
const envPath = path.join(path.dirname(resolvedPath), ".env");
|
|
450
|
+
if (fs.existsSync(envPath)) {
|
|
451
|
+
dotenv.config({ path: envPath });
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
dotenv.config();
|
|
455
|
+
}
|
|
456
|
+
const { loadConfig } = await import("./config-loader.js");
|
|
457
|
+
return {
|
|
458
|
+
config: loadConfig(resolvedPath),
|
|
459
|
+
resolvedPath,
|
|
460
|
+
envPath: fs.existsSync(envPath) ? envPath : null,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function configToJson(config, resolvedPath, envPath) {
|
|
464
|
+
return {
|
|
465
|
+
path: resolvedPath,
|
|
466
|
+
envPath,
|
|
467
|
+
mcpEndpoint: config.mcpEndpoint,
|
|
468
|
+
endpointHost: endpointHost(config.mcpEndpoint),
|
|
469
|
+
apiKey: "[redacted]",
|
|
470
|
+
llm: {
|
|
471
|
+
provider: config.llmProvider,
|
|
472
|
+
model: config.llmModel ?? null,
|
|
473
|
+
baseUrl: config.llmBaseUrl ?? null,
|
|
474
|
+
apiKey: "[redacted]",
|
|
475
|
+
maxDailyUsd: config.maxDailyLlmCost ?? null,
|
|
476
|
+
timeoutMs: config.llmTimeoutMs ?? null,
|
|
477
|
+
},
|
|
478
|
+
webhook: {
|
|
479
|
+
port: config.webhookPort ?? null,
|
|
480
|
+
secret: config.webhookSecret ? "[redacted]" : null,
|
|
481
|
+
},
|
|
482
|
+
behaviorRulesConfigured: Boolean(config.behaviorRules || config.behaviorRulesPath),
|
|
483
|
+
redacted: true,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
async function validateConfigCommand(commandName) {
|
|
487
|
+
const configPath = args[0] ?? path.join(process.cwd(), "agent.config.yaml");
|
|
488
|
+
const { config, resolvedPath, envPath } = await loadConfigWithEnv(configPath);
|
|
489
|
+
const data = configToJson(config, resolvedPath, envPath);
|
|
490
|
+
if (parsed.flags.json) {
|
|
491
|
+
reporter.json(jsonEnvelope({ ok: true, command: commandName, version: CLI_VERSION, data }));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
printSummaryBlock(commandName === "config.list" ? "Effective config" : "Config valid", [
|
|
495
|
+
{ label: "Config", value: resolvedPath },
|
|
496
|
+
{ label: "Env", value: envPath ?? "not found; process env used" },
|
|
497
|
+
{ label: "Endpoint", value: compactValue(config.mcpEndpoint, 42, 12) },
|
|
498
|
+
{ label: "Host", value: endpointHost(config.mcpEndpoint) },
|
|
499
|
+
{ label: "Provider", value: config.llmProvider },
|
|
500
|
+
{ label: "Model", value: config.llmModel ?? "default" },
|
|
501
|
+
{ label: "LLM cap", value: `$${(config.maxDailyLlmCost ?? 5).toFixed(2)} / day` },
|
|
502
|
+
{ label: "Secrets", value: "resolved and redacted" },
|
|
503
|
+
]);
|
|
504
|
+
}
|
|
505
|
+
function initDryRun() {
|
|
506
|
+
const yamlPath = path.join(process.cwd(), "agent.config.yaml");
|
|
507
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
508
|
+
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
509
|
+
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
510
|
+
const plan = [
|
|
511
|
+
{
|
|
512
|
+
action: fs.existsSync(yamlPath) ? "overwrite" : "create",
|
|
513
|
+
path: yamlPath,
|
|
514
|
+
exists: fs.existsSync(yamlPath),
|
|
515
|
+
wouldOverwrite: fs.existsSync(yamlPath),
|
|
516
|
+
containsSecret: false,
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
action: fs.existsSync(envPath) ? "overwrite" : "create",
|
|
520
|
+
path: envPath,
|
|
521
|
+
exists: fs.existsSync(envPath),
|
|
522
|
+
wouldOverwrite: fs.existsSync(envPath),
|
|
523
|
+
containsSecret: true,
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
action: gitignore.includes(".env") ? "skip" : (fs.existsSync(gitignorePath) ? "append" : "create"),
|
|
527
|
+
path: gitignorePath,
|
|
528
|
+
exists: fs.existsSync(gitignorePath),
|
|
529
|
+
wouldOverwrite: false,
|
|
530
|
+
containsSecret: false,
|
|
531
|
+
},
|
|
532
|
+
];
|
|
533
|
+
if (parsed.flags.json) {
|
|
534
|
+
reporter.json(jsonEnvelope({
|
|
535
|
+
ok: true,
|
|
536
|
+
command: "init.preview",
|
|
537
|
+
version: CLI_VERSION,
|
|
538
|
+
data: { dryRun: true, outDir: process.cwd(), plan },
|
|
539
|
+
}));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
reporter.write(`\n ${C.T}Setup file preview${C.R} ${C.D}(dry-run; no files written)${C.R}\n`);
|
|
543
|
+
for (const item of plan) {
|
|
544
|
+
const record = item;
|
|
545
|
+
reporter.write(` ${record.action.toUpperCase().padEnd(9)} ${record.path}${record.containsSecret ? " contains secrets" : ""}\n`);
|
|
546
|
+
}
|
|
547
|
+
reporter.write(`\n ${C.D}Run${C.R} ${C.W}balchemy init${C.R} ${C.D}in an interactive terminal to continue.${C.R}\n\n`);
|
|
548
|
+
}
|
|
549
|
+
function dockerPlanToJson(plan, dryRun) {
|
|
550
|
+
return {
|
|
551
|
+
outDir: plan.outDir,
|
|
552
|
+
dryRun,
|
|
553
|
+
hasOverwrites: plan.hasOverwrites,
|
|
554
|
+
plan: plan.files.map((file) => ({
|
|
555
|
+
filename: file.filename,
|
|
556
|
+
path: file.path,
|
|
557
|
+
action: file.action,
|
|
558
|
+
exists: file.exists,
|
|
559
|
+
wouldOverwrite: file.wouldOverwrite,
|
|
560
|
+
containsSecret: file.containsSecret,
|
|
561
|
+
})),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function renderDockerPlan(plan, dryRun) {
|
|
565
|
+
reporter.write(`\n ${C.T}Docker generation ${dryRun ? "preview" : "plan"}${C.R}${dryRun ? ` ${C.D}(dry-run; no files written)${C.R}` : ""}\n`);
|
|
566
|
+
for (const file of plan.files) {
|
|
567
|
+
const label = file.action.toUpperCase().padEnd(9);
|
|
568
|
+
const suffix = file.wouldOverwrite ? ` ${C.W}requires confirmation${C.R}` : "";
|
|
569
|
+
reporter.write(` ${label} ${file.path}${suffix}\n`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async function dockerCommand() {
|
|
573
|
+
const outDir = path.resolve(args[0] ?? process.cwd());
|
|
574
|
+
const { buildDockerPlan, generateDocker } = await import("./docker-gen.js");
|
|
575
|
+
const plan = buildDockerPlan(outDir);
|
|
576
|
+
if (parsed.flags.json) {
|
|
577
|
+
if (!parsed.flags.dryRun && plan.hasOverwrites && !parsed.flags.force) {
|
|
578
|
+
throw new TerminalError({
|
|
579
|
+
code: "FILE_OVERWRITE_CONFIRMATION_REQUIRED",
|
|
580
|
+
title: "Overwrite confirmation required",
|
|
581
|
+
cause: "Docker generation would overwrite existing files.",
|
|
582
|
+
fix: "Run with --dry-run to inspect or pass --force after reviewing the target paths.",
|
|
583
|
+
commandSuggestion: `balchemy docker ${outDir} --dry-run --json`,
|
|
584
|
+
exitCode: 4,
|
|
335
585
|
});
|
|
336
|
-
break;
|
|
337
586
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
587
|
+
const written = parsed.flags.dryRun ? plan : await generateDocker(outDir, { force: parsed.flags.force });
|
|
588
|
+
reporter.json(jsonEnvelope({
|
|
589
|
+
ok: true,
|
|
590
|
+
command: parsed.flags.dryRun ? "docker.generate.preview" : "docker.generate",
|
|
591
|
+
version: CLI_VERSION,
|
|
592
|
+
data: dockerPlanToJson(written, parsed.flags.dryRun),
|
|
593
|
+
}));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
renderDockerPlan(plan, parsed.flags.dryRun);
|
|
597
|
+
if (parsed.flags.dryRun) {
|
|
598
|
+
reporter.write(`\n ${C.D}Run without --dry-run to write files. Use --force only after reviewing overwrites.${C.R}\n\n`);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (plan.hasOverwrites && !parsed.flags.force) {
|
|
602
|
+
if (parsed.flags.ci || isNonInteractive(parsed.flags)) {
|
|
603
|
+
throw new TerminalError({
|
|
604
|
+
code: "FILE_OVERWRITE_CONFIRMATION_REQUIRED",
|
|
605
|
+
title: "Overwrite confirmation required",
|
|
606
|
+
cause: "Docker generation would overwrite files, and this session cannot prompt.",
|
|
607
|
+
fix: "Run balchemy docker --dry-run to inspect, then rerun with --force if the overwrites are intended.",
|
|
608
|
+
commandSuggestion: `balchemy docker ${outDir} --dry-run`,
|
|
609
|
+
exitCode: 4,
|
|
610
|
+
});
|
|
344
611
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
612
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY && process.stdout.isTTY });
|
|
613
|
+
const answer = await ask(rl, `${C.W}Type overwrite to replace existing Docker files${C.R}`);
|
|
614
|
+
rl.close();
|
|
615
|
+
if (answer !== "overwrite") {
|
|
616
|
+
throw new TerminalError({
|
|
617
|
+
code: "FILE_OVERWRITE_CONFIRMATION_REQUIRED",
|
|
618
|
+
title: "Docker generation cancelled",
|
|
619
|
+
cause: "Overwrite confirmation was not provided.",
|
|
620
|
+
fix: "No files were changed. Rerun with --dry-run to inspect the plan.",
|
|
621
|
+
commandSuggestion: `balchemy docker ${outDir} --dry-run`,
|
|
622
|
+
exitCode: 4,
|
|
623
|
+
});
|
|
348
624
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
625
|
+
}
|
|
626
|
+
await generateDocker(outDir, { force: parsed.flags.force || plan.hasOverwrites });
|
|
627
|
+
reporter.write(`\n ${C.OK}Docker files written to${C.R} ${outDir}\n`);
|
|
628
|
+
reporter.write(`\n ${C.W}Next steps${C.R}\n`);
|
|
629
|
+
reporter.write(` 1. Copy .env.example to .env and fill in your credentials\n`);
|
|
630
|
+
reporter.write(` 2. Place agent.config.yaml in the same directory\n`);
|
|
631
|
+
reporter.write(` 3. Run: docker compose up -d\n\n`);
|
|
632
|
+
}
|
|
633
|
+
async function doctorCommand() {
|
|
634
|
+
const configPath = args[0] ?? path.join(process.cwd(), "agent.config.yaml");
|
|
635
|
+
const checks = [];
|
|
636
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
637
|
+
checks.push({
|
|
638
|
+
name: "node-version",
|
|
639
|
+
status: nodeMajor >= 18 ? "ok" : "error",
|
|
640
|
+
detail: `Node ${process.versions.node}; required >=18`,
|
|
641
|
+
});
|
|
642
|
+
checks.push({
|
|
643
|
+
name: "agent-store",
|
|
644
|
+
status: fs.existsSync(getStorePath()) ? "ok" : "warn",
|
|
645
|
+
detail: fs.existsSync(getStorePath()) ? `Encrypted store found at ${getStorePath()}` : `No encrypted store at ${getStorePath()}`,
|
|
646
|
+
});
|
|
647
|
+
checks.push({
|
|
648
|
+
name: "terminal",
|
|
649
|
+
status: process.stdout.isTTY ? "ok" : "warn",
|
|
650
|
+
detail: process.stdout.isTTY ? "stdout is interactive" : "stdout is non-interactive; TUI commands will be blocked",
|
|
651
|
+
});
|
|
652
|
+
checks.push({
|
|
653
|
+
name: "config-file",
|
|
654
|
+
status: fs.existsSync(configPath) ? "ok" : "warn",
|
|
655
|
+
detail: fs.existsSync(configPath) ? `Config found at ${path.resolve(configPath)}` : `Config not found at ${path.resolve(configPath)}`,
|
|
656
|
+
});
|
|
657
|
+
if (fs.existsSync(configPath)) {
|
|
658
|
+
try {
|
|
659
|
+
const loaded = await loadConfigWithEnv(configPath);
|
|
660
|
+
checks.push({
|
|
661
|
+
name: "config-validate",
|
|
662
|
+
status: "ok",
|
|
663
|
+
detail: `Config resolves for ${endpointHost(loaded.config.mcpEndpoint)}`,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
checks.push({
|
|
668
|
+
name: "config-validate",
|
|
669
|
+
status: "error",
|
|
670
|
+
detail: toTerminalError(err).cause,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const hasErrors = checks.some((check) => check.status === "error");
|
|
675
|
+
if (parsed.flags.json) {
|
|
676
|
+
reporter.json(jsonEnvelope({
|
|
677
|
+
ok: !hasErrors,
|
|
678
|
+
command: "doctor",
|
|
679
|
+
version: CLI_VERSION,
|
|
680
|
+
data: { checks: checks, configPath: path.resolve(configPath) },
|
|
681
|
+
}));
|
|
682
|
+
if (hasErrors)
|
|
683
|
+
process.exitCode = 2;
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
reporter.write(`\n ${C.T}Balchemy CLI doctor${C.R}\n`);
|
|
687
|
+
for (const check of checks) {
|
|
688
|
+
const label = check.status === "ok" ? `${C.OK}OK${C.R}` : check.status === "warn" ? `${C.G}WARN${C.R}` : `${C.ERR}ERROR${C.R}`;
|
|
689
|
+
reporter.write(` ${label.padEnd(18)} ${check.name.padEnd(16)} ${check.detail}\n`);
|
|
690
|
+
}
|
|
691
|
+
reporter.write("\n");
|
|
692
|
+
if (hasErrors)
|
|
693
|
+
process.exitCode = 2;
|
|
694
|
+
}
|
|
695
|
+
async function startFromConfig() {
|
|
696
|
+
ensureInteractive("balchemy start");
|
|
697
|
+
const configPath = args[0] ?? path.join(process.cwd(), "agent.config.yaml");
|
|
698
|
+
const { config } = await loadConfigWithEnv(configPath);
|
|
699
|
+
const publicId = config.mcpEndpoint.split("/").filter(Boolean).pop() ?? "unknown";
|
|
700
|
+
const { startTui } = await import("./tui/start.js");
|
|
701
|
+
await startTui({
|
|
702
|
+
mcpEndpoint: config.mcpEndpoint,
|
|
703
|
+
apiKey: config.apiKey,
|
|
704
|
+
llmProvider: config.llmProvider,
|
|
705
|
+
llmApiKey: config.llmApiKey,
|
|
706
|
+
llmModel: config.llmModel,
|
|
707
|
+
llmBaseUrl: config.llmBaseUrl,
|
|
708
|
+
maxDailyLlmCost: config.maxDailyLlmCost,
|
|
709
|
+
llmTimeoutMs: config.llmTimeoutMs,
|
|
710
|
+
publicId,
|
|
711
|
+
strategy: "custom",
|
|
712
|
+
shadowMode: false,
|
|
713
|
+
behaviorRules: config.behaviorRules,
|
|
714
|
+
autoSeedSubscriptions: false,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
async function defaultLauncher() {
|
|
718
|
+
if (parsed.flags.json) {
|
|
719
|
+
printAgentCurrent();
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
ensureInteractive("balchemy");
|
|
723
|
+
const agents = listAgents();
|
|
724
|
+
const active = loadAgent();
|
|
725
|
+
const last = active ?? mostRecentAgent(agents);
|
|
726
|
+
if (!last) {
|
|
727
|
+
await runWizardFromCwd();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const { renderLogo } = await import("./terminal-logo.js");
|
|
731
|
+
reporter.write(renderLogo(20));
|
|
732
|
+
reporter.write(`\n ${C.G}B${C.T}alchemy ${C.W}Agent${C.R}\n`);
|
|
733
|
+
reporter.write(` ${C.D}Saved agents ready${C.R}\n\n`);
|
|
734
|
+
printSummaryBlock(active ? "Last session" : "Most recent session", [
|
|
735
|
+
{ label: "Agent", value: last.publicId },
|
|
736
|
+
{ label: "Endpoint", value: compactValue(last.mcpEndpoint, 42, 12) },
|
|
737
|
+
{ label: "Host", value: endpointHost(last.mcpEndpoint) },
|
|
738
|
+
{ label: "Model", value: last.llmModel ?? "default" },
|
|
739
|
+
{ label: "Strategy", value: compactValue(last.strategy, 42, 8) },
|
|
740
|
+
{ label: "Mode", value: last.shadowMode ? "Shadow" : "LIVE" },
|
|
741
|
+
{ label: "Saved", value: last.createdAt },
|
|
742
|
+
]);
|
|
743
|
+
const actions = [
|
|
744
|
+
{ label: "y", value: "Resume this agent" },
|
|
745
|
+
...(agents.length > 1 ? [{ label: "list", value: "Choose another saved agent" }] : []),
|
|
746
|
+
{ label: "new", value: "Create a new agent or connect existing credentials" },
|
|
747
|
+
];
|
|
748
|
+
printSummaryBlock("Available actions", actions);
|
|
749
|
+
reporter.write("\n");
|
|
750
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY && process.stdout.isTTY });
|
|
751
|
+
try {
|
|
752
|
+
const choice = normalizeChoice(await ask(rl, `${C.W}Action?${C.R} (y${agents.length > 1 ? "/list" : ""}/new)`, "y"));
|
|
753
|
+
if (choice === "y" || choice === "yes" || choice === "resume" || choice === "last") {
|
|
754
|
+
await startSavedAgent(last);
|
|
755
|
+
}
|
|
756
|
+
else if (Number.isInteger(Number(choice))
|
|
757
|
+
&& Number(choice) >= 1
|
|
758
|
+
&& Number(choice) <= agents.length) {
|
|
759
|
+
await startSavedAgent(agents[Number(choice) - 1]);
|
|
760
|
+
}
|
|
761
|
+
else if (agents.length > 1
|
|
762
|
+
&& (choice === "list" || choice === "choose" || choice === "select" || choice === "agents")) {
|
|
763
|
+
const selected = await chooseSavedAgent(rl, agents);
|
|
764
|
+
if (!selected) {
|
|
765
|
+
reporter.warn(` ${C.D}No matching saved agent. Starting setup instead.${C.R}\n\n`);
|
|
766
|
+
await runWizardFromCwd();
|
|
408
767
|
}
|
|
409
768
|
else {
|
|
410
|
-
|
|
411
|
-
await runWizardFromCwd();
|
|
769
|
+
await startSavedAgent(selected);
|
|
412
770
|
}
|
|
413
|
-
break;
|
|
414
771
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
772
|
+
else if (choice === "new" || choice === "n") {
|
|
773
|
+
await runWizardFromCwd();
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
reporter.warn(` ${C.D}Unknown action. Starting setup instead.${C.R}\n\n`);
|
|
777
|
+
await runWizardFromCwd();
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
rl.close();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
async function main() {
|
|
785
|
+
if (parsed.unknownFlags.length > 0) {
|
|
786
|
+
throw new TerminalError({
|
|
787
|
+
code: "UNKNOWN_FLAG",
|
|
788
|
+
title: "Unknown flag",
|
|
789
|
+
cause: `Unsupported flag${parsed.unknownFlags.length > 1 ? "s" : ""}: ${parsed.unknownFlags.join(", ")}`,
|
|
790
|
+
fix: "Remove the unsupported flag or run help to see supported global flags.",
|
|
791
|
+
commandSuggestion: "balchemy --help",
|
|
792
|
+
exitCode: 2,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
if (parsed.flags.help || cmd === "help") {
|
|
796
|
+
printHelp();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (parsed.flags.version || cmd === "version") {
|
|
800
|
+
printVersion();
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
await checkForUpdate(parsed.flags);
|
|
804
|
+
switch (cmd) {
|
|
805
|
+
case "init":
|
|
806
|
+
if (parsed.flags.dryRun) {
|
|
807
|
+
initDryRun();
|
|
808
|
+
return;
|
|
420
809
|
}
|
|
421
|
-
|
|
422
|
-
|
|
810
|
+
await runWizardFromCwd();
|
|
811
|
+
return;
|
|
812
|
+
case "start":
|
|
813
|
+
await startFromConfig();
|
|
814
|
+
return;
|
|
815
|
+
case "docker":
|
|
816
|
+
await dockerCommand();
|
|
817
|
+
return;
|
|
818
|
+
case "list":
|
|
819
|
+
case "agent list":
|
|
820
|
+
printAgentList(cmd === "list" ? "agent.list" : "agent.list");
|
|
821
|
+
return;
|
|
822
|
+
case "agent current":
|
|
823
|
+
printAgentCurrent();
|
|
824
|
+
return;
|
|
825
|
+
case "auth login":
|
|
826
|
+
if (parsed.flags.dryRun) {
|
|
827
|
+
initDryRun();
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
await runWizardFromCwd();
|
|
831
|
+
return;
|
|
832
|
+
case "agent use":
|
|
833
|
+
useAgent(args[0]);
|
|
834
|
+
return;
|
|
835
|
+
case "auth status":
|
|
836
|
+
printAuthStatus();
|
|
837
|
+
return;
|
|
838
|
+
case "auth logout":
|
|
839
|
+
authLogoutCommand();
|
|
840
|
+
return;
|
|
841
|
+
case "config validate":
|
|
842
|
+
await validateConfigCommand("config.validate");
|
|
843
|
+
return;
|
|
844
|
+
case "config list":
|
|
845
|
+
await validateConfigCommand("config.list");
|
|
846
|
+
return;
|
|
847
|
+
case "doctor":
|
|
848
|
+
await doctorCommand();
|
|
849
|
+
return;
|
|
850
|
+
case "":
|
|
851
|
+
await defaultLauncher();
|
|
852
|
+
return;
|
|
853
|
+
default: {
|
|
854
|
+
const suggestion = suggestCommand(cmd || (parsed.commandPath[0] ?? ""));
|
|
855
|
+
throw new TerminalError({
|
|
856
|
+
code: "UNKNOWN_COMMAND",
|
|
857
|
+
title: "Unknown command",
|
|
858
|
+
cause: `Unknown command: ${cmd || parsed.commandPath.join(" ")}`,
|
|
859
|
+
fix: suggestion ? `Did you mean balchemy ${suggestion}?` : "Run balchemy --help for available commands.",
|
|
860
|
+
commandSuggestion: suggestion ? `balchemy ${suggestion}` : "balchemy --help",
|
|
861
|
+
exitCode: 2,
|
|
862
|
+
});
|
|
423
863
|
}
|
|
424
864
|
}
|
|
425
865
|
}
|
|
426
866
|
main().catch((err) => {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
867
|
+
const terminalError = toTerminalError(err);
|
|
868
|
+
if (parsed.flags.json) {
|
|
869
|
+
reporter.json(jsonEnvelope({
|
|
870
|
+
ok: false,
|
|
871
|
+
command: cmd || "root",
|
|
872
|
+
version: CLI_VERSION,
|
|
873
|
+
error: terminalErrorToJson(terminalError),
|
|
874
|
+
}));
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
renderTerminalError(reporter, terminalError);
|
|
878
|
+
}
|
|
879
|
+
process.exit(terminalError.exitCode);
|
|
430
880
|
});
|
|
431
881
|
//# sourceMappingURL=index.js.map
|