@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.
- package/dist/commands/agent-keys.js +21 -7
- package/dist/commands/agents.js +60 -5
- package/dist/commands/config.js +4 -0
- package/dist/commands/delete.js +2 -2
- package/dist/commands/dev.js +226 -0
- package/dist/commands/diff.js +418 -0
- package/dist/commands/estimate.js +105 -0
- package/dist/commands/fork.js +11 -1
- package/dist/commands/health.js +226 -0
- package/dist/commands/index.js +14 -0
- package/dist/commands/info.js +75 -0
- package/dist/commands/init.js +729 -38
- package/dist/commands/metrics.js +137 -0
- package/dist/commands/publish.js +237 -21
- package/dist/commands/replay.js +198 -0
- package/dist/commands/run.js +272 -28
- package/dist/commands/schedule.js +11 -6
- package/dist/commands/test.js +68 -1
- package/dist/commands/trace.js +311 -0
- package/dist/lib/api.js +29 -4
- package/dist/lib/batch-publish.js +223 -0
- package/dist/lib/dev-server.js +425 -0
- package/dist/lib/doctor/checks/environment.js +1 -1
- package/dist/lib/key-store.js +121 -0
- package/dist/lib/spinner.js +50 -0
- package/dist/lib/test-mock-runner.js +334 -0
- package/dist/lib/update-notifier.js +1 -1
- package/package.json +1 -1
- package/src/resources/__pycache__/agent_runner.cpython-311.pyc +0 -0
- package/src/resources/__pycache__/agent_runner.cpython-312.pyc +0 -0
- package/src/resources/__pycache__/test_agent_runner_mocks.cpython-311-pytest-9.0.2.pyc +0 -0
- package/src/resources/__pycache__/test_agent_runner_mocks.cpython-312-pytest-8.4.2.pyc +0 -0
- package/src/resources/agent_runner.py +29 -2
- 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
|
|
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
|
+
}
|
package/dist/lib/spinner.js
CHANGED
|
@@ -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
|
+
}
|