@orchagent/cli 0.3.86 → 0.3.88

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.
Files changed (34) hide show
  1. package/dist/commands/agent-keys.js +21 -7
  2. package/dist/commands/agents.js +60 -5
  3. package/dist/commands/config.js +4 -0
  4. package/dist/commands/delete.js +2 -2
  5. package/dist/commands/dev.js +226 -0
  6. package/dist/commands/diff.js +418 -0
  7. package/dist/commands/estimate.js +105 -0
  8. package/dist/commands/fork.js +11 -1
  9. package/dist/commands/health.js +226 -0
  10. package/dist/commands/index.js +14 -0
  11. package/dist/commands/info.js +75 -0
  12. package/dist/commands/init.js +729 -38
  13. package/dist/commands/metrics.js +137 -0
  14. package/dist/commands/publish.js +237 -21
  15. package/dist/commands/replay.js +198 -0
  16. package/dist/commands/run.js +272 -28
  17. package/dist/commands/schedule.js +11 -6
  18. package/dist/commands/test.js +68 -1
  19. package/dist/commands/trace.js +311 -0
  20. package/dist/lib/api.js +29 -4
  21. package/dist/lib/batch-publish.js +223 -0
  22. package/dist/lib/dev-server.js +425 -0
  23. package/dist/lib/doctor/checks/environment.js +1 -1
  24. package/dist/lib/key-store.js +121 -0
  25. package/dist/lib/spinner.js +50 -0
  26. package/dist/lib/test-mock-runner.js +334 -0
  27. package/dist/lib/update-notifier.js +1 -1
  28. package/package.json +1 -1
  29. package/src/resources/__pycache__/agent_runner.cpython-311.pyc +0 -0
  30. package/src/resources/__pycache__/agent_runner.cpython-312.pyc +0 -0
  31. package/src/resources/__pycache__/test_agent_runner_mocks.cpython-311-pytest-9.0.2.pyc +0 -0
  32. package/src/resources/__pycache__/test_agent_runner_mocks.cpython-312-pytest-8.4.2.pyc +0 -0
  33. package/src/resources/agent_runner.py +29 -2
  34. package/src/resources/test_agent_runner_mocks.py +290 -0
@@ -0,0 +1,425 @@
1
+ "use strict";
2
+ /**
3
+ * Local HTTP dev server for agent development.
4
+ *
5
+ * Provides agent config loading, execution dispatch (code_runtime, direct_llm,
6
+ * managed_loop), and an HTTP server that accepts JSON input and returns results.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.inferEngine = inferEngine;
13
+ exports.engineLabel = engineLabel;
14
+ exports.loadAgentConfig = loadAgentConfig;
15
+ exports.executeAgent = executeAgent;
16
+ exports.createDevServer = createDevServer;
17
+ const http_1 = __importDefault(require("http"));
18
+ const promises_1 = __importDefault(require("fs/promises"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const os_1 = __importDefault(require("os"));
21
+ const child_process_1 = require("child_process");
22
+ const llm_1 = require("./llm");
23
+ const bundle_1 = require("./bundle");
24
+ const config_1 = require("./config");
25
+ // ─── Engine inference ───────────────────────────────────────────────────────
26
+ function inferEngine(manifest) {
27
+ const hasRuntimeCommand = Boolean(manifest.runtime?.command?.trim());
28
+ const hasLoop = Boolean(manifest.loop && Object.keys(manifest.loop).length > 0);
29
+ if (hasRuntimeCommand)
30
+ return 'code_runtime';
31
+ if (hasLoop)
32
+ return 'managed_loop';
33
+ const rawType = (manifest.type || 'agent').trim().toLowerCase();
34
+ if (rawType === 'tool' || rawType === 'code')
35
+ return 'code_runtime';
36
+ if (rawType === 'agentic')
37
+ return 'managed_loop';
38
+ if (rawType === 'agent')
39
+ return 'managed_loop';
40
+ return 'direct_llm';
41
+ }
42
+ function engineLabel(engine) {
43
+ switch (engine) {
44
+ case 'direct_llm': return 'prompt';
45
+ case 'managed_loop': return 'agent loop';
46
+ case 'code_runtime': return 'code runtime';
47
+ }
48
+ }
49
+ // ─── Agent config loading ───────────────────────────────────────────────────
50
+ async function loadAgentConfig(agentDir) {
51
+ const manifestPath = path_1.default.join(agentDir, 'orchagent.json');
52
+ const raw = await promises_1.default.readFile(manifestPath, 'utf-8');
53
+ const manifest = JSON.parse(raw);
54
+ const engine = inferEngine(manifest);
55
+ // Read prompt.md if needed
56
+ let prompt;
57
+ if (engine === 'direct_llm' || engine === 'managed_loop') {
58
+ try {
59
+ prompt = await promises_1.default.readFile(path_1.default.join(agentDir, 'prompt.md'), 'utf-8');
60
+ }
61
+ catch {
62
+ // Will error at execution time
63
+ }
64
+ }
65
+ // Read schema.json
66
+ let inputSchema;
67
+ let outputSchema;
68
+ try {
69
+ const schemaRaw = await promises_1.default.readFile(path_1.default.join(agentDir, 'schema.json'), 'utf-8');
70
+ const schemas = JSON.parse(schemaRaw);
71
+ inputSchema = schemas.input;
72
+ outputSchema = schemas.output;
73
+ }
74
+ catch {
75
+ // Optional
76
+ }
77
+ // Custom tools
78
+ const customTools = manifest.custom_tools || undefined;
79
+ // Detect entrypoint for code_runtime
80
+ let entrypoint;
81
+ if (engine === 'code_runtime') {
82
+ entrypoint = manifest.entrypoint || (await (0, bundle_1.detectEntrypoint)(agentDir)) || undefined;
83
+ }
84
+ return { manifest, engine, entrypoint, prompt, outputSchema, inputSchema, customTools, agentDir };
85
+ }
86
+ // ─── Agent execution ────────────────────────────────────────────────────────
87
+ /**
88
+ * Execute a code_runtime agent: spawn entrypoint with stdin JSON, return stdout.
89
+ */
90
+ async function executeCodeRuntime(config, input, verbose) {
91
+ if (!config.entrypoint) {
92
+ throw new Error('No entrypoint found. Set "entrypoint" in orchagent.json or create main.py/main.js.');
93
+ }
94
+ const entrypoint = config.entrypoint;
95
+ const isJs = entrypoint.endsWith('.js') || entrypoint.endsWith('.ts') ||
96
+ entrypoint.endsWith('.mjs') || entrypoint.endsWith('.cjs');
97
+ const cmd = isJs ? 'node' : 'python3';
98
+ const inputJson = JSON.stringify(input);
99
+ return new Promise((resolve, reject) => {
100
+ const proc = (0, child_process_1.spawn)(cmd, [entrypoint], {
101
+ cwd: config.agentDir,
102
+ stdio: ['pipe', 'pipe', 'pipe'],
103
+ env: { ...process.env, ORCHAGENT_LOCAL_EXECUTION: 'true' },
104
+ });
105
+ let stdout = '';
106
+ let stderr = '';
107
+ proc.stdout?.on('data', (data) => {
108
+ stdout += data.toString();
109
+ });
110
+ proc.stderr?.on('data', (data) => {
111
+ const text = data.toString();
112
+ stderr += text;
113
+ if (verbose) {
114
+ process.stderr.write(text);
115
+ }
116
+ });
117
+ proc.stdin?.write(inputJson);
118
+ proc.stdin?.end();
119
+ proc.on('close', (code) => {
120
+ if (code !== 0) {
121
+ const detail = stderr.trim() ? `\n${stderr.trim()}` : '';
122
+ reject(new Error(`Entrypoint exited with code ${code}${detail}`));
123
+ return;
124
+ }
125
+ const trimmed = stdout.trim();
126
+ if (!trimmed) {
127
+ resolve({});
128
+ return;
129
+ }
130
+ try {
131
+ resolve(JSON.parse(trimmed));
132
+ }
133
+ catch {
134
+ // Return raw output wrapped
135
+ resolve({ raw_output: trimmed });
136
+ }
137
+ });
138
+ proc.on('error', (err) => {
139
+ reject(new Error(`Failed to spawn ${cmd}: ${err.message}`));
140
+ });
141
+ });
142
+ }
143
+ /**
144
+ * Execute a direct_llm agent: call LLM with prompt + input.
145
+ */
146
+ async function executeDirectLlm(config, input, verbose, resolvedConfig) {
147
+ if (!config.prompt) {
148
+ throw new Error('No prompt.md found. Create prompt.md in the agent directory.');
149
+ }
150
+ const supportedProviders = config.manifest.supported_providers || ['any'];
151
+ const detected = await (0, llm_1.detectLlmKey)(supportedProviders, resolvedConfig);
152
+ if (!detected) {
153
+ throw new Error(`No LLM key found. Set an environment variable (e.g., OPENAI_API_KEY) or add one to .env`);
154
+ }
155
+ const { provider, key, model: serverModel } = detected;
156
+ const model = serverModel
157
+ || config.manifest.default_models?.[provider]
158
+ || (0, llm_1.getDefaultModel)(provider);
159
+ if (verbose) {
160
+ process.stderr.write(` LLM: ${provider} (${model})\n`);
161
+ }
162
+ const prompt = (0, llm_1.buildPrompt)(config.prompt, input);
163
+ return await (0, llm_1.callLlm)(provider, key, model, prompt, config.outputSchema);
164
+ }
165
+ /**
166
+ * Execute a managed_loop agent: spawn agent_runner.py with temp files.
167
+ */
168
+ async function executeManagedLoop(config, input, verbose, resolvedConfig) {
169
+ if (!config.prompt) {
170
+ throw new Error('No prompt.md found. Create prompt.md in the agent directory.');
171
+ }
172
+ const supportedProviders = config.manifest.supported_providers || ['any'];
173
+ const detected = await (0, llm_1.detectLlmKey)(supportedProviders, resolvedConfig);
174
+ if (!detected) {
175
+ throw new Error(`No LLM key found. Set an environment variable (e.g., OPENAI_API_KEY) or add one to .env`);
176
+ }
177
+ const { provider, key: apiKey, model: serverModel } = detected;
178
+ const model = serverModel
179
+ || config.manifest.default_models?.[provider]
180
+ || (0, llm_1.getDefaultModel)(provider);
181
+ const apiKeyEnvVar = llm_1.PROVIDER_ENV_VARS[provider];
182
+ if (verbose) {
183
+ process.stderr.write(` LLM: ${provider} (${model})\n`);
184
+ }
185
+ // Create temp directory with agent files
186
+ const tempDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-dev-${Date.now()}`);
187
+ await promises_1.default.mkdir(tempDir, { recursive: true });
188
+ try {
189
+ // Copy agent_runner.py from resources
190
+ const runnerSource = path_1.default.join(__dirname, '..', 'resources', 'agent_runner.py');
191
+ let runnerContent;
192
+ try {
193
+ runnerContent = await promises_1.default.readFile(runnerSource, 'utf-8');
194
+ }
195
+ catch {
196
+ const altSource = path_1.default.join(__dirname, '..', '..', 'src', 'resources', 'agent_runner.py');
197
+ runnerContent = await promises_1.default.readFile(altSource, 'utf-8');
198
+ }
199
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'agent_runner.py'), runnerContent);
200
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'prompt.md'), config.prompt);
201
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'input.json'), JSON.stringify(input, null, 2));
202
+ if (config.outputSchema) {
203
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'output_schema.json'), JSON.stringify(config.outputSchema));
204
+ }
205
+ if (config.customTools && config.customTools.length > 0) {
206
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'custom_tools.json'), JSON.stringify(config.customTools));
207
+ }
208
+ const subprocessEnv = { ...process.env };
209
+ subprocessEnv.LOCAL_MODE = '1';
210
+ subprocessEnv.LLM_PROVIDER = provider;
211
+ subprocessEnv.LLM_MODEL = model;
212
+ if (apiKeyEnvVar && apiKey) {
213
+ subprocessEnv[apiKeyEnvVar] = apiKey;
214
+ }
215
+ const maxTurns = config.manifest.max_turns || 25;
216
+ return await new Promise((resolve, reject) => {
217
+ const proc = (0, child_process_1.spawn)('python3', ['agent_runner.py', '--max-turns', String(maxTurns), '--verbose'], {
218
+ cwd: tempDir,
219
+ stdio: ['pipe', 'pipe', 'pipe'],
220
+ env: subprocessEnv,
221
+ });
222
+ proc.stdin.end();
223
+ let stdout = '';
224
+ let stderr = '';
225
+ proc.stdout?.on('data', (data) => {
226
+ stdout += data.toString();
227
+ });
228
+ proc.stderr?.on('data', (data) => {
229
+ const text = data.toString();
230
+ stderr += text;
231
+ if (verbose) {
232
+ for (const line of text.split('\n')) {
233
+ if (line.startsWith('@@ORCHAGENT_EVENT:'))
234
+ continue;
235
+ if (line.trim() === '.' || line.trim() === '')
236
+ continue;
237
+ process.stderr.write(` ${line}\n`);
238
+ }
239
+ }
240
+ });
241
+ proc.on('close', (code) => {
242
+ if (stdout.trim()) {
243
+ try {
244
+ const result = JSON.parse(stdout.trim());
245
+ if (code !== 0 && typeof result === 'object' && result !== null && 'error' in result) {
246
+ reject(new Error(String(result.error)));
247
+ return;
248
+ }
249
+ resolve(result);
250
+ }
251
+ catch {
252
+ if (code !== 0) {
253
+ reject(new Error(`Agent exited with code ${code}`));
254
+ }
255
+ else {
256
+ resolve({ raw_output: stdout.trim() });
257
+ }
258
+ }
259
+ }
260
+ else if (code !== 0) {
261
+ reject(new Error(`Agent exited with code ${code}${stderr.trim() ? `: ${stderr.trim().slice(0, 200)}` : ''}`));
262
+ }
263
+ else {
264
+ resolve({});
265
+ }
266
+ });
267
+ proc.on('error', (err) => {
268
+ reject(new Error(`Failed to spawn python3: ${err.message}`));
269
+ });
270
+ });
271
+ }
272
+ finally {
273
+ try {
274
+ await promises_1.default.rm(tempDir, { recursive: true, force: true });
275
+ }
276
+ catch {
277
+ // Ignore cleanup errors
278
+ }
279
+ }
280
+ }
281
+ /**
282
+ * Execute an agent with the given input, dispatching by engine type.
283
+ */
284
+ async function executeAgent(config, input, verbose, resolvedConfig) {
285
+ switch (config.engine) {
286
+ case 'code_runtime':
287
+ return executeCodeRuntime(config, input, verbose);
288
+ case 'direct_llm':
289
+ return executeDirectLlm(config, input, verbose, resolvedConfig);
290
+ case 'managed_loop':
291
+ return executeManagedLoop(config, input, verbose, resolvedConfig);
292
+ }
293
+ }
294
+ // ─── HTTP server ────────────────────────────────────────────────────────────
295
+ function readBody(req) {
296
+ return new Promise((resolve, reject) => {
297
+ const chunks = [];
298
+ req.on('data', (chunk) => chunks.push(chunk));
299
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
300
+ req.on('error', reject);
301
+ });
302
+ }
303
+ function sendJson(res, statusCode, body) {
304
+ const json = JSON.stringify(body, null, 2);
305
+ res.writeHead(statusCode, {
306
+ 'Content-Type': 'application/json',
307
+ 'Access-Control-Allow-Origin': '*',
308
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
309
+ 'Access-Control-Allow-Headers': 'Content-Type',
310
+ });
311
+ res.end(json);
312
+ }
313
+ function createDevServer(port, verbose, getConfig, callbacks = {}) {
314
+ let requestCounter = 0;
315
+ const server = http_1.default.createServer(async (req, res) => {
316
+ const startTime = Date.now();
317
+ const reqId = ++requestCounter;
318
+ const method = req.method || 'GET';
319
+ const urlPath = req.url || '/';
320
+ // CORS preflight
321
+ if (method === 'OPTIONS') {
322
+ sendJson(res, 204, null);
323
+ return;
324
+ }
325
+ // GET /health — agent info
326
+ if (method === 'GET' && (urlPath === '/health' || urlPath === '/info')) {
327
+ const config = getConfig();
328
+ if (!config) {
329
+ sendJson(res, 503, { status: 'error', error: 'Agent configuration invalid' });
330
+ return;
331
+ }
332
+ sendJson(res, 200, {
333
+ status: 'ok',
334
+ agent: config.manifest.name,
335
+ version: config.manifest.version,
336
+ type: config.manifest.type,
337
+ engine: config.engine,
338
+ entrypoint: config.entrypoint,
339
+ has_prompt: Boolean(config.prompt),
340
+ has_schema: Boolean(config.inputSchema || config.outputSchema),
341
+ });
342
+ return;
343
+ }
344
+ // GET / — usage page
345
+ if (method === 'GET' && urlPath === '/') {
346
+ const config = getConfig();
347
+ const name = config?.manifest.name || 'unknown';
348
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
349
+ res.end(`orchagent dev server — ${name}\n\n` +
350
+ `Endpoints:\n` +
351
+ ` POST / Run agent with JSON body\n` +
352
+ ` POST /run Run agent with JSON body\n` +
353
+ ` GET /health Agent configuration info\n\n` +
354
+ `Example:\n` +
355
+ ` curl -X POST http://localhost:${port}/run \\\n` +
356
+ ` -H "Content-Type: application/json" \\\n` +
357
+ ` -d '{"task": "hello world"}'\n`);
358
+ return;
359
+ }
360
+ // POST / or POST /run — execute agent
361
+ if (method === 'POST' && (urlPath === '/' || urlPath === '/run')) {
362
+ const config = getConfig();
363
+ if (!config) {
364
+ const log = {
365
+ id: reqId, method, path: urlPath, statusCode: 503,
366
+ durationMs: Date.now() - startTime, error: 'Agent configuration invalid',
367
+ };
368
+ callbacks.onRequest?.(log);
369
+ sendJson(res, 503, { error: 'Agent configuration invalid. Check console for validation errors.' });
370
+ return;
371
+ }
372
+ let input;
373
+ try {
374
+ const body = await readBody(req);
375
+ input = body.trim() ? JSON.parse(body) : {};
376
+ }
377
+ catch {
378
+ const log = {
379
+ id: reqId, method, path: urlPath, statusCode: 400,
380
+ durationMs: Date.now() - startTime, error: 'Invalid JSON body',
381
+ };
382
+ callbacks.onRequest?.(log);
383
+ sendJson(res, 400, { error: 'Invalid JSON body' });
384
+ return;
385
+ }
386
+ const inputPreview = JSON.stringify(input).slice(0, 80);
387
+ try {
388
+ const resolvedCfg = await (0, config_1.getResolvedConfig)();
389
+ const result = await executeAgent(config, input, verbose, resolvedCfg);
390
+ const durationMs = Date.now() - startTime;
391
+ const log = {
392
+ id: reqId, method, path: urlPath, statusCode: 200,
393
+ durationMs, inputPreview,
394
+ };
395
+ callbacks.onRequest?.(log);
396
+ sendJson(res, 200, result);
397
+ }
398
+ catch (err) {
399
+ const durationMs = Date.now() - startTime;
400
+ const message = err instanceof Error ? err.message : String(err);
401
+ const log = {
402
+ id: reqId, method, path: urlPath, statusCode: 500,
403
+ durationMs, inputPreview, error: message,
404
+ };
405
+ callbacks.onRequest?.(log);
406
+ callbacks.onError?.(err instanceof Error ? err : new Error(message));
407
+ sendJson(res, 500, { error: message });
408
+ }
409
+ return;
410
+ }
411
+ // 404 for everything else
412
+ sendJson(res, 404, { error: `Not found: ${method} ${urlPath}` });
413
+ });
414
+ const close = () => {
415
+ return new Promise((resolve, reject) => {
416
+ server.close((err) => {
417
+ if (err)
418
+ reject(err);
419
+ else
420
+ resolve();
421
+ });
422
+ });
423
+ };
424
+ return { server, close };
425
+ }
@@ -87,7 +87,7 @@ async function checkCliVersion() {
87
87
  name: 'cli_version',
88
88
  status: 'warning',
89
89
  message: `CLI v${installedVersion} (v${latestVersion} available)`,
90
- fix: 'Run `npm update -g @orchagent/cli` to update',
90
+ fix: 'Run `npm install -g @orchagent/cli@latest` to update',
91
91
  details: { installed: installedVersion, latest: latestVersion },
92
92
  };
93
93
  }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.saveServiceKey = saveServiceKey;
7
+ exports.loadServiceKeys = loadServiceKeys;
8
+ exports.listAllLocalKeys = listAllLocalKeys;
9
+ exports.deleteLocalKey = deleteLocalKey;
10
+ exports.getKeysDir = getKeysDir;
11
+ const promises_1 = __importDefault(require("fs/promises"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const KEYS_DIR = path_1.default.join(os_1.default.homedir(), '.orchagent', 'keys');
15
+ /**
16
+ * Build the path to the key file for an agent: ~/.orchagent/keys/{org}/{agent}.json
17
+ */
18
+ function keyFilePath(org, agentName) {
19
+ return path_1.default.join(KEYS_DIR, org, `${agentName}.json`);
20
+ }
21
+ /**
22
+ * Save a service key locally after creation (publish, fork, or agent-keys create).
23
+ * Keys are stored per-agent in ~/.orchagent/keys/{org}/{agent}.json with 0600 permissions.
24
+ */
25
+ async function saveServiceKey(org, agentName, agentVersion, key, prefix) {
26
+ const filePath = keyFilePath(org, agentName);
27
+ const dir = path_1.default.dirname(filePath);
28
+ await promises_1.default.mkdir(dir, { recursive: true, mode: 0o700 });
29
+ // Load existing keys for this agent
30
+ const existing = await loadServiceKeys(org, agentName);
31
+ const entry = {
32
+ key,
33
+ prefix,
34
+ agent_version: agentVersion,
35
+ created_at: new Date().toISOString(),
36
+ };
37
+ existing.push(entry);
38
+ await promises_1.default.writeFile(filePath, JSON.stringify(existing, null, 2) + '\n', { mode: 0o600 });
39
+ await promises_1.default.chmod(filePath, 0o600);
40
+ return filePath;
41
+ }
42
+ /**
43
+ * Load locally-saved service keys for a specific agent.
44
+ */
45
+ async function loadServiceKeys(org, agentName) {
46
+ try {
47
+ const raw = await promises_1.default.readFile(keyFilePath(org, agentName), 'utf-8');
48
+ return JSON.parse(raw);
49
+ }
50
+ catch (err) {
51
+ if (err.code === 'ENOENT') {
52
+ return [];
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+ /**
58
+ * List all locally-saved service keys across all orgs/agents.
59
+ * Returns entries grouped by org/agent.
60
+ */
61
+ async function listAllLocalKeys() {
62
+ const results = [];
63
+ let orgDirs;
64
+ try {
65
+ orgDirs = await promises_1.default.readdir(KEYS_DIR);
66
+ }
67
+ catch (err) {
68
+ if (err.code === 'ENOENT') {
69
+ return [];
70
+ }
71
+ throw err;
72
+ }
73
+ for (const orgDir of orgDirs) {
74
+ const orgPath = path_1.default.join(KEYS_DIR, orgDir);
75
+ const stat = await promises_1.default.stat(orgPath);
76
+ if (!stat.isDirectory())
77
+ continue;
78
+ const files = await promises_1.default.readdir(orgPath);
79
+ for (const file of files) {
80
+ if (!file.endsWith('.json'))
81
+ continue;
82
+ const agentName = file.replace('.json', '');
83
+ try {
84
+ const raw = await promises_1.default.readFile(path_1.default.join(orgPath, file), 'utf-8');
85
+ const keys = JSON.parse(raw);
86
+ if (keys.length > 0) {
87
+ results.push({ org: orgDir, agent: agentName, keys });
88
+ }
89
+ }
90
+ catch {
91
+ // Skip corrupted files
92
+ }
93
+ }
94
+ }
95
+ return results;
96
+ }
97
+ /**
98
+ * Delete a locally-saved key by prefix match. Returns true if found and removed.
99
+ */
100
+ async function deleteLocalKey(org, agentName, prefix) {
101
+ const keys = await loadServiceKeys(org, agentName);
102
+ const filtered = keys.filter(k => k.prefix !== prefix);
103
+ if (filtered.length === keys.length) {
104
+ return false; // nothing removed
105
+ }
106
+ const filePath = keyFilePath(org, agentName);
107
+ if (filtered.length === 0) {
108
+ await promises_1.default.unlink(filePath);
109
+ }
110
+ else {
111
+ await promises_1.default.writeFile(filePath, JSON.stringify(filtered, null, 2) + '\n', { mode: 0o600 });
112
+ await promises_1.default.chmod(filePath, 0o600);
113
+ }
114
+ return true;
115
+ }
116
+ /**
117
+ * Get the keys directory path (for display purposes).
118
+ */
119
+ function getKeysDir() {
120
+ return KEYS_DIR;
121
+ }
@@ -8,6 +8,8 @@ exports.isProgressEnabled = isProgressEnabled;
8
8
  exports.createSpinner = createSpinner;
9
9
  exports.withSpinner = withSpinner;
10
10
  exports.createProgressSpinner = createProgressSpinner;
11
+ exports.formatElapsed = formatElapsed;
12
+ exports.createElapsedSpinner = createElapsedSpinner;
11
13
  const ora_1 = __importDefault(require("ora"));
12
14
  // Global flag to control spinner visibility (set via --no-progress)
13
15
  let progressEnabled = true;
@@ -118,3 +120,51 @@ function createProgressSpinner(initialText) {
118
120
  };
119
121
  return { spinner, updateProgress };
120
122
  }
123
+ /**
124
+ * Format elapsed seconds as a human-readable string.
125
+ * Under 60s: "5.0s", 60s+: "1m 23s"
126
+ */
127
+ function formatElapsed(seconds) {
128
+ if (seconds < 60) {
129
+ return `${seconds.toFixed(1)}s`;
130
+ }
131
+ const mins = Math.floor(seconds / 60);
132
+ const secs = Math.floor(seconds % 60);
133
+ return `${mins}m ${secs.toString().padStart(2, '0')}s`;
134
+ }
135
+ /**
136
+ * Create a spinner that auto-updates with elapsed time.
137
+ * Shows "Running agent... (5.0s)" and ticks every second.
138
+ * Call dispose() to stop the timer when done (before spinner.stop/succeed/fail).
139
+ */
140
+ function createElapsedSpinner(text) {
141
+ const spinner = createSpinner(text);
142
+ const startTime = Date.now();
143
+ let timer = null;
144
+ const originalStart = spinner.start.bind(spinner);
145
+ spinner.start = function (newText) {
146
+ originalStart(newText);
147
+ timer = setInterval(() => {
148
+ const elapsed = (Date.now() - startTime) / 1000;
149
+ spinner.text = `${text} (${formatElapsed(elapsed)})`;
150
+ }, 1000);
151
+ return this;
152
+ };
153
+ const dispose = () => {
154
+ if (timer) {
155
+ clearInterval(timer);
156
+ timer = null;
157
+ }
158
+ };
159
+ // Auto-dispose on stop/succeed/fail so callers can't leak timers
160
+ const wrap = (fn) => {
161
+ return ((...args) => {
162
+ dispose();
163
+ return fn(...args);
164
+ });
165
+ };
166
+ spinner.stop = wrap(spinner.stop.bind(spinner));
167
+ spinner.succeed = wrap(spinner.succeed.bind(spinner));
168
+ spinner.fail = wrap(spinner.fail.bind(spinner));
169
+ return { spinner, dispose };
170
+ }