@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,311 @@
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.registerTraceCommand = registerTraceCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const config_1 = require("../lib/config");
9
+ const api_1 = require("../lib/api");
10
+ const errors_1 = require("../lib/errors");
11
+ const output_1 = require("../lib/output");
12
+ // ============================================
13
+ // HELPERS
14
+ // ============================================
15
+ async function resolveWorkspaceId(config, slug) {
16
+ const configFile = await (0, config_1.loadConfig)();
17
+ const targetSlug = slug ?? configFile.workspace;
18
+ if (!targetSlug) {
19
+ throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
20
+ }
21
+ const response = await (0, api_1.request)(config, 'GET', '/workspaces');
22
+ const workspace = response.workspaces.find((w) => w.slug === targetSlug);
23
+ if (!workspace) {
24
+ throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
25
+ }
26
+ return workspace.id;
27
+ }
28
+ function isUuid(value) {
29
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
30
+ }
31
+ function isShortUuid(value) {
32
+ return /^[0-9a-f]{7,}$/i.test(value) && !value.includes('/');
33
+ }
34
+ async function resolveShortRunId(config, workspaceId, shortId) {
35
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs?limit=200&run_id_prefix=${encodeURIComponent(shortId)}`);
36
+ if (result.runs.length === 0) {
37
+ throw new errors_1.CliError(`No run found matching '${shortId}'.`);
38
+ }
39
+ if (result.runs.length > 1) {
40
+ throw new errors_1.CliError(`Ambiguous run ID '${shortId}' — matches ${result.runs.length} runs. Use more characters to narrow it down.`);
41
+ }
42
+ return result.runs[0].id;
43
+ }
44
+ function statusColor(status) {
45
+ if (!status)
46
+ return '-';
47
+ switch (status) {
48
+ case 'completed':
49
+ return chalk_1.default.green(status);
50
+ case 'failed':
51
+ return chalk_1.default.red(status);
52
+ case 'running':
53
+ return chalk_1.default.yellow(status);
54
+ case 'timeout':
55
+ return chalk_1.default.red(status);
56
+ default:
57
+ return status;
58
+ }
59
+ }
60
+ function formatDuration(ms) {
61
+ if (ms == null)
62
+ return '-';
63
+ if (ms < 1000)
64
+ return `${ms}ms`;
65
+ if (ms < 60000)
66
+ return `${(ms / 1000).toFixed(1)}s`;
67
+ return `${(ms / 60000).toFixed(1)}m`;
68
+ }
69
+ function formatCost(usd) {
70
+ if (usd == null || usd === 0)
71
+ return '-';
72
+ if (usd < 0.01)
73
+ return `$${usd.toFixed(6)}`;
74
+ return `$${usd.toFixed(4)}`;
75
+ }
76
+ function formatTokens(input, output) {
77
+ const parts = [];
78
+ if (input)
79
+ parts.push(`${input.toLocaleString()} in`);
80
+ if (output)
81
+ parts.push(`${output.toLocaleString()} out`);
82
+ return parts.length > 0 ? parts.join(', ') : '-';
83
+ }
84
+ // ============================================
85
+ // COMMAND REGISTRATION
86
+ // ============================================
87
+ function registerTraceCommand(program) {
88
+ program
89
+ .command('trace <run-id>')
90
+ .description('View the execution trace for a run. Shows LLM calls, tool calls, decisions, and errors in timeline order.')
91
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
92
+ .option('--json', 'Output as JSON')
93
+ .action(async (runId, options) => {
94
+ const config = await (0, config_1.getResolvedConfig)();
95
+ if (!config.apiKey) {
96
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
97
+ }
98
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
99
+ // Resolve short run IDs
100
+ let resolvedRunId = runId;
101
+ if (isUuid(runId)) {
102
+ resolvedRunId = runId;
103
+ }
104
+ else if (isShortUuid(runId)) {
105
+ resolvedRunId = await resolveShortRunId(config, workspaceId, runId);
106
+ }
107
+ else {
108
+ throw new errors_1.CliError(`Invalid run ID '${runId}'. Provide a full UUID or a short hex prefix (7+ characters).`);
109
+ }
110
+ // Fetch run detail for context
111
+ const run = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs/${resolvedRunId}`);
112
+ // Fetch trace header
113
+ let trace;
114
+ try {
115
+ const traceResp = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs/${resolvedRunId}/trace`);
116
+ trace = traceResp.trace;
117
+ }
118
+ catch {
119
+ throw new errors_1.CliError(`No trace available for run ${resolvedRunId.slice(0, 8)}. Traces are captured for cloud runs only.`);
120
+ }
121
+ // Fetch all trace events (paginate if needed)
122
+ const allEvents = [];
123
+ let offset = 0;
124
+ const pageSize = 500;
125
+ while (true) {
126
+ const eventsResp = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/traces/${trace.id}/events?limit=${pageSize}&offset=${offset}`);
127
+ allEvents.push(...eventsResp.events);
128
+ if (eventsResp.next_cursor === null || allEvents.length >= eventsResp.total) {
129
+ break;
130
+ }
131
+ offset = parseInt(eventsResp.next_cursor, 10);
132
+ }
133
+ if (options.json) {
134
+ (0, output_1.printJson)({ run, trace, events: allEvents });
135
+ return;
136
+ }
137
+ renderTrace(run, trace, allEvents);
138
+ });
139
+ }
140
+ // ============================================
141
+ // TRACE RENDERING
142
+ // ============================================
143
+ function renderTrace(run, trace, events) {
144
+ // Header
145
+ process.stdout.write(chalk_1.default.bold(`\nTrace for run ${run.id}\n`) +
146
+ ` Agent: ${run.agent_name ?? '-'}@${run.agent_version ?? '-'}\n` +
147
+ ` Status: ${statusColor(run.status)}\n` +
148
+ ` Duration: ${formatDuration(run.duration_ms)}\n` +
149
+ ` Source: ${run.trigger_source ?? '-'}\n`);
150
+ // Aggregate stats
151
+ const stats = computeStats(events);
152
+ if (stats.totalLlmCalls > 0 || stats.totalToolCalls > 0) {
153
+ process.stdout.write('\n' + chalk_1.default.bold(' Summary\n'));
154
+ if (stats.totalLlmCalls > 0) {
155
+ process.stdout.write(` LLM calls: ${stats.totalLlmCalls}` +
156
+ ` (${formatTokens(stats.totalTokenInput, stats.totalTokenOutput)})` +
157
+ ` cost: ${formatCost(stats.totalCostUsd)}\n`);
158
+ }
159
+ if (stats.totalToolCalls > 0) {
160
+ process.stdout.write(` Tool calls: ${stats.totalToolCalls}\n`);
161
+ }
162
+ if (stats.providers.length > 0) {
163
+ process.stdout.write(` Providers: ${stats.providers.join(', ')}\n`);
164
+ }
165
+ if (stats.totalErrors > 0) {
166
+ process.stdout.write(` Errors: ${chalk_1.default.red(String(stats.totalErrors))}\n`);
167
+ }
168
+ }
169
+ if (events.length === 0) {
170
+ process.stdout.write(chalk_1.default.gray('\n No trace events recorded.\n\n'));
171
+ return;
172
+ }
173
+ // Timeline
174
+ process.stdout.write('\n' + chalk_1.default.bold(' Timeline\n'));
175
+ for (const event of events) {
176
+ renderEvent(event);
177
+ }
178
+ process.stdout.write('\n');
179
+ // Footer hint
180
+ process.stdout.write(chalk_1.default.gray(`View logs: orch logs ${run.id.slice(0, 8)}\n`));
181
+ process.stdout.write(chalk_1.default.gray(`Replay: orch replay ${run.id.slice(0, 8)}\n`));
182
+ }
183
+ function computeStats(events) {
184
+ const stats = {
185
+ totalLlmCalls: 0,
186
+ totalToolCalls: 0,
187
+ totalErrors: 0,
188
+ totalTokenInput: 0,
189
+ totalTokenOutput: 0,
190
+ totalCostUsd: 0,
191
+ providers: [],
192
+ };
193
+ const providerSet = new Set();
194
+ for (const e of events) {
195
+ if (e.event_type === 'llm_call_succeeded') {
196
+ stats.totalLlmCalls++;
197
+ stats.totalTokenInput += e.token_input ?? 0;
198
+ stats.totalTokenOutput += e.token_output ?? 0;
199
+ stats.totalCostUsd += e.cost_usd ?? 0;
200
+ if (e.provider)
201
+ providerSet.add(e.provider);
202
+ }
203
+ else if (e.event_type === 'llm_call_failed') {
204
+ stats.totalLlmCalls++;
205
+ stats.totalErrors++;
206
+ if (e.provider)
207
+ providerSet.add(e.provider);
208
+ }
209
+ else if (e.event_type === 'tool_call_succeeded') {
210
+ stats.totalToolCalls++;
211
+ }
212
+ else if (e.event_type === 'tool_call_failed') {
213
+ stats.totalToolCalls++;
214
+ stats.totalErrors++;
215
+ }
216
+ else if (e.event_type === 'error') {
217
+ stats.totalErrors++;
218
+ }
219
+ else if (e.event_type === 'policy_violation') {
220
+ stats.totalErrors++;
221
+ }
222
+ }
223
+ stats.providers = Array.from(providerSet);
224
+ return stats;
225
+ }
226
+ function renderEvent(event) {
227
+ const seq = chalk_1.default.gray(` #${String(event.sequence_no).padStart(2, ' ')} `);
228
+ switch (event.event_type) {
229
+ case 'llm_call_started': {
230
+ const provider = event.provider ?? '?';
231
+ const model = event.model ?? '?';
232
+ process.stdout.write(seq + chalk_1.default.blue('LLM ') + chalk_1.default.gray(`${provider}/${model} started\n`));
233
+ break;
234
+ }
235
+ case 'llm_call_succeeded': {
236
+ const provider = event.provider ?? '?';
237
+ const model = event.model ?? '?';
238
+ const tokens = formatTokens(event.token_input, event.token_output);
239
+ const cost = formatCost(event.cost_usd);
240
+ const dur = formatDuration(event.duration_ms);
241
+ process.stdout.write(seq + chalk_1.default.green('LLM ') +
242
+ `${provider}/${model}` +
243
+ chalk_1.default.gray(` | ${tokens} | ${cost} | ${dur}`) + '\n');
244
+ break;
245
+ }
246
+ case 'llm_call_failed': {
247
+ const provider = event.provider ?? '?';
248
+ const model = event.model ?? '?';
249
+ const errMsg = event.error_message || event.error_type || 'unknown error';
250
+ const dur = formatDuration(event.duration_ms);
251
+ process.stdout.write(seq + chalk_1.default.red('LLM ') +
252
+ `${provider}/${model} ` +
253
+ chalk_1.default.red(errMsg) +
254
+ chalk_1.default.gray(` | ${dur}`) + '\n');
255
+ break;
256
+ }
257
+ case 'tool_call_started': {
258
+ const toolName = event.payload?.tool_name || '?';
259
+ process.stdout.write(seq + chalk_1.default.cyan('TOOL ') + chalk_1.default.gray(`${toolName} started\n`));
260
+ break;
261
+ }
262
+ case 'tool_call_succeeded': {
263
+ const toolName = event.payload?.tool_name || '?';
264
+ const dur = formatDuration(event.duration_ms);
265
+ process.stdout.write(seq + chalk_1.default.green('TOOL ') + `${toolName}` +
266
+ chalk_1.default.gray(` | ${dur}`) + '\n');
267
+ break;
268
+ }
269
+ case 'tool_call_failed': {
270
+ const toolName = event.payload?.tool_name || '?';
271
+ const errMsg = event.error_message || event.error_type || 'unknown error';
272
+ const dur = formatDuration(event.duration_ms);
273
+ process.stdout.write(seq + chalk_1.default.red('TOOL ') + `${toolName} ` +
274
+ chalk_1.default.red(errMsg) +
275
+ chalk_1.default.gray(` | ${dur}`) + '\n');
276
+ break;
277
+ }
278
+ case 'decision': {
279
+ const desc = event.payload?.description || 'decision';
280
+ process.stdout.write(seq + chalk_1.default.magenta('DECIDE ') + chalk_1.default.gray(desc) + '\n');
281
+ break;
282
+ }
283
+ case 'fallback_transition': {
284
+ const from = event.payload?.from_provider || '?';
285
+ const to = event.payload?.to_provider || '?';
286
+ const reason = event.payload?.reason || '';
287
+ process.stdout.write(seq + chalk_1.default.yellow('FALLBACK ') +
288
+ `${from} -> ${to}` +
289
+ (reason ? chalk_1.default.gray(` (${reason})`) : '') + '\n');
290
+ break;
291
+ }
292
+ case 'policy_violation': {
293
+ const vType = event.payload?.violation_type || 'violation';
294
+ const detail = event.payload?.detail || '';
295
+ process.stdout.write(seq + chalk_1.default.red('POLICY ') + chalk_1.default.red(vType) +
296
+ (detail ? chalk_1.default.gray(` — ${detail}`) : '') + '\n');
297
+ break;
298
+ }
299
+ case 'error': {
300
+ const errType = event.error_type || 'error';
301
+ const errMsg = event.error_message || '';
302
+ process.stdout.write(seq + chalk_1.default.red('ERROR ') + chalk_1.default.red(errType) +
303
+ (errMsg ? chalk_1.default.gray(` — ${errMsg}`) : '') + '\n');
304
+ break;
305
+ }
306
+ default: {
307
+ // Unknown event type — show raw
308
+ process.stdout.write(seq + chalk_1.default.gray(event.event_type) + '\n');
309
+ }
310
+ }
311
+ }
package/dist/lib/api.js CHANGED
@@ -44,6 +44,7 @@ exports.publicRequest = publicRequest;
44
44
  exports.getOrg = getOrg;
45
45
  exports.updateOrg = updateOrg;
46
46
  exports.getPublicAgent = getPublicAgent;
47
+ exports.getAgentCostEstimate = getAgentCostEstimate;
47
48
  exports.listMyAgents = listMyAgents;
48
49
  exports.createAgent = createAgent;
49
50
  exports.downloadCodeBundle = downloadCodeBundle;
@@ -58,6 +59,7 @@ exports.forkAgent = forkAgent;
58
59
  exports.checkAgentTransfer = checkAgentTransfer;
59
60
  exports.transferAgent = transferAgent;
60
61
  exports.previewAgentVersion = previewAgentVersion;
62
+ exports.validateAgentPublish = validateAgentPublish;
61
63
  exports.reportInstall = reportInstall;
62
64
  exports.fetchUserProfile = fetchUserProfile;
63
65
  exports.listEnvironments = listEnvironments;
@@ -288,6 +290,9 @@ async function updateOrg(config, payload) {
288
290
  async function getPublicAgent(config, org, agent, version) {
289
291
  return publicRequest(config, `/public/agents/${org}/${agent}/${version}`);
290
292
  }
293
+ async function getAgentCostEstimate(config, org, agent, version) {
294
+ return publicRequest(config, `/public/agents/${org}/${agent}/${version}/cost-estimate`);
295
+ }
291
296
  async function listMyAgents(config, workspaceId) {
292
297
  const headers = {};
293
298
  if (workspaceId)
@@ -430,15 +435,21 @@ async function downloadCodeBundleAuthenticated(config, agentId) {
430
435
  /**
431
436
  * Check if an agent requires confirmation for deletion.
432
437
  */
433
- async function checkAgentDelete(config, agentId) {
434
- return request(config, 'GET', `/agents/${agentId}/delete-check`);
438
+ async function checkAgentDelete(config, agentId, workspaceId) {
439
+ const headers = {};
440
+ if (workspaceId)
441
+ headers['X-Workspace-Id'] = workspaceId;
442
+ return request(config, 'GET', `/agents/${agentId}/delete-check`, { headers });
435
443
  }
436
444
  /**
437
445
  * Soft delete an agent.
438
446
  */
439
- async function deleteAgent(config, agentId, confirmationName) {
447
+ async function deleteAgent(config, agentId, confirmationName, workspaceId) {
440
448
  const params = confirmationName ? `?confirmation_name=${encodeURIComponent(confirmationName)}` : '';
441
- return request(config, 'DELETE', `/agents/${agentId}${params}`);
449
+ const headers = {};
450
+ if (workspaceId)
451
+ headers['X-Workspace-Id'] = workspaceId;
452
+ return request(config, 'DELETE', `/agents/${agentId}${params}`, { headers });
442
453
  }
443
454
  /**
444
455
  * Fork a public agent into the caller's workspace (or an explicit workspace_id).
@@ -473,6 +484,20 @@ async function previewAgentVersion(config, agentName, workspaceId) {
473
484
  headers['X-Workspace-Id'] = workspaceId;
474
485
  return request(config, 'GET', `/agents/preview?name=${encodeURIComponent(agentName)}`, { headers });
475
486
  }
487
+ /**
488
+ * Validate an agent publish payload without actually creating the agent.
489
+ * Runs server-side validation (name, tier limits, manifest, dependencies, etc.)
490
+ * and returns a validation report. Used by --dry-run.
491
+ */
492
+ async function validateAgentPublish(config, data, workspaceId) {
493
+ const headers = { 'Content-Type': 'application/json' };
494
+ if (workspaceId)
495
+ headers['X-Workspace-Id'] = workspaceId;
496
+ return request(config, 'POST', '/agents/validate', {
497
+ body: JSON.stringify(data),
498
+ headers,
499
+ });
500
+ }
476
501
  /**
477
502
  * Report a skill installation to the backend.
478
503
  * Only tracks authenticated installs (requires API key).
@@ -0,0 +1,223 @@
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.discoverAgents = discoverAgents;
7
+ exports.topoSort = topoSort;
8
+ exports.formatPublishPlan = formatPublishPlan;
9
+ const promises_1 = __importDefault(require("fs/promises"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ /**
13
+ * Scan immediate subdirectories for orchagent.json or SKILL.md files.
14
+ * Does NOT recurse deeper than one level (matching GitHub App import behavior).
15
+ */
16
+ async function discoverAgents(rootDir) {
17
+ const agents = [];
18
+ let entries;
19
+ try {
20
+ entries = await promises_1.default.readdir(rootDir, { withFileTypes: true });
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ // Also check the root directory itself
26
+ const dirsToCheck = [];
27
+ for (const entry of entries) {
28
+ if (!entry.isDirectory())
29
+ continue;
30
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '__pycache__')
31
+ continue;
32
+ dirsToCheck.push({
33
+ dir: path_1.default.join(rootDir, entry.name),
34
+ dirName: entry.name,
35
+ });
36
+ }
37
+ for (const { dir, dirName } of dirsToCheck) {
38
+ const agent = await tryParseAgentDir(dir, dirName);
39
+ if (agent)
40
+ agents.push(agent);
41
+ }
42
+ return agents;
43
+ }
44
+ /**
45
+ * Try to parse a directory as an agent. Returns null if no orchagent.json or SKILL.md found.
46
+ */
47
+ async function tryParseAgentDir(dir, dirName) {
48
+ // Check for SKILL.md first (takes precedence, matching publish.ts behavior)
49
+ const skillMdPath = path_1.default.join(dir, 'SKILL.md');
50
+ try {
51
+ const content = await promises_1.default.readFile(skillMdPath, 'utf-8');
52
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
53
+ if (match) {
54
+ // Parse YAML frontmatter to get name
55
+ const yamlContent = match[1];
56
+ const nameMatch = yamlContent.match(/^name:\s*(.+)$/m);
57
+ const name = nameMatch ? nameMatch[1].trim().replace(/^['"]|['"]$/g, '') : dirName;
58
+ return {
59
+ dir,
60
+ dirName,
61
+ name,
62
+ isSkill: true,
63
+ dependencyRefs: [], // Skills have no dependencies
64
+ };
65
+ }
66
+ }
67
+ catch {
68
+ // No SKILL.md, try orchagent.json
69
+ }
70
+ // Check for orchagent.json
71
+ const manifestPath = path_1.default.join(dir, 'orchagent.json');
72
+ try {
73
+ const raw = await promises_1.default.readFile(manifestPath, 'utf-8');
74
+ const manifest = JSON.parse(raw);
75
+ if (!manifest.name)
76
+ return null;
77
+ // Extract dependency refs from manifest.manifest.dependencies
78
+ const dependencyRefs = [];
79
+ const deps = manifest.manifest?.dependencies;
80
+ if (deps && Array.isArray(deps)) {
81
+ for (const dep of deps) {
82
+ if (dep.id && typeof dep.id === 'string') {
83
+ dependencyRefs.push(dep.id);
84
+ }
85
+ }
86
+ }
87
+ // Also extract from custom_tools that reference other agents via orch_call
88
+ // Pattern: orch_call.py org/agent@version or orch_call.js org/agent@version
89
+ const loopTools = manifest.loop?.custom_tools || manifest.custom_tools;
90
+ if (Array.isArray(loopTools)) {
91
+ for (const tool of loopTools) {
92
+ if (tool.command) {
93
+ const orchCallMatch = tool.command.match(/orch_call(?:\.py|\.js)?\s+([a-z0-9-]+\/[a-z0-9-]+)/);
94
+ if (orchCallMatch) {
95
+ const ref = orchCallMatch[1];
96
+ if (!dependencyRefs.includes(ref)) {
97
+ dependencyRefs.push(ref);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ return {
104
+ dir,
105
+ dirName,
106
+ name: manifest.name,
107
+ isSkill: false,
108
+ dependencyRefs,
109
+ };
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
115
+ /**
116
+ * Topologically sort agents so dependencies are published first (leaf-first).
117
+ *
118
+ * Only considers intra-project dependencies (agents whose dependency refs
119
+ * match another discovered agent's name). Cross-org or external deps are
120
+ * ignored since they must already be published.
121
+ */
122
+ function topoSort(agents) {
123
+ // Build name → agent lookup (using agent name, not dir name)
124
+ const byName = new Map();
125
+ for (const agent of agents) {
126
+ byName.set(agent.name, agent);
127
+ }
128
+ // Build adjacency: agent → set of local agents it depends on
129
+ // "depends on" means: must be published BEFORE this agent
130
+ const adjList = new Map();
131
+ for (const agent of agents) {
132
+ const localDeps = new Set();
133
+ for (const ref of agent.dependencyRefs) {
134
+ // ref is "org/name" — extract just the name part for local matching
135
+ const depName = ref.includes('/') ? ref.split('/')[1] : ref;
136
+ if (byName.has(depName) && depName !== agent.name) {
137
+ localDeps.add(depName);
138
+ }
139
+ }
140
+ adjList.set(agent.name, localDeps);
141
+ }
142
+ // Kahn's algorithm for topological sort
143
+ const inDegree = new Map();
144
+ for (const agent of agents) {
145
+ inDegree.set(agent.name, 0);
146
+ }
147
+ for (const [, deps] of adjList) {
148
+ for (const dep of deps) {
149
+ inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
150
+ }
151
+ }
152
+ // Note: inDegree counts how many agents depend ON this agent.
153
+ // We want to publish agents with no dependents-that-haven't-been-published first.
154
+ // Actually, let me re-think: we want leaf-first ordering.
155
+ // A "leaf" is an agent with NO dependencies (inDegree in the "depends-on" graph = 0 outgoing).
156
+ // Let's use Kahn's on the reverse: process nodes whose dependencies are all satisfied.
157
+ // Reset: inDegree = number of local deps this agent has (not yet satisfied)
158
+ const remaining = new Map();
159
+ for (const agent of agents) {
160
+ remaining.set(agent.name, adjList.get(agent.name)?.size || 0);
161
+ }
162
+ const queue = [];
163
+ for (const [name, count] of remaining) {
164
+ if (count === 0)
165
+ queue.push(name);
166
+ }
167
+ // Sort queue alphabetically for deterministic ordering among peers
168
+ queue.sort();
169
+ const sorted = [];
170
+ const visited = new Set();
171
+ while (queue.length > 0) {
172
+ const name = queue.shift();
173
+ if (visited.has(name))
174
+ continue;
175
+ visited.add(name);
176
+ const agent = byName.get(name);
177
+ sorted.push(agent);
178
+ // For each agent that depends on this one, decrement their remaining count
179
+ for (const [otherName, deps] of adjList) {
180
+ if (deps.has(name) && !visited.has(otherName)) {
181
+ const newCount = (remaining.get(otherName) || 1) - 1;
182
+ remaining.set(otherName, newCount);
183
+ if (newCount === 0) {
184
+ // Insert in sorted position for determinism
185
+ const insertIdx = queue.findIndex(q => q > otherName);
186
+ if (insertIdx === -1)
187
+ queue.push(otherName);
188
+ else
189
+ queue.splice(insertIdx, 0, otherName);
190
+ }
191
+ }
192
+ }
193
+ }
194
+ // If not all agents were visited, there's a cycle
195
+ if (sorted.length < agents.length) {
196
+ const inCycle = agents
197
+ .filter(a => !visited.has(a.name))
198
+ .map(a => a.name);
199
+ return { ok: false, cycle: inCycle };
200
+ }
201
+ return { ok: true, sorted };
202
+ }
203
+ /**
204
+ * Format the publish plan for display (used by both dry-run and normal mode).
205
+ */
206
+ function formatPublishPlan(sorted, orgSlug) {
207
+ const lines = [];
208
+ lines.push('');
209
+ lines.push(` Found ${sorted.length} agent${sorted.length === 1 ? '' : 's'} to publish:`);
210
+ lines.push('');
211
+ for (let i = 0; i < sorted.length; i++) {
212
+ const agent = sorted[i];
213
+ const type = agent.isSkill ? 'skill' : 'agent';
214
+ const deps = agent.dependencyRefs.length > 0
215
+ ? ` (depends on: ${agent.dependencyRefs.map(r => r.split('/').pop()).join(', ')})`
216
+ : '';
217
+ const prefix = orgSlug ? `${orgSlug}/` : '';
218
+ lines.push(` ${i + 1}. ${prefix}${agent.name} [${type}]${deps}`);
219
+ lines.push(` ${chalk_1.default.gray(agent.dirName + '/')}`);
220
+ }
221
+ lines.push('');
222
+ return lines.join('\n');
223
+ }