@orchagent/cli 0.3.85 → 0.3.87

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.
@@ -17,6 +17,7 @@ const dotenv_1 = require("../lib/dotenv");
17
17
  const config_1 = require("../lib/config");
18
18
  const llm_1 = require("../lib/llm");
19
19
  const bundle_1 = require("../lib/bundle");
20
+ const test_mock_runner_1 = require("../lib/test-mock-runner");
20
21
  // ─── Utility functions ───────────────────────────────────────────────────────
21
22
  function validateFixture(data, fixturePath) {
22
23
  const fileName = path_1.default.basename(fixturePath);
@@ -793,6 +794,13 @@ async function executeTests(agentDir, validation, testFiles, verbose, config) {
793
794
  process.stderr.write(chalk_1.default.gray(' "expected_contains": ["response"]\n'));
794
795
  process.stderr.write(chalk_1.default.gray(' }\n\n'));
795
796
  if (validation.executionEngine === 'managed_loop') {
797
+ process.stderr.write('For orchestrators with sub-agents, add mocked fixtures:\n');
798
+ process.stderr.write(chalk_1.default.gray(' # tests/fixture-mock-basic.json\n'));
799
+ process.stderr.write(chalk_1.default.gray(' {\n'));
800
+ process.stderr.write(chalk_1.default.gray(' "input": {"task": "..."},\n'));
801
+ process.stderr.write(chalk_1.default.gray(' "mocks": {"tool_name": {"key": "mock response"}},\n'));
802
+ process.stderr.write(chalk_1.default.gray(' "expected_contains": ["expected"]\n'));
803
+ process.stderr.write(chalk_1.default.gray(' }\n\n'));
796
804
  process.stderr.write('Or test the full agent loop:\n');
797
805
  process.stderr.write(chalk_1.default.gray(` orch run . --local --data '{"task": "..."}'\n\n`));
798
806
  }
@@ -833,8 +841,51 @@ async function executeTests(agentDir, validation, testFiles, verbose, config) {
833
841
  if (code !== 0)
834
842
  exitCode = 1;
835
843
  }
844
+ else if (validation.executionEngine === 'managed_loop') {
845
+ // For managed_loop agents, split fixtures: mocked vs regular
846
+ const mockedFixtures = [];
847
+ const regularFixtures = [];
848
+ for (const fixturePath of testFiles.fixtures) {
849
+ try {
850
+ const raw = await promises_1.default.readFile(fixturePath, 'utf-8');
851
+ const data = JSON.parse(raw);
852
+ if (data.mocks && typeof data.mocks === 'object' && !Array.isArray(data.mocks)) {
853
+ mockedFixtures.push(fixturePath);
854
+ }
855
+ else {
856
+ regularFixtures.push(fixturePath);
857
+ }
858
+ }
859
+ catch {
860
+ regularFixtures.push(fixturePath); // Let downstream validation handle errors
861
+ }
862
+ }
863
+ // Run mocked orchestration tests (full agent loop with mock sub-agents)
864
+ if (mockedFixtures.length > 0) {
865
+ try {
866
+ const manifestPath = path_1.default.join(agentDir, 'orchagent.json');
867
+ const manifestRaw = await promises_1.default.readFile(manifestPath, 'utf-8');
868
+ const manifest = JSON.parse(manifestRaw);
869
+ const code = await (0, test_mock_runner_1.runMockedAgentFixtureTests)(agentDir, mockedFixtures, manifest, verbose, config);
870
+ if (code !== 0)
871
+ exitCode = 1;
872
+ }
873
+ catch (err) {
874
+ if (err instanceof errors_1.CliError)
875
+ throw err;
876
+ process.stderr.write(chalk_1.default.red(` Error running mocked tests: ${err.message}\n`));
877
+ exitCode = 1;
878
+ }
879
+ }
880
+ // Run regular (non-mocked) fixtures as LLM-based prompt tests
881
+ if (regularFixtures.length > 0) {
882
+ const code = await runPromptFixtureTests(agentDir, regularFixtures, verbose, config);
883
+ if (code !== 0)
884
+ exitCode = 1;
885
+ }
886
+ }
836
887
  else {
837
- // Prompt, skill, and managed_loop agents: LLM-based fixture tests
888
+ // Prompt, skill agents: LLM-based fixture tests
838
889
  const code = await runPromptFixtureTests(agentDir, testFiles.fixtures, verbose, config);
839
890
  if (code !== 0)
840
891
  exitCode = 1;
@@ -1040,6 +1091,22 @@ Fixture Format (tests/fixture-basic.json):
1040
1091
  For code_runtime agents, fixtures run your entrypoint with input as stdin.
1041
1092
  For prompt/agent types, fixtures call the LLM with your prompt + input.
1042
1093
 
1094
+ Mocked Orchestration Tests (tests/fixture-mock-*.json):
1095
+ For agent-type (managed_loop) orchestrators, add "mocks" to test the full
1096
+ agent loop with deterministic sub-agent responses:
1097
+ {
1098
+ "description": "Orchestrator handles scan results",
1099
+ "input": {"code": "import os"},
1100
+ "mocks": {
1101
+ "scan_secrets": {"findings": [{"type": "code_injection"}]},
1102
+ "scan_deps": {"vulnerabilities": []}
1103
+ },
1104
+ "expected_contains": ["code_injection"]
1105
+ }
1106
+
1107
+ The LLM runs the full tool-use loop, but custom tool calls return mock
1108
+ responses instead of calling real sub-agents. Great for CI testing.
1109
+
1043
1110
  Run mode (--run):
1044
1111
  Validates the agent, then executes it once with the provided --data.
1045
1112
  Loads .env automatically. Same interface as: orch run . --local --data '...'
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
+ }