@agent-compose/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/index.js +979 -0
- package/package.json +58 -0
- package/skills/ac:demo.md +134 -0
- package/skills/ac:generate-agent.md +70 -0
- package/skills/ac:generate-runtime.md +41 -0
- package/skills/ac:generate-workflow.md +85 -0
- package/skills/ac:invoke.md +42 -0
- package/skills/ac:logs.md +39 -0
- package/skills/ac:register-invoke.md +28 -0
- package/skills/ac:register.md +43 -0
- package/skills/ac:secrets.md +42 -0
- package/skills/ac:setup.md +91 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import { program } from "commander";
|
|
7
|
+
|
|
8
|
+
// src/commands/register.ts
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
11
|
+
import { resolve, basename } from "node:path";
|
|
12
|
+
import { bundleWorkflow } from "@agent-compose/sdk";
|
|
13
|
+
|
|
14
|
+
// src/config.ts
|
|
15
|
+
import { AgentComposeClient, AgentComposeError } from "@agent-compose/sdk";
|
|
16
|
+
import { InvalidArgumentError } from "commander";
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { join, dirname } from "node:path";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
function parseUrlFlag(v) {
|
|
21
|
+
if (!v || !v.trim())
|
|
22
|
+
throw new InvalidArgumentError("Server URL is empty — set AGENT_COMPOSE_URL or pass --url https://...");
|
|
23
|
+
try {
|
|
24
|
+
new URL(v);
|
|
25
|
+
} catch {
|
|
26
|
+
throw new InvalidArgumentError(`"${v}" is not a valid URL.`);
|
|
27
|
+
}
|
|
28
|
+
if (!/^https?:/.test(v))
|
|
29
|
+
throw new InvalidArgumentError(`Server URL must be http(s), got "${v}".`);
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
var CONFIG_DIR_NAME = ".agentc";
|
|
33
|
+
var SETTINGS_FILE = "settings.json";
|
|
34
|
+
var LOCAL_SETTINGS_FILE = "settings.local.json";
|
|
35
|
+
var GLOBAL_CONFIG_DIR = join(homedir(), ".config", "agentc");
|
|
36
|
+
var GLOBAL_CONFIG_FILE = join(GLOBAL_CONFIG_DIR, SETTINGS_FILE);
|
|
37
|
+
function loadGlobalSettings() {
|
|
38
|
+
for (const name of [SETTINGS_FILE, "config.json"]) {
|
|
39
|
+
const path = join(GLOBAL_CONFIG_DIR, name);
|
|
40
|
+
try {
|
|
41
|
+
if (existsSync(path))
|
|
42
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
async function saveGlobalSettings(config) {
|
|
48
|
+
await Bun.spawn(["mkdir", "-p", GLOBAL_CONFIG_DIR]).exited;
|
|
49
|
+
await Bun.write(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2) + `
|
|
50
|
+
`);
|
|
51
|
+
await Bun.spawn(["chmod", "600", GLOBAL_CONFIG_FILE]).exited;
|
|
52
|
+
}
|
|
53
|
+
function migrateLegacyConfig(dir, legacyPath) {
|
|
54
|
+
const { mkdirSync, writeFileSync, unlinkSync } = __require("node:fs");
|
|
55
|
+
const legacy = JSON.parse(readFileSync(legacyPath, "utf-8"));
|
|
56
|
+
const newDir = join(dir, CONFIG_DIR_NAME);
|
|
57
|
+
mkdirSync(newDir, { recursive: true });
|
|
58
|
+
const envs = {};
|
|
59
|
+
const keys = {};
|
|
60
|
+
if (legacy.envs) {
|
|
61
|
+
for (const [name, def] of Object.entries(legacy.envs)) {
|
|
62
|
+
if (def.url)
|
|
63
|
+
envs[name] = { url: def.url, ...def.dashboardUrl ? { dashboardUrl: def.dashboardUrl } : {} };
|
|
64
|
+
if (def.apiKey)
|
|
65
|
+
keys[name] = def.apiKey;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (Object.keys(envs).length) {
|
|
69
|
+
writeFileSync(join(newDir, SETTINGS_FILE), JSON.stringify({ envs }, null, 2) + `
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
const local = {};
|
|
73
|
+
if (legacy.env)
|
|
74
|
+
local.env = legacy.env;
|
|
75
|
+
if (Object.keys(keys).length)
|
|
76
|
+
local.keys = keys;
|
|
77
|
+
if (Object.keys(local).length) {
|
|
78
|
+
writeFileSync(join(newDir, LOCAL_SETTINGS_FILE), JSON.stringify(local, null, 2) + `
|
|
79
|
+
`);
|
|
80
|
+
}
|
|
81
|
+
unlinkSync(legacyPath);
|
|
82
|
+
console.error(`✓ Migrated .agentc.json → .agentc/ (${dir})
|
|
83
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
function findProjectRoot() {
|
|
86
|
+
let dir = process.cwd();
|
|
87
|
+
const root = homedir();
|
|
88
|
+
while (dir !== root && dir !== dirname(dir)) {
|
|
89
|
+
const candidate = join(dir, CONFIG_DIR_NAME, SETTINGS_FILE);
|
|
90
|
+
if (existsSync(candidate)) {
|
|
91
|
+
try {
|
|
92
|
+
return [JSON.parse(readFileSync(candidate, "utf-8")), dir];
|
|
93
|
+
} catch {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const legacyPath = join(dir, ".agentc.json");
|
|
98
|
+
if (existsSync(legacyPath)) {
|
|
99
|
+
try {
|
|
100
|
+
migrateLegacyConfig(dir, legacyPath);
|
|
101
|
+
} catch {}
|
|
102
|
+
if (existsSync(candidate)) {
|
|
103
|
+
try {
|
|
104
|
+
return [JSON.parse(readFileSync(candidate, "utf-8")), dir];
|
|
105
|
+
} catch {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
dir = dirname(dir);
|
|
112
|
+
}
|
|
113
|
+
return [{}, null];
|
|
114
|
+
}
|
|
115
|
+
function loadLocalSettings(projectDir) {
|
|
116
|
+
if (!projectDir)
|
|
117
|
+
return {};
|
|
118
|
+
const path = join(projectDir, CONFIG_DIR_NAME, LOCAL_SETTINGS_FILE);
|
|
119
|
+
try {
|
|
120
|
+
if (!existsSync(path))
|
|
121
|
+
return {};
|
|
122
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
123
|
+
} catch {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function saveLocalSettings(projectDir, settings) {
|
|
128
|
+
const dir = join(projectDir, CONFIG_DIR_NAME);
|
|
129
|
+
const path = join(dir, LOCAL_SETTINGS_FILE);
|
|
130
|
+
await Bun.spawn(["mkdir", "-p", dir]).exited;
|
|
131
|
+
await Bun.write(path, JSON.stringify(settings, null, 2) + `
|
|
132
|
+
`);
|
|
133
|
+
await Bun.spawn(["chmod", "600", path]).exited;
|
|
134
|
+
}
|
|
135
|
+
var [_project, _projectDir] = findProjectRoot();
|
|
136
|
+
var _local = loadLocalSettings(_projectDir);
|
|
137
|
+
var _global = loadGlobalSettings();
|
|
138
|
+
var _activeEnv = _local.env ?? (_project.envs?.["prod"] ? "prod" : undefined);
|
|
139
|
+
var _envUrl = _activeEnv ? _project.envs?.[_activeEnv]?.url : undefined;
|
|
140
|
+
var _envDashboardUrl = _activeEnv ? _project.envs?.[_activeEnv]?.dashboardUrl : undefined;
|
|
141
|
+
var _envKey = _activeEnv ? _local.keys?.[_activeEnv] : undefined;
|
|
142
|
+
var _envFactory = _activeEnv ? _local.factories?.[_activeEnv] : undefined;
|
|
143
|
+
function fallbackDashboardUrl(env) {
|
|
144
|
+
return env === "local" ? "http://localhost:3000" : "https://platform.agentcompose.ai";
|
|
145
|
+
}
|
|
146
|
+
var defaultUrl = process.env.AGENT_COMPOSE_URL ?? _envUrl ?? _global.url ?? "https://agent-compose-server.fly.dev";
|
|
147
|
+
var defaultDashboardUrl = process.env.AGENT_COMPOSE_DASHBOARD_URL ?? _envDashboardUrl ?? _global.dashboardUrl ?? fallbackDashboardUrl(_activeEnv);
|
|
148
|
+
var defaultApiKey = process.env.AGENT_COMPOSE_API_KEY ?? _envKey ?? _global.apiKey ?? "";
|
|
149
|
+
var defaultFactory = process.env.AGENT_COMPOSE_FACTORY ?? _envFactory ?? "default";
|
|
150
|
+
var projectDir = _projectDir;
|
|
151
|
+
var projectSettings = _project;
|
|
152
|
+
var localSettings = _local;
|
|
153
|
+
var activeEnv = _activeEnv;
|
|
154
|
+
function makeClient({ url, apiKey }) {
|
|
155
|
+
if (!apiKey) {
|
|
156
|
+
console.error(`API key required. Provide via:
|
|
157
|
+
` + ` • Project: .agentc/settings.local.json
|
|
158
|
+
` + ` • Auth: agentc auth login YOUR_KEY
|
|
159
|
+
` + ` • Environment: export AGENT_COMPOSE_API_KEY=YOUR_KEY
|
|
160
|
+
` + " • Flag: --api-key YOUR_KEY");
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
return new AgentComposeClient(url, apiKey);
|
|
164
|
+
}
|
|
165
|
+
function reportSdkError(err) {
|
|
166
|
+
const msg = err instanceof AgentComposeError ? err.message : err instanceof Error ? err.message : String(err);
|
|
167
|
+
console.error(`Error: ${msg}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/commands/register.ts
|
|
172
|
+
var registerCommand = new Command("register").description("Register a workflow with the server").argument("<workflow>", "Path to the workflow .ts file").option("-n, --name <name>", "Template name (default: workflow filename without extension)").option("-v, --version <version>", "Version tag").option("--schedule <cron>", "Cron expression to run on a schedule (e.g. '*/30 * * * *')").option("--build", "After register, invoke once and wait so a snapshot exists for referencing workflows").option("--build-timeout <minutes>", "Minutes to wait for --build invoke to finish (default 30)", (v) => {
|
|
173
|
+
const n = Number.parseInt(v, 10);
|
|
174
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
175
|
+
throw new Error(`--build-timeout must be a positive integer (got "${v}")`);
|
|
176
|
+
return n;
|
|
177
|
+
}).option("--factory <slug>", `Factory to register into (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (workflowArg, opts) => {
|
|
178
|
+
const workflowPath = resolve(workflowArg);
|
|
179
|
+
const name = opts.name ?? basename(workflowPath).replace(/\.(ts|js)$/, "");
|
|
180
|
+
if (!existsSync2(workflowPath)) {
|
|
181
|
+
console.error(`Error: workflow not found at ${workflowPath}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
const { source, networkPolicy, placeholders, snapshot, saveSnapshot } = await bundleWorkflow(workflowPath);
|
|
185
|
+
if (networkPolicy)
|
|
186
|
+
console.log(`[register] Network policy detected — credentials will be brokered via Vercel firewall`);
|
|
187
|
+
if (snapshot)
|
|
188
|
+
console.log(`[register] Boots from snapshot: ${snapshot}`);
|
|
189
|
+
if (saveSnapshot)
|
|
190
|
+
console.log(`[register] Captures snapshot on success (saveSnapshot: true)`);
|
|
191
|
+
const client = makeClient(opts);
|
|
192
|
+
console.log(`[register] Registering "${name}" → factory "${opts.factory}"…`);
|
|
193
|
+
const result = await client.register({
|
|
194
|
+
name,
|
|
195
|
+
source,
|
|
196
|
+
factorySlug: opts.factory,
|
|
197
|
+
...opts.version ? { version: opts.version } : {},
|
|
198
|
+
...opts.schedule ? { schedule: opts.schedule } : {},
|
|
199
|
+
...networkPolicy ? { networkPolicy } : {},
|
|
200
|
+
...placeholders ? { placeholders } : {},
|
|
201
|
+
...snapshot ? { snapshot } : {},
|
|
202
|
+
...saveSnapshot !== undefined ? { saveSnapshot } : {}
|
|
203
|
+
});
|
|
204
|
+
const scheduleNote = opts.schedule ? ` — schedule: ${opts.schedule}` : "";
|
|
205
|
+
console.log(`✓ Workflow: ${result.name}@${result.version} (${result.id})${scheduleNote}`);
|
|
206
|
+
if (!opts.build)
|
|
207
|
+
return;
|
|
208
|
+
console.log(`[build] Invoking "${name}" with saveSnapshot:true to capture a snapshot…`);
|
|
209
|
+
const { id: runId } = await client.invoke(name, {}, { saveSnapshot: true, factorySlug: opts.factory });
|
|
210
|
+
console.log(`[build] Run: ${runId} — waiting for completion…`);
|
|
211
|
+
const pollIntervalMs = 2000;
|
|
212
|
+
const timeoutMinutes = opts.buildTimeout ?? 30;
|
|
213
|
+
const timeoutMs = timeoutMinutes * 60 * 1000;
|
|
214
|
+
const deadline = Date.now() + timeoutMs;
|
|
215
|
+
while (Date.now() < deadline) {
|
|
216
|
+
const { status } = await client.getStatus(runId);
|
|
217
|
+
if (status === "success") {
|
|
218
|
+
console.log(`✓ Snapshot ready — other workflows can now reference \`snapshot: "${name}"\``);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (status === "failed" || status === "abandoned") {
|
|
222
|
+
console.error(`✗ Build run ${status}: ${runId}`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
226
|
+
}
|
|
227
|
+
console.error(`✗ Build timed out after ${timeoutMs / 60000}m (run ${runId} still running)`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// src/commands/invoke.ts
|
|
232
|
+
import { Command as Command3 } from "commander";
|
|
233
|
+
|
|
234
|
+
// src/commands/logs.ts
|
|
235
|
+
import { Command as Command2 } from "commander";
|
|
236
|
+
import pc from "picocolors";
|
|
237
|
+
import { AgentComposeError as AgentComposeError2 } from "@agent-compose/sdk";
|
|
238
|
+
function fmt(ms) {
|
|
239
|
+
if (ms < 60000)
|
|
240
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
241
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor(ms % 60000 / 1000)}s`;
|
|
242
|
+
}
|
|
243
|
+
function extractToolSnippet(toolInput) {
|
|
244
|
+
const raw = toolInput ? String(toolInput) : "";
|
|
245
|
+
return raw.match(/"(?:command|file_path|url|query|pattern)":"([^"]{1,80})/)?.[1] ?? "";
|
|
246
|
+
}
|
|
247
|
+
function printEvent(event, data, state) {
|
|
248
|
+
switch (event) {
|
|
249
|
+
case "step_started":
|
|
250
|
+
process.stdout.write(`
|
|
251
|
+
${pc.dim(`● ${data.step}`)}
|
|
252
|
+
`);
|
|
253
|
+
break;
|
|
254
|
+
case "step_completed": {
|
|
255
|
+
const dur = data.durationMs ? pc.dim(` (${fmt(Number(data.durationMs))})`) : "";
|
|
256
|
+
process.stdout.write(` ${pc.green("✓")} ${data.step}${dur}
|
|
257
|
+
`);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "step_failed":
|
|
261
|
+
process.stdout.write(` ${pc.red("✗")} ${data.step}: ${pc.dim(String(data.reason ?? ""))}
|
|
262
|
+
`);
|
|
263
|
+
break;
|
|
264
|
+
case "agent_spawned": {
|
|
265
|
+
const name = String(data.agentName ?? data.label ?? "agent");
|
|
266
|
+
const id = String(data.agentId ?? "");
|
|
267
|
+
const sbId = data.sandboxId ? String(data.sandboxId).slice(-8) : "";
|
|
268
|
+
state.agentStart.set(id, Date.now());
|
|
269
|
+
state.agentLabel.set(id, name);
|
|
270
|
+
const tag = sbId ? ` ${pc.dim(`[${sbId}]`)}` : "";
|
|
271
|
+
process.stdout.write(`
|
|
272
|
+
${pc.bold(pc.magenta(`▶ ${name}`))}${tag}
|
|
273
|
+
`);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case "agent_settled": {
|
|
277
|
+
const ok = data.outcome === "success" || data.outcome === "done";
|
|
278
|
+
const id = String(data.agentId ?? "");
|
|
279
|
+
const ms = data.durationMs ? Number(data.durationMs) : state.agentStart.get(id) ? Date.now() - state.agentStart.get(id) : 0;
|
|
280
|
+
const name = state.agentLabel.get(id) ?? String(data.label ?? "agent");
|
|
281
|
+
const dur = ms > 0 ? ` ${pc.dim(fmt(ms))}` : "";
|
|
282
|
+
if (ok) {
|
|
283
|
+
process.stdout.write(`${pc.green(`✓ ${name}`)}${dur}
|
|
284
|
+
`);
|
|
285
|
+
} else {
|
|
286
|
+
const reason = data.failureReason ? `
|
|
287
|
+
${pc.dim(String(data.failureReason).slice(0, 120))}` : "";
|
|
288
|
+
process.stdout.write(`${pc.red(`✗ ${name}`)}${dur}${reason}
|
|
289
|
+
`);
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case "agent_event": {
|
|
294
|
+
const { toolName } = data;
|
|
295
|
+
if (toolName) {
|
|
296
|
+
const snippet = extractToolSnippet(data.toolInput);
|
|
297
|
+
const detail = snippet ? ` ${pc.dim(snippet)}` : "";
|
|
298
|
+
process.stdout.write(` ${pc.cyan("→")} ${pc.bold(String(toolName))}${detail}
|
|
299
|
+
`);
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case "workflow_log": {
|
|
304
|
+
const msg = String(data.msg ?? "").trim();
|
|
305
|
+
if (!msg || msg === ".")
|
|
306
|
+
break;
|
|
307
|
+
if (/^\[.+\]\[(cwd|init|RAW):/.test(msg))
|
|
308
|
+
break;
|
|
309
|
+
if (/^\[.+\]\[stderr\]/.test(msg))
|
|
310
|
+
break;
|
|
311
|
+
if (/^\[.+\] iteration \d+\/\d+ · \d+ turns/.test(msg)) {
|
|
312
|
+
const m = msg.match(/iteration (\d+)\/(\d+)/);
|
|
313
|
+
if (m)
|
|
314
|
+
process.stdout.write(` ${pc.dim(`iteration ${m[1]}/${m[2]}`)}
|
|
315
|
+
`);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
if (/^\[.+\]\[result:\d+\/\d+\/\w+\]/.test(msg)) {
|
|
319
|
+
const m = msg.match(/\[result:(\d+)\/(\d+)\/(\w+)\]/);
|
|
320
|
+
if (m)
|
|
321
|
+
process.stdout.write(` ${pc.dim(`${m[3]} ${m[1]}/${m[2]} turns`)}
|
|
322
|
+
`);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
if (/^\[.+\] iteration \d+ status:/.test(msg)) {
|
|
326
|
+
const m = msg.match(/blockers=\[([^\]]+)\]/);
|
|
327
|
+
if (m?.[1]) {
|
|
328
|
+
const blockers = m[1].replace(/^"|"$/g, "").trim();
|
|
329
|
+
if (blockers)
|
|
330
|
+
process.stdout.write(` ${pc.yellow(`⚠ ${blockers.slice(0, 120)}`)}
|
|
331
|
+
`);
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
if (/^\[.+\] (Bash|Read|Write|Edit|Glob|Grep|Agent|TodoWrite|ToolSearch|WebFetch|WebSearch)\(/.test(msg))
|
|
336
|
+
break;
|
|
337
|
+
if (/^\[.+\] (iteration|continuing|done after|stalled|exhausted|circuit break)/.test(msg))
|
|
338
|
+
break;
|
|
339
|
+
const lvl = String(data.level ?? "info");
|
|
340
|
+
const color = lvl === "error" ? pc.red : lvl === "warn" ? pc.yellow : pc.dim;
|
|
341
|
+
process.stdout.write(`${color(msg)}
|
|
342
|
+
`);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case "run_complete": {
|
|
346
|
+
const elapsed = fmt(Date.now() - state.startedAt);
|
|
347
|
+
process.stdout.write(`
|
|
348
|
+
${pc.bold(pc.green("✓ Run complete"))} ${pc.dim(elapsed)}
|
|
349
|
+
`);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case "run_failed": {
|
|
353
|
+
const reason = data.reason ? `
|
|
354
|
+
${pc.dim(String(data.reason).slice(0, 200))}` : "";
|
|
355
|
+
process.stdout.write(`
|
|
356
|
+
${pc.bold(pc.red("✗ Run failed"))}${reason}
|
|
357
|
+
`);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case "run_canceled":
|
|
361
|
+
process.stdout.write(`
|
|
362
|
+
${pc.bold(pc.yellow("⊘ Run canceled"))}
|
|
363
|
+
`);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
var TERMINAL_EVENTS = new Set(["run_complete", "run_failed", "run_canceled"]);
|
|
368
|
+
async function streamLogs(runId, opts) {
|
|
369
|
+
const client = makeClient(opts);
|
|
370
|
+
const state = {
|
|
371
|
+
agentStart: new Map,
|
|
372
|
+
agentLabel: new Map,
|
|
373
|
+
startedAt: Date.now()
|
|
374
|
+
};
|
|
375
|
+
let lastSeq = 0;
|
|
376
|
+
const MAX_RETRIES = 5;
|
|
377
|
+
for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
|
|
378
|
+
let settled = false;
|
|
379
|
+
try {
|
|
380
|
+
for await (const ev of client.streamRunLogs(runId, { lastEventId: lastSeq })) {
|
|
381
|
+
const seq = ev.seq ?? 0;
|
|
382
|
+
if (seq > 0)
|
|
383
|
+
lastSeq = seq;
|
|
384
|
+
printEvent(ev.event, ev, state);
|
|
385
|
+
if (TERMINAL_EVENTS.has(ev.event)) {
|
|
386
|
+
settled = true;
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
if (err instanceof AgentComposeError2) {
|
|
392
|
+
console.error(`Error: failed to connect to log stream (${err.status})`);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
throw err;
|
|
396
|
+
}
|
|
397
|
+
if (settled)
|
|
398
|
+
return;
|
|
399
|
+
if (attempt < MAX_RETRIES)
|
|
400
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
401
|
+
}
|
|
402
|
+
console.error("Error: stream closed without terminal event after retries");
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
var logsCommand = new Command2("logs").description("Stream logs for a run").argument("<run-id>", "Run ID").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (runId, opts) => {
|
|
406
|
+
await streamLogs(runId, opts);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// src/commands/invoke.ts
|
|
410
|
+
var invokeCommand = new Command3("invoke").description("Invoke a registered workflow template").argument("<template>", "Template name").option("-i, --input <json>", "Input as JSON string").option("-f, --follow", "Stream logs after invoking").option("--factory <slug>", `Factory the template lives in (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (template, opts) => {
|
|
411
|
+
let input;
|
|
412
|
+
if (opts.input) {
|
|
413
|
+
try {
|
|
414
|
+
input = JSON.parse(opts.input);
|
|
415
|
+
} catch {
|
|
416
|
+
console.error("Error: --input must be valid JSON");
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const client = makeClient(opts);
|
|
421
|
+
const { id } = await client.invoke(template, input, { factorySlug: opts.factory });
|
|
422
|
+
console.log(`Run started: ${id}`);
|
|
423
|
+
if (opts.follow)
|
|
424
|
+
await streamLogs(id, opts);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// src/commands/list.ts
|
|
428
|
+
import { Command as Command4 } from "commander";
|
|
429
|
+
var listCommand = new Command4("list").description("List registered workflow templates").option("--factory <slug>", "Scope to a factory (default: every factory in your team)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (opts) => {
|
|
430
|
+
const factorySlug = opts.factory ?? (defaultFactory !== "default" ? defaultFactory : undefined);
|
|
431
|
+
const templates = await makeClient(opts).listTemplates(factorySlug ? { factorySlug } : undefined);
|
|
432
|
+
if (templates.length === 0) {
|
|
433
|
+
const scope = factorySlug ? ` in factory "${factorySlug}"` : "";
|
|
434
|
+
console.log(`No templates registered${scope}.`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
for (const t of templates)
|
|
438
|
+
console.log(`${t.name} ${t.version}`);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// src/commands/init.ts
|
|
442
|
+
import { Command as Command5 } from "commander";
|
|
443
|
+
import {
|
|
444
|
+
existsSync as existsSync3,
|
|
445
|
+
mkdirSync,
|
|
446
|
+
copyFileSync,
|
|
447
|
+
readdirSync,
|
|
448
|
+
readFileSync as readFileSync2,
|
|
449
|
+
appendFileSync,
|
|
450
|
+
unlinkSync,
|
|
451
|
+
rmSync,
|
|
452
|
+
statSync
|
|
453
|
+
} from "node:fs";
|
|
454
|
+
import { resolve as resolve2, join as join2, dirname as dirname2 } from "node:path";
|
|
455
|
+
import { fileURLToPath } from "node:url";
|
|
456
|
+
import { homedir as homedir2 } from "node:os";
|
|
457
|
+
var BUN_PATH_LINE = 'export PATH="$HOME/.bun/bin:$PATH"';
|
|
458
|
+
function shellConfigFile() {
|
|
459
|
+
const shell = process.env.SHELL ?? "";
|
|
460
|
+
if (shell.includes("zsh"))
|
|
461
|
+
return join2(homedir2(), ".zshrc");
|
|
462
|
+
if (shell.includes("fish"))
|
|
463
|
+
return join2(homedir2(), ".config", "fish", "config.fish");
|
|
464
|
+
return join2(homedir2(), ".bashrc");
|
|
465
|
+
}
|
|
466
|
+
function ensureBunInPath() {
|
|
467
|
+
const configFile = shellConfigFile();
|
|
468
|
+
try {
|
|
469
|
+
const content = existsSync3(configFile) ? readFileSync2(configFile, "utf-8") : "";
|
|
470
|
+
if (content.includes(".bun/bin")) {
|
|
471
|
+
console.log(`✓ ~/.bun/bin already in PATH (${configFile})`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
appendFileSync(configFile, `
|
|
475
|
+
${BUN_PATH_LINE}
|
|
476
|
+
`);
|
|
477
|
+
console.log(`✓ Added ~/.bun/bin to PATH → ${configFile}`);
|
|
478
|
+
console.log(" Run: source " + configFile);
|
|
479
|
+
} catch {
|
|
480
|
+
console.warn(` Could not update ${configFile} — add this manually:`);
|
|
481
|
+
console.warn(` ${BUN_PATH_LINE}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function readDescription(filePath) {
|
|
485
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
486
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/m);
|
|
487
|
+
if (!fmMatch)
|
|
488
|
+
return;
|
|
489
|
+
const descMatch = fmMatch[1].match(/^description:\s*(.+)$/m);
|
|
490
|
+
return descMatch?.[1].trim();
|
|
491
|
+
}
|
|
492
|
+
function locateSkillsDir(name) {
|
|
493
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
494
|
+
const candidates = [
|
|
495
|
+
resolve2(here, "..", "..", name),
|
|
496
|
+
resolve2(here, "..", name)
|
|
497
|
+
];
|
|
498
|
+
return candidates.find(existsSync3) ?? null;
|
|
499
|
+
}
|
|
500
|
+
function cleanupLegacyCommands() {
|
|
501
|
+
const legacyDir = join2(homedir2(), ".claude", "commands");
|
|
502
|
+
if (!existsSync3(legacyDir))
|
|
503
|
+
return 0;
|
|
504
|
+
let removed = 0;
|
|
505
|
+
for (const entry of readdirSync(legacyDir)) {
|
|
506
|
+
if (entry.startsWith("ac:") && entry.endsWith(".md")) {
|
|
507
|
+
try {
|
|
508
|
+
unlinkSync(join2(legacyDir, entry));
|
|
509
|
+
removed++;
|
|
510
|
+
} catch {}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return removed;
|
|
514
|
+
}
|
|
515
|
+
function loadSource(dir) {
|
|
516
|
+
if (!dir)
|
|
517
|
+
return null;
|
|
518
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
519
|
+
return { dir, files };
|
|
520
|
+
}
|
|
521
|
+
var initCommand = new Command5("init").description("Install agentc Claude Code skills and ensure PATH is configured").option("--dev", "Also install dev-only skills from cli/skills-dev/ (clone install only)").action((opts) => {
|
|
522
|
+
ensureBunInPath();
|
|
523
|
+
const publicSrc = loadSource(locateSkillsDir("skills"));
|
|
524
|
+
if (!publicSrc) {
|
|
525
|
+
console.error("Error: skills directory not found — reinstall agentc");
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
const devSrc = opts.dev ? loadSource(locateSkillsDir("skills-dev")) : null;
|
|
529
|
+
if (opts.dev && !devSrc) {
|
|
530
|
+
console.log(`
|
|
531
|
+
Note: --dev passed but cli/skills-dev/ is not present. Dev skills ` + "are not bundled with npm installs of @agent-compose/cli — clone the " + "repo (https://github.com/Layr-Labs/agent-compose) and re-run from there " + "to install them.");
|
|
532
|
+
}
|
|
533
|
+
const targetRoot = join2(homedir2(), ".claude", "skills");
|
|
534
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
535
|
+
const wanted = [
|
|
536
|
+
...publicSrc.files.map((file) => ({ src: publicSrc, file })),
|
|
537
|
+
...(devSrc?.files ?? []).map((file) => ({ src: devSrc, file }))
|
|
538
|
+
];
|
|
539
|
+
const wantedNames = new Set(wanted.map((w) => w.file.replace(".md", "")));
|
|
540
|
+
for (const existing of readdirSync(targetRoot)) {
|
|
541
|
+
if (!existing.startsWith("ac:"))
|
|
542
|
+
continue;
|
|
543
|
+
const path = join2(targetRoot, existing);
|
|
544
|
+
if (!statSync(path).isDirectory())
|
|
545
|
+
continue;
|
|
546
|
+
if (wantedNames.has(existing))
|
|
547
|
+
continue;
|
|
548
|
+
rmSync(path, { recursive: true, force: true });
|
|
549
|
+
console.log(`✗ Removed /${existing}`);
|
|
550
|
+
}
|
|
551
|
+
console.log(`
|
|
552
|
+
Installed skills${opts.dev ? " (dev mode)" : ""}:`);
|
|
553
|
+
for (const { src, file } of [...wanted].sort((a, b) => a.file.localeCompare(b.file))) {
|
|
554
|
+
const name = file.replace(".md", "");
|
|
555
|
+
const dir = join2(targetRoot, name);
|
|
556
|
+
mkdirSync(dir, { recursive: true });
|
|
557
|
+
copyFileSync(join2(src.dir, file), join2(dir, "SKILL.md"));
|
|
558
|
+
const description = readDescription(join2(src.dir, file)) ?? "";
|
|
559
|
+
console.log(` ${("/" + name).padEnd(28)} ${description}`);
|
|
560
|
+
}
|
|
561
|
+
const legacyRemoved = cleanupLegacyCommands();
|
|
562
|
+
if (legacyRemoved > 0) {
|
|
563
|
+
console.log(`
|
|
564
|
+
Cleaned up ${legacyRemoved} legacy ac:* file${legacyRemoved > 1 ? "s" : ""} from ~/.claude/commands/`);
|
|
565
|
+
}
|
|
566
|
+
const inactiveDev = !opts.dev ? loadSource(locateSkillsDir("skills-dev"))?.files.length ?? 0 : 0;
|
|
567
|
+
const note = inactiveDev > 0 ? ` (${inactiveDev} dev-only skill${inactiveDev > 1 ? "s" : ""} available — re-run with --dev to include)` : "";
|
|
568
|
+
console.log(`
|
|
569
|
+
${wanted.length} skill(s) installed to ${targetRoot}${note}`);
|
|
570
|
+
console.log(" Run /reload-plugins in Claude Code to pick them up immediately.");
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// src/commands/keys.ts
|
|
574
|
+
import { Command as Command6 } from "commander";
|
|
575
|
+
import { AgentComposeClient as AgentComposeClient2 } from "@agent-compose/sdk";
|
|
576
|
+
var DURATION_UNITS = {
|
|
577
|
+
s: 1000,
|
|
578
|
+
m: 60000,
|
|
579
|
+
h: 3600000,
|
|
580
|
+
d: 86400000,
|
|
581
|
+
w: 86400000 * 7
|
|
582
|
+
};
|
|
583
|
+
function parseDuration(s) {
|
|
584
|
+
if (!/^(\d+[smhdw])+$/.test(s))
|
|
585
|
+
throw new Error(`Invalid duration "${s}". Expected e.g. "30d", "2h", "7d12h".`);
|
|
586
|
+
let total = 0;
|
|
587
|
+
for (const m of s.matchAll(/(\d+)([smhdw])/g))
|
|
588
|
+
total += parseInt(m[1], 10) * DURATION_UNITS[m[2]];
|
|
589
|
+
return total;
|
|
590
|
+
}
|
|
591
|
+
function requireAdminKey(adminKey) {
|
|
592
|
+
if (adminKey)
|
|
593
|
+
return;
|
|
594
|
+
console.error(`Admin-scoped ac_… key required. Provide via:
|
|
595
|
+
` + ` • Flag: --admin-key ac_…
|
|
596
|
+
` + ` • Environment: export AGENT_COMPOSE_ADMIN_KEY=ac_…
|
|
597
|
+
` + "Mint your first admin key by signing into the dashboard.");
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
var keysCommand = new Command6("keys").description("Manage API keys");
|
|
601
|
+
keysCommand.command("create <name>").description("Create a new API key (requires admin-scoped ac_… key)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--admin-key <key>", "Admin-scoped ac_… key (or AGENT_COMPOSE_ADMIN_KEY env var)", process.env.AGENT_COMPOSE_ADMIN_KEY ?? "").option("--scopes <list>", "Comma-separated scope list: read, invoke, admin. Default (empty) = full team access.").option("--expires-in <duration>", `Expire the key after this duration — e.g. "30d", "2h", "7d12h", "1w", "90m". Default: never.`).option("--factory <slug>", "Restrict the key to a single factory (slug). Default: team-wide.").action(async (name, opts) => {
|
|
602
|
+
requireAdminKey(opts.adminKey);
|
|
603
|
+
const scopes = opts.scopes ? opts.scopes.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
604
|
+
let expiresAt;
|
|
605
|
+
if (opts.expiresIn) {
|
|
606
|
+
try {
|
|
607
|
+
expiresAt = new Date(Date.now() + parseDuration(opts.expiresIn)).toISOString();
|
|
608
|
+
} catch (err) {
|
|
609
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const client = new AgentComposeClient2(opts.url, opts.adminKey);
|
|
614
|
+
const created = await client.createApiKey({
|
|
615
|
+
name,
|
|
616
|
+
...scopes && { scopes },
|
|
617
|
+
...expiresAt && { expiresAt },
|
|
618
|
+
...opts.factory && { factorySlug: opts.factory }
|
|
619
|
+
}).catch(reportSdkError);
|
|
620
|
+
console.log(`
|
|
621
|
+
API key created for "${name}":
|
|
622
|
+
`);
|
|
623
|
+
console.log(` ${created.key}
|
|
624
|
+
`);
|
|
625
|
+
console.log(` id: ${created.id}`);
|
|
626
|
+
console.log(` preview: ac_…${created.last4 ?? "????"}`);
|
|
627
|
+
console.log(` scopes: ${scopes?.join(", ") || "(full team access)"}`);
|
|
628
|
+
console.log(` factory: ${opts.factory ?? "(team-wide)"}`);
|
|
629
|
+
console.log(` expires: ${created.expiresAt ? new Date(created.expiresAt).toISOString() : "never"}
|
|
630
|
+
`);
|
|
631
|
+
console.log(`Share this with the user. They can save it permanently with:`);
|
|
632
|
+
console.log(` agentc auth login ${created.key}
|
|
633
|
+
`);
|
|
634
|
+
});
|
|
635
|
+
keysCommand.command("list").description("List API keys for your team (requires admin-scoped ac_… key)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--admin-key <key>", "Admin-scoped ac_… key", process.env.AGENT_COMPOSE_ADMIN_KEY ?? "").action(async (opts) => {
|
|
636
|
+
requireAdminKey(opts.adminKey);
|
|
637
|
+
const client = new AgentComposeClient2(opts.url, opts.adminKey);
|
|
638
|
+
const data = await client.listApiKeys().catch(reportSdkError);
|
|
639
|
+
if (!data.length) {
|
|
640
|
+
console.log("No API keys.");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const fmt2 = (d) => d ? new Date(d).toISOString().slice(0, 10) : "—";
|
|
644
|
+
for (const k of data) {
|
|
645
|
+
const status = k.revokedAt ? "revoked" : k.expiresAt && new Date(k.expiresAt) < new Date ? "expired" : "active";
|
|
646
|
+
const scopes = k.scopes.length > 0 ? k.scopes.join(",") : "full";
|
|
647
|
+
const id = `ac_…${k.last4 ?? "????"}`;
|
|
648
|
+
const factory = k.factoryId ? `factory:${k.factoryId}` : "factory:team-wide";
|
|
649
|
+
console.log(`${k.name ?? "(unnamed)"} ${id} ${k.id} [${scopes}] ${factory} ${status} created:${fmt2(k.createdAt)} last_used:${fmt2(k.lastUsedAt)} expires:${fmt2(k.expiresAt)}`);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// src/commands/auth.ts
|
|
654
|
+
import { Command as Command7 } from "commander";
|
|
655
|
+
var authCommand = new Command7("auth").description("Manage authentication");
|
|
656
|
+
authCommand.command("login <key>").description("Save an API key for the active environment").action(async (key) => {
|
|
657
|
+
if (projectDir && activeEnv) {
|
|
658
|
+
if (!projectSettings.envs?.[activeEnv]) {
|
|
659
|
+
console.error(`Environment "${activeEnv}" is not defined in .agentc/settings.json`);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
await saveLocalSettings(projectDir, {
|
|
663
|
+
...localSettings,
|
|
664
|
+
keys: { ...localSettings.keys, [activeEnv]: key }
|
|
665
|
+
});
|
|
666
|
+
console.log(`
|
|
667
|
+
✓ API key saved for "${activeEnv}" in .agentc/settings.local.json
|
|
668
|
+
`);
|
|
669
|
+
} else if (projectDir) {
|
|
670
|
+
console.error(`No active environment. Set "env" in .agentc/settings.local.json first.
|
|
671
|
+
` + `Available: ${Object.keys(projectSettings.envs ?? {}).join(", ") || "(none)"}`);
|
|
672
|
+
process.exit(1);
|
|
673
|
+
} else {
|
|
674
|
+
const existing = loadGlobalSettings();
|
|
675
|
+
await saveGlobalSettings({ ...existing, apiKey: key });
|
|
676
|
+
console.log(`
|
|
677
|
+
✓ API key saved to ${GLOBAL_CONFIG_FILE}
|
|
678
|
+
`);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
authCommand.command("logout").description("Remove the API key for the active environment").action(async () => {
|
|
682
|
+
if (projectDir && activeEnv) {
|
|
683
|
+
if (!localSettings.keys?.[activeEnv]) {
|
|
684
|
+
console.log(`No API key stored for "${activeEnv}".`);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const { [activeEnv]: _, ...rest } = localSettings.keys;
|
|
688
|
+
await saveLocalSettings(projectDir, { ...localSettings, keys: rest });
|
|
689
|
+
console.log(`✓ API key removed for "${activeEnv}".`);
|
|
690
|
+
} else {
|
|
691
|
+
const existing = loadGlobalSettings();
|
|
692
|
+
const { apiKey: _, ...rest } = existing;
|
|
693
|
+
await saveGlobalSettings(rest);
|
|
694
|
+
console.log(`✓ API key removed from ${GLOBAL_CONFIG_FILE}.`);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
authCommand.command("status").description("Show the resolved configuration").action(() => {
|
|
698
|
+
if (activeEnv) {
|
|
699
|
+
console.log(`Env: ${activeEnv}`);
|
|
700
|
+
}
|
|
701
|
+
if (process.env.AGENT_COMPOSE_URL) {
|
|
702
|
+
console.log(`Server: ${defaultUrl} ($AGENT_COMPOSE_URL)`);
|
|
703
|
+
} else if (activeEnv && projectSettings.envs?.[activeEnv]?.url) {
|
|
704
|
+
console.log(`Server: ${defaultUrl} (.agentc/settings.json → ${activeEnv})`);
|
|
705
|
+
} else {
|
|
706
|
+
console.log(`Server: ${defaultUrl}`);
|
|
707
|
+
}
|
|
708
|
+
if (process.env.AGENT_COMPOSE_DASHBOARD_URL) {
|
|
709
|
+
console.log(`Dashboard: ${defaultDashboardUrl} ($AGENT_COMPOSE_DASHBOARD_URL)`);
|
|
710
|
+
} else if (activeEnv && projectSettings.envs?.[activeEnv]?.dashboardUrl) {
|
|
711
|
+
console.log(`Dashboard: ${defaultDashboardUrl} (.agentc/settings.json → ${activeEnv})`);
|
|
712
|
+
} else {
|
|
713
|
+
console.log(`Dashboard: ${defaultDashboardUrl}`);
|
|
714
|
+
}
|
|
715
|
+
if (process.env.AGENT_COMPOSE_FACTORY) {
|
|
716
|
+
console.log(`Factory: ${defaultFactory} ($AGENT_COMPOSE_FACTORY)`);
|
|
717
|
+
} else if (activeEnv && localSettings.factories?.[activeEnv]) {
|
|
718
|
+
console.log(`Factory: ${defaultFactory} (.agentc/settings.local.json → ${activeEnv})`);
|
|
719
|
+
} else {
|
|
720
|
+
console.log(`Factory: ${defaultFactory} (default)`);
|
|
721
|
+
}
|
|
722
|
+
if (process.env.AGENT_COMPOSE_API_KEY) {
|
|
723
|
+
console.log(`API key: set ($AGENT_COMPOSE_API_KEY)`);
|
|
724
|
+
} else if (activeEnv && localSettings.keys?.[activeEnv]) {
|
|
725
|
+
console.log(`API key: set (.agentc/settings.local.json → ${activeEnv})`);
|
|
726
|
+
} else if (defaultApiKey) {
|
|
727
|
+
console.log(`API key: set (${GLOBAL_CONFIG_FILE})`);
|
|
728
|
+
} else {
|
|
729
|
+
console.log("API key: not set");
|
|
730
|
+
console.log(`
|
|
731
|
+
Run: agentc auth login YOUR_KEY`);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// src/commands/secrets.ts
|
|
736
|
+
import { Command as Command8 } from "commander";
|
|
737
|
+
var sharedOpts = (cmd) => cmd.option("--factory <slug>", `Factory the workflow lives in (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey);
|
|
738
|
+
var secretsCommand = new Command8("secrets").description("Manage workflow secrets stored in GCP Secret Manager");
|
|
739
|
+
sharedOpts(secretsCommand.command("set <workflow> <key> <value>").description("Create or update a secret for a workflow")).action(async (workflow, key, value, opts) => {
|
|
740
|
+
const client = makeClient(opts);
|
|
741
|
+
await client.setSecret(workflow, key, value, { factorySlug: opts.factory });
|
|
742
|
+
console.log(`✓ Secret "${key}" set for workflow "${workflow}" in factory "${opts.factory}"`);
|
|
743
|
+
});
|
|
744
|
+
sharedOpts(secretsCommand.command("list <workflow>").description("List secrets registered for a workflow (metadata only)")).action(async (workflow, opts) => {
|
|
745
|
+
const client = makeClient(opts);
|
|
746
|
+
const secrets = await client.listSecrets(workflow, { factorySlug: opts.factory });
|
|
747
|
+
if (secrets.length === 0) {
|
|
748
|
+
console.log(`No secrets registered for workflow "${workflow}" in factory "${opts.factory}"`);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
console.log(`Secrets for "${workflow}" in factory "${opts.factory}":`);
|
|
752
|
+
for (const s of secrets) {
|
|
753
|
+
console.log(` ${s.key} (updated ${new Date(s.updatedAt).toLocaleString()})`);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
sharedOpts(secretsCommand.command("delete <workflow> <key>").description("Delete a secret for a workflow")).action(async (workflow, key, opts) => {
|
|
757
|
+
const client = makeClient(opts);
|
|
758
|
+
await client.deleteSecret(workflow, key, { factorySlug: opts.factory });
|
|
759
|
+
console.log(`✓ Secret "${key}" deleted from workflow "${workflow}" in factory "${opts.factory}"`);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// src/commands/usage.ts
|
|
763
|
+
import { Command as Command9 } from "commander";
|
|
764
|
+
function parseMonth(s) {
|
|
765
|
+
const m = /^(\d{4})-(\d{1,2})$/.exec(s);
|
|
766
|
+
if (!m)
|
|
767
|
+
throw new Error(`--month must be YYYY-MM, got "${s}"`);
|
|
768
|
+
const year = parseInt(m[1], 10);
|
|
769
|
+
const month = parseInt(m[2], 10) - 1;
|
|
770
|
+
if (month < 0 || month > 11)
|
|
771
|
+
throw new Error(`Invalid month "${s}"`);
|
|
772
|
+
return { from: new Date(Date.UTC(year, month, 1)), to: new Date(Date.UTC(year, month + 1, 1)) };
|
|
773
|
+
}
|
|
774
|
+
function fmtSeconds(s) {
|
|
775
|
+
if (s < 60)
|
|
776
|
+
return `${s.toFixed(1)}s`;
|
|
777
|
+
if (s < 3600)
|
|
778
|
+
return `${Math.floor(s / 60)}m ${Math.floor(s % 60)}s`;
|
|
779
|
+
const h = Math.floor(s / 3600);
|
|
780
|
+
return `${h}h ${Math.floor(s % 3600 / 60)}m`;
|
|
781
|
+
}
|
|
782
|
+
var usageCommand = new Command9("usage").description("Show billable usage — runs + sandbox seconds — for the current team").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).option("--month <yyyy-mm>", "Calendar month to report (default: current month, UTC)").option("--from <iso>", "Window start (ISO 8601) — overrides --month").option("--to <iso>", "Window end (ISO 8601) — overrides --month").action(async (opts) => {
|
|
783
|
+
let from, to;
|
|
784
|
+
if (opts.from || opts.to) {
|
|
785
|
+
if (!opts.from || !opts.to) {
|
|
786
|
+
console.error("Error: --from and --to must be provided together");
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
from = new Date(opts.from);
|
|
790
|
+
to = new Date(opts.to);
|
|
791
|
+
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) {
|
|
792
|
+
console.error("Error: --from and --to must be ISO timestamps");
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
} else {
|
|
796
|
+
try {
|
|
797
|
+
({ from, to } = opts.month ? parseMonth(opts.month) : (() => {
|
|
798
|
+
const n = new Date;
|
|
799
|
+
return { from: new Date(Date.UTC(n.getUTCFullYear(), n.getUTCMonth(), 1)), to: new Date(Date.UTC(n.getUTCFullYear(), n.getUTCMonth() + 1, 1)) };
|
|
800
|
+
})());
|
|
801
|
+
} catch (err) {
|
|
802
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const client = makeClient(opts);
|
|
807
|
+
const body = await client.getUsage(from, to).catch(reportSdkError);
|
|
808
|
+
console.log(`
|
|
809
|
+
Usage ${from.toISOString().slice(0, 10)} → ${to.toISOString().slice(0, 10)}
|
|
810
|
+
`);
|
|
811
|
+
const runs = body.data.filter((r) => r.eventType === "run.settled");
|
|
812
|
+
const runsSuccess = runs.filter((r) => r.tags.status === "success").reduce((a, r) => a + r.total, 0);
|
|
813
|
+
const runsFailed = runs.filter((r) => r.tags.status === "failed").reduce((a, r) => a + r.total, 0);
|
|
814
|
+
console.log(` runs completed: ${String(runsSuccess).padStart(8)}`);
|
|
815
|
+
console.log(` runs failed: ${String(runsFailed).padStart(8)}`);
|
|
816
|
+
const seconds = body.data.filter((r) => r.eventType === "sandbox.seconds");
|
|
817
|
+
const totalSec = seconds.reduce((a, r) => a + r.total, 0);
|
|
818
|
+
if (totalSec > 0) {
|
|
819
|
+
console.log(`
|
|
820
|
+
sandbox seconds: ${String(Math.floor(totalSec)).padStart(8)} (${fmtSeconds(totalSec)})`);
|
|
821
|
+
const grouped = new Map;
|
|
822
|
+
for (const r of seconds) {
|
|
823
|
+
const key = `${r.tags.sandbox_kind ?? "?"} (${r.tags.provider ?? "?"})`;
|
|
824
|
+
grouped.set(key, (grouped.get(key) ?? 0) + r.total);
|
|
825
|
+
}
|
|
826
|
+
for (const [k, v] of [...grouped].sort((a, b) => b[1] - a[1])) {
|
|
827
|
+
console.log(` ${k.padEnd(26)} ${String(Math.floor(v)).padStart(8)} (${fmtSeconds(v)})`);
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
console.log(`
|
|
831
|
+
sandbox seconds: 0`);
|
|
832
|
+
}
|
|
833
|
+
console.log();
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// src/commands/snapshot.ts
|
|
837
|
+
import { Command as Command10 } from "commander";
|
|
838
|
+
var snapshotCommand = new Command10("snapshot").description("Manage captured sandbox snapshots");
|
|
839
|
+
snapshotCommand.command("list").description("List runs that have a captured sandbox snapshot").option("-w, --workflow <name>", "Filter to a single workflow").option("--limit <n>", "Max rows to show (default 50, max 500)", (v) => parseInt(v, 10)).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (opts) => {
|
|
840
|
+
const rows = await makeClient(opts).listSnapshots({
|
|
841
|
+
...opts.workflow ? { workflow: opts.workflow } : {},
|
|
842
|
+
...opts.limit != null ? { limit: opts.limit } : {}
|
|
843
|
+
});
|
|
844
|
+
if (rows.length === 0) {
|
|
845
|
+
console.log("No captured snapshots.");
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
for (const r of rows) {
|
|
849
|
+
const wfRef = r.workflow ? `${r.workflow}${r.version ? `@${r.version}` : ""}` : "-";
|
|
850
|
+
const when = r.endedAt ?? "-";
|
|
851
|
+
console.log(`${r.runId} ${wfRef} ${when} ${r.vercelSnapshotId}`);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
snapshotCommand.command("delete").description("Delete the snapshot captured by a specific run").argument("<run-id>", "Run ID (UUID)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (runId, opts) => {
|
|
855
|
+
await makeClient(opts).deleteSnapshot(runId);
|
|
856
|
+
console.log(`✓ Deleted snapshot for run ${runId}`);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// src/commands/factory.ts
|
|
860
|
+
import { Command as Command11 } from "commander";
|
|
861
|
+
import { AgentComposeError as AgentComposeError3 } from "@agent-compose/sdk";
|
|
862
|
+
var factoryCommand = new Command11("factory").description("Manage factories (projects within your team)");
|
|
863
|
+
var SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
864
|
+
var SLUG_MAX_LEN = 64;
|
|
865
|
+
function validateSlugOrExit(slug) {
|
|
866
|
+
if (!SLUG_PATTERN.test(slug) || slug.length > SLUG_MAX_LEN) {
|
|
867
|
+
console.error(`Error: slug "${slug}" must be lowercase kebab-case, start with a letter, ` + `max ${SLUG_MAX_LEN} chars (e.g. "mvp-project")`);
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
factoryCommand.command("list").description("List factories in your team").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (opts) => {
|
|
872
|
+
const client = makeClient(opts);
|
|
873
|
+
const factories = await client.listFactories();
|
|
874
|
+
if (factories.length === 0) {
|
|
875
|
+
console.log("No factories. Run `agentc factory create <slug> -n <name>` to make one.");
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const active = defaultFactory;
|
|
879
|
+
for (const f of factories) {
|
|
880
|
+
const marker = f.slug === active ? "*" : " ";
|
|
881
|
+
const desc = f.description ? ` — ${f.description}` : "";
|
|
882
|
+
console.log(`${marker} ${f.slug.padEnd(24)} ${f.name}${desc}`);
|
|
883
|
+
}
|
|
884
|
+
console.log("\n* = active factory (use `agentc factory use <slug>` to change)");
|
|
885
|
+
});
|
|
886
|
+
factoryCommand.command("create <slug>").description("Create a factory").option("-n, --name <name>", "Human-readable name (defaults to the slug)").option("-d, --description <text>", "Optional description").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (slug, opts) => {
|
|
887
|
+
validateSlugOrExit(slug);
|
|
888
|
+
const client = makeClient(opts);
|
|
889
|
+
const created = await client.createFactory({
|
|
890
|
+
slug,
|
|
891
|
+
name: opts.name ?? slug,
|
|
892
|
+
...opts.description ? { description: opts.description } : {}
|
|
893
|
+
});
|
|
894
|
+
console.log(`✓ Factory "${created.slug}" created (${created.id})`);
|
|
895
|
+
});
|
|
896
|
+
factoryCommand.command("use <slug>").description("Set the active factory for the current environment").option("--no-verify", "Skip the server-side existence check (offline workflows)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (slug, opts) => {
|
|
897
|
+
if (!projectDir || !activeEnv) {
|
|
898
|
+
console.error("No active environment. `factory use` writes the active factory to\n" + `.agentc/settings.local.json scoped per env. Set up an env first via
|
|
899
|
+
` + "`agentc auth login <key>` (which assumes the project's settings.json\n" + "defines an `envs` block).");
|
|
900
|
+
process.exit(1);
|
|
901
|
+
}
|
|
902
|
+
if (opts.verify) {
|
|
903
|
+
try {
|
|
904
|
+
await makeClient(opts).getFactory(slug);
|
|
905
|
+
} catch (err) {
|
|
906
|
+
if (err instanceof AgentComposeError3 && err.status === 404) {
|
|
907
|
+
console.error(`Error: factory "${slug}" not found on the server.
|
|
908
|
+
` + " Run `agentc factory list` to see available factories,\n" + " or `agentc factory create <slug>` to make a new one.\n" + " Pass --no-verify to pin anyway (e.g. before creating).");
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
throw err;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
await saveLocalSettings(projectDir, {
|
|
915
|
+
...localSettings,
|
|
916
|
+
factories: { ...localSettings.factories ?? {}, [activeEnv]: slug }
|
|
917
|
+
});
|
|
918
|
+
console.log(`✓ Active factory for "${activeEnv}" set to "${slug}".`);
|
|
919
|
+
});
|
|
920
|
+
factoryCommand.command("delete <slug>").description("Delete a factory (refuses `default` and any factory still containing workflows)").option("--yes", "Skip the confirmation prompt").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (slug, opts) => {
|
|
921
|
+
if (slug === "default") {
|
|
922
|
+
console.error("Error: cannot delete the `default` factory — it's the back-compat anchor for legacy callers.");
|
|
923
|
+
process.exit(1);
|
|
924
|
+
}
|
|
925
|
+
if (!opts.yes) {
|
|
926
|
+
process.stdout.write(`Delete factory "${slug}"? [y/N] `);
|
|
927
|
+
const answer = await new Promise((resolve3) => {
|
|
928
|
+
process.stdin.once("data", (b) => resolve3(b.toString().trim().toLowerCase()));
|
|
929
|
+
});
|
|
930
|
+
if (answer !== "y" && answer !== "yes") {
|
|
931
|
+
console.log("Aborted.");
|
|
932
|
+
process.exit(0);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
await makeClient(opts).deleteFactory(slug);
|
|
936
|
+
console.log(`✓ Factory "${slug}" deleted.`);
|
|
937
|
+
});
|
|
938
|
+
factoryCommand.command("show").description("Show the active factory + how it's resolved").action(() => {
|
|
939
|
+
if (process.env.AGENT_COMPOSE_FACTORY) {
|
|
940
|
+
console.log(`Active factory: ${defaultFactory} ($AGENT_COMPOSE_FACTORY)`);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (activeEnv && localSettings.factories?.[activeEnv]) {
|
|
944
|
+
console.log(`Active factory: ${defaultFactory} (.agentc/settings.local.json → ${activeEnv})`);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
console.log(`Active factory: ${defaultFactory} (default fallback)`);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// src/commands/cancel.ts
|
|
951
|
+
import { Command as Command12 } from "commander";
|
|
952
|
+
import pc2 from "picocolors";
|
|
953
|
+
var cancelCommand = new Command12("cancel").description("Cancel an in-progress run").argument("<run-id>", "Run ID").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).option("--wait", "Stream the run's events until it terminates").action(async (runId, opts) => {
|
|
954
|
+
const client = makeClient(opts);
|
|
955
|
+
const res = await client.cancelRun(runId).catch(reportSdkError);
|
|
956
|
+
if (res.status === "canceled") {
|
|
957
|
+
console.log(`${pc2.yellow("⊘")} canceled ${runId} at ${new Date(res.canceledAt).toISOString()}`);
|
|
958
|
+
} else {
|
|
959
|
+
console.log(`${pc2.dim("⊘")} run ${runId} already ${res.status} at ${new Date(res.canceledAt).toISOString()}`);
|
|
960
|
+
}
|
|
961
|
+
if (opts.wait)
|
|
962
|
+
await streamLogs(runId, opts);
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
// src/index.ts
|
|
966
|
+
program.name("agentc").description("CLI for agent-compose — register, invoke, and monitor workflows").version("0.1.0");
|
|
967
|
+
program.addCommand(registerCommand);
|
|
968
|
+
program.addCommand(invokeCommand);
|
|
969
|
+
program.addCommand(listCommand);
|
|
970
|
+
program.addCommand(logsCommand);
|
|
971
|
+
program.addCommand(initCommand);
|
|
972
|
+
program.addCommand(keysCommand);
|
|
973
|
+
program.addCommand(authCommand);
|
|
974
|
+
program.addCommand(secretsCommand);
|
|
975
|
+
program.addCommand(usageCommand);
|
|
976
|
+
program.addCommand(snapshotCommand);
|
|
977
|
+
program.addCommand(factoryCommand);
|
|
978
|
+
program.addCommand(cancelCommand);
|
|
979
|
+
program.parse();
|