@orchagent/cli 0.2.8 → 0.2.10

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.
@@ -128,7 +128,7 @@ Note: Use 'call' for server-side execution (requires login), 'run' for local exe
128
128
  if (!org) {
129
129
  throw new errors_1.CliError('Missing org. Use org/agent or set default org.');
130
130
  }
131
- const agentMeta = await (0, api_1.getPublicAgent)(resolved, org, parsed.agent, parsed.version);
131
+ const agentMeta = await (0, api_1.getAgentWithFallback)(resolved, org, parsed.agent, parsed.version);
132
132
  const endpoint = options.endpoint?.trim() || agentMeta.default_endpoint || 'analyze';
133
133
  const headers = {
134
134
  Authorization: `Bearer ${resolved.apiKey}`,
@@ -54,6 +54,57 @@ async function fetchReadme(url) {
54
54
  return null;
55
55
  }
56
56
  }
57
+ async function downloadAgentWithFallback(config, org, agent, version) {
58
+ // Try public endpoint first
59
+ try {
60
+ return await (0, api_1.publicRequest)(config, `/public/agents/${org}/${agent}/${version}/download`);
61
+ }
62
+ catch (err) {
63
+ if (!(err instanceof api_1.ApiError) || err.status !== 404)
64
+ throw err;
65
+ }
66
+ // Fallback to authenticated endpoint for private agents
67
+ if (!config.apiKey) {
68
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
69
+ }
70
+ const userOrg = await (0, api_1.getOrg)(config);
71
+ if (userOrg.slug !== org) {
72
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
73
+ }
74
+ // Find agent in user's list and construct download data
75
+ const agents = await (0, api_1.listMyAgents)(config);
76
+ const matching = agents.filter(a => a.name === agent);
77
+ if (matching.length === 0) {
78
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
79
+ }
80
+ let targetAgent = matching[0];
81
+ if (version !== 'latest') {
82
+ const found = matching.find(a => a.version === version);
83
+ if (!found) {
84
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
85
+ }
86
+ targetAgent = found;
87
+ }
88
+ else {
89
+ // Get most recent
90
+ targetAgent = matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
91
+ }
92
+ // Convert Agent to AgentDownload format
93
+ return {
94
+ type: targetAgent.type,
95
+ name: targetAgent.name,
96
+ version: targetAgent.version,
97
+ description: targetAgent.description,
98
+ prompt: targetAgent.prompt,
99
+ input_schema: targetAgent.input_schema,
100
+ output_schema: targetAgent.output_schema,
101
+ supported_providers: targetAgent.supported_providers || ['any'],
102
+ source_url: targetAgent.source_url,
103
+ run_command: targetAgent.run_command,
104
+ url: targetAgent.url,
105
+ sdk_compatible: false,
106
+ };
107
+ }
57
108
  function registerInfoCommand(program) {
58
109
  program
59
110
  .command('info <agent>')
@@ -63,7 +114,7 @@ function registerInfoCommand(program) {
63
114
  const config = await (0, config_1.getResolvedConfig)();
64
115
  const { org, agent, version } = parseAgentRef(agentArg);
65
116
  // Fetch agent metadata
66
- const agentData = await (0, api_1.publicRequest)(config, `/public/agents/${org}/${agent}/${version}/download`);
117
+ const agentData = await downloadAgentWithFallback(config, org, agent, version);
67
118
  if (options.json) {
68
119
  // Don't expose internal routing URLs in JSON output
69
120
  const output = { ...agentData };
@@ -93,6 +144,10 @@ function registerInfoCommand(program) {
93
144
  if (agentData.run_command) {
94
145
  process.stdout.write(`Run: ${agentData.run_command}\n`);
95
146
  }
147
+ // Display SDK/Local Ready status
148
+ if (agentData.sdk_compatible) {
149
+ process.stdout.write(`Local Ready: yes (uses orchagent-sdk)\n`);
150
+ }
96
151
  }
97
152
  // Display input schema if available
98
153
  if (agentData.input_schema?.properties && Object.keys(agentData.input_schema.properties).length > 0) {
@@ -46,15 +46,38 @@ const SCHEMA_TEMPLATE = `{
46
46
  }
47
47
  }
48
48
  `;
49
+ const SKILL_TEMPLATE = `---
50
+ name: my-skill
51
+ description: When to use this skill
52
+ license: MIT
53
+ ---
54
+
55
+ # My Skill
56
+
57
+ Instructions and guidance for AI agents...
58
+ `;
49
59
  function registerInitCommand(program) {
50
60
  program
51
61
  .command('init')
52
62
  .description('Initialize a new agent project')
53
63
  .argument('[name]', 'Agent name (default: current directory name)')
54
- .option('--type <type>', 'Agent type: prompt or code (default: prompt)', 'prompt')
64
+ .option('--type <type>', 'Type: prompt, code, or skill (default: prompt)', 'prompt')
55
65
  .action(async (name, options) => {
56
66
  const cwd = process.cwd();
57
67
  const agentName = name || path_1.default.basename(cwd);
68
+ // Handle skill type separately
69
+ if (options.type === 'skill') {
70
+ const skillPath = path_1.default.join(cwd, 'SKILL.md');
71
+ const skillContent = SKILL_TEMPLATE.replace('my-skill', agentName);
72
+ await promises_1.default.writeFile(skillPath, skillContent);
73
+ process.stdout.write(`Initialized skill "${agentName}" in ${cwd}\n`);
74
+ process.stdout.write(`\nFiles created:\n`);
75
+ process.stdout.write(` SKILL.md - Skill content with frontmatter\n`);
76
+ process.stdout.write(`\nNext steps:\n`);
77
+ process.stdout.write(` 1. Edit SKILL.md with your skill content\n`);
78
+ process.stdout.write(` 2. Run: orchagent publish\n`);
79
+ return;
80
+ }
58
81
  const manifestPath = path_1.default.join(cwd, 'orchagent.json');
59
82
  const promptPath = path_1.default.join(cwd, 'prompt.md');
60
83
  const schemaPath = path_1.default.join(cwd, 'schema.json');
@@ -71,7 +94,7 @@ function registerInitCommand(program) {
71
94
  // Create manifest
72
95
  const manifest = JSON.parse(MANIFEST_TEMPLATE);
73
96
  manifest.name = agentName;
74
- manifest.type = options.type === 'code' ? 'code' : 'prompt';
97
+ manifest.type = ['code', 'skill'].includes(options.type) ? options.type : 'prompt';
75
98
  await promises_1.default.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
76
99
  // Create prompt template (for prompt-based agents)
77
100
  if (options.type !== 'code') {
@@ -13,6 +13,37 @@ const api_1 = require("../lib/api");
13
13
  const errors_1 = require("../lib/errors");
14
14
  const analytics_1 = require("../lib/analytics");
15
15
  const bundle_1 = require("../lib/bundle");
16
+ /**
17
+ * Check if orchagent-sdk is listed in requirements.txt or pyproject.toml
18
+ */
19
+ async function detectSdkCompatible(agentDir) {
20
+ // Check requirements.txt
21
+ const requirementsPath = path_1.default.join(agentDir, 'requirements.txt');
22
+ try {
23
+ const content = await promises_1.default.readFile(requirementsPath, 'utf-8');
24
+ // Match orchagent-sdk with or without version specifier (e.g., orchagent-sdk, orchagent-sdk>=0.1.0)
25
+ if (/^orchagent-sdk\b/m.test(content)) {
26
+ return true;
27
+ }
28
+ }
29
+ catch {
30
+ // File doesn't exist, continue checking pyproject.toml
31
+ }
32
+ // Check pyproject.toml
33
+ const pyprojectPath = path_1.default.join(agentDir, 'pyproject.toml');
34
+ try {
35
+ const content = await promises_1.default.readFile(pyprojectPath, 'utf-8');
36
+ // Match orchagent-sdk in dependencies (various formats in TOML)
37
+ // e.g., "orchagent-sdk", 'orchagent-sdk>=0.1.0', orchagent-sdk = ">=0.1.0"
38
+ if (/["']?orchagent-sdk["']?\s*[=<>~!]?/m.test(content)) {
39
+ return true;
40
+ }
41
+ }
42
+ catch {
43
+ // File doesn't exist
44
+ }
45
+ return false;
46
+ }
16
47
  async function parseSkillMd(filePath) {
17
48
  try {
18
49
  const content = await promises_1.default.readFile(filePath, 'utf-8');
@@ -32,7 +63,7 @@ async function parseSkillMd(filePath) {
32
63
  function registerPublishCommand(program) {
33
64
  program
34
65
  .command('publish')
35
- .description('Publish agent from local files')
66
+ .description('Publish agent or skill from local files')
36
67
  .option('--url <url>', 'Agent URL (for code-based agents)')
37
68
  .option('--public', 'Make agent public (default: true)', true)
38
69
  .option('--private', 'Make agent private')
@@ -77,16 +108,17 @@ function registerPublishCommand(program) {
77
108
  if (!manifest.name) {
78
109
  throw new errors_1.CliError('orchagent.json must have name');
79
110
  }
80
- // Read prompt (for prompt-based agents)
111
+ // Read prompt (for prompt-based agents and skills)
81
112
  let prompt;
82
- if (manifest.type === 'prompt') {
113
+ if (manifest.type === 'prompt' || manifest.type === 'skill') {
83
114
  const promptPath = path_1.default.join(cwd, 'prompt.md');
84
115
  try {
85
116
  prompt = await promises_1.default.readFile(promptPath, 'utf-8');
86
117
  }
87
118
  catch (err) {
88
119
  if (err.code === 'ENOENT') {
89
- throw new errors_1.CliError('No prompt.md found for prompt-based agent');
120
+ const agentTypeName = manifest.type === 'skill' ? 'skill' : 'prompt-based agent';
121
+ throw new errors_1.CliError(`No prompt.md found for ${agentTypeName}`);
90
122
  }
91
123
  throw err;
92
124
  }
@@ -128,6 +160,14 @@ function registerPublishCommand(program) {
128
160
  const org = await (0, api_1.getOrg)(config);
129
161
  // Default to 'any' provider if not specified
130
162
  const supportedProviders = manifest.supported_providers || ['any'];
163
+ // Detect SDK compatibility for code agents
164
+ let sdkCompatible = false;
165
+ if (manifest.type === 'code') {
166
+ sdkCompatible = await detectSdkCompatible(cwd);
167
+ if (sdkCompatible) {
168
+ process.stdout.write(`SDK detected - agent will be marked as Local Ready\n`);
169
+ }
170
+ }
131
171
  // Create the agent (server auto-assigns version)
132
172
  const result = await (0, api_1.createAgent)(config, {
133
173
  name: manifest.name,
@@ -145,6 +185,10 @@ function registerPublishCommand(program) {
145
185
  source_url: manifest.source_url,
146
186
  pip_package: manifest.pip_package,
147
187
  run_command: manifest.run_command,
188
+ // SDK compatibility flag
189
+ sdk_compatible: sdkCompatible || undefined,
190
+ // Orchestration manifest (includes dependencies)
191
+ manifest: manifest.manifest,
148
192
  });
149
193
  const assignedVersion = result.agent?.version || 'v1';
150
194
  const agentId = result.agent?.id;
@@ -165,9 +209,9 @@ function registerPublishCommand(program) {
165
209
  if (!validation.valid) {
166
210
  throw new errors_1.CliError(`Bundle validation failed: ${validation.error}`);
167
211
  }
168
- // Upload the bundle
212
+ // Upload the bundle with entrypoint
169
213
  process.stdout.write(` Uploading bundle...\n`);
170
- const uploadResult = await (0, api_1.uploadCodeBundle)(config, agentId, bundlePath);
214
+ const uploadResult = await (0, api_1.uploadCodeBundle)(config, agentId, bundlePath, manifest.entrypoint);
171
215
  process.stdout.write(` Uploaded: ${uploadResult.code_hash.substring(0, 12)}...\n`);
172
216
  }
173
217
  finally {
@@ -48,6 +48,13 @@ const output_1 = require("../lib/output");
48
48
  const llm_1 = require("../lib/llm");
49
49
  const DEFAULT_VERSION = 'latest';
50
50
  const AGENTS_DIR = path_1.default.join(os_1.default.homedir(), '.orchagent', 'agents');
51
+ // Local execution environment variables
52
+ const LOCAL_EXECUTION_ENV = 'ORCHAGENT_LOCAL_EXECUTION';
53
+ const AGENTS_DIR_ENV = 'ORCHAGENT_AGENTS_DIR';
54
+ const CALL_CHAIN_ENV = 'ORCHAGENT_CALL_CHAIN';
55
+ const DEADLINE_MS_ENV = 'ORCHAGENT_DEADLINE_MS';
56
+ const MAX_HOPS_ENV = 'ORCHAGENT_MAX_HOPS';
57
+ const DOWNSTREAM_REMAINING_ENV = 'ORCHAGENT_DOWNSTREAM_REMAINING';
51
58
  function parseAgentRef(value) {
52
59
  const [ref, versionPart] = value.split('@');
53
60
  const version = versionPart?.trim() || DEFAULT_VERSION;
@@ -61,8 +68,73 @@ function parseAgentRef(value) {
61
68
  throw new errors_1.CliError('Invalid agent reference. Use org/agent or agent format.');
62
69
  }
63
70
  async function downloadAgent(config, org, agent, version) {
64
- // Public endpoint - no auth required
65
- return (0, api_1.publicRequest)(config, `/public/agents/${org}/${agent}/${version}/download`);
71
+ // Try public endpoint first
72
+ try {
73
+ return await (0, api_1.publicRequest)(config, `/public/agents/${org}/${agent}/${version}/download`);
74
+ }
75
+ catch (err) {
76
+ if (!(err instanceof api_1.ApiError) || err.status !== 404)
77
+ throw err;
78
+ }
79
+ // Fallback to authenticated endpoint for private agents
80
+ if (!config.apiKey) {
81
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
82
+ }
83
+ const userOrg = await (0, api_1.getOrg)(config);
84
+ if (userOrg.slug !== org) {
85
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
86
+ }
87
+ // Find agent in user's list
88
+ const agents = await (0, api_1.listMyAgents)(config);
89
+ const matching = agents.filter(a => a.name === agent);
90
+ if (matching.length === 0) {
91
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
92
+ }
93
+ let targetAgent = matching[0];
94
+ if (version !== 'latest') {
95
+ const found = matching.find(a => a.version === version);
96
+ if (!found) {
97
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
98
+ }
99
+ targetAgent = found;
100
+ }
101
+ else {
102
+ targetAgent = matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
103
+ }
104
+ // Convert Agent to AgentDownload format
105
+ return {
106
+ id: targetAgent.id,
107
+ type: targetAgent.type,
108
+ name: targetAgent.name,
109
+ version: targetAgent.version,
110
+ description: targetAgent.description,
111
+ prompt: targetAgent.prompt,
112
+ input_schema: targetAgent.input_schema,
113
+ output_schema: targetAgent.output_schema,
114
+ supported_providers: targetAgent.supported_providers || ['any'],
115
+ default_models: targetAgent.default_models,
116
+ source_url: targetAgent.source_url,
117
+ pip_package: targetAgent.pip_package,
118
+ run_command: targetAgent.run_command,
119
+ url: targetAgent.url,
120
+ has_bundle: !!targetAgent.code_bundle_url,
121
+ entrypoint: targetAgent.entrypoint,
122
+ };
123
+ }
124
+ async function downloadBundleWithFallback(config, org, agentName, version, agentId) {
125
+ // Try public endpoint first
126
+ try {
127
+ return await (0, api_1.downloadCodeBundle)(config, org, agentName, version);
128
+ }
129
+ catch (err) {
130
+ if (!(err instanceof api_1.ApiError) || err.status !== 404)
131
+ throw err;
132
+ }
133
+ // Fallback to authenticated endpoint
134
+ if (!config.apiKey || !agentId) {
135
+ throw new api_1.ApiError(`Bundle for '${org}/${agentName}@${version}' not found`, 404);
136
+ }
137
+ return await (0, api_1.downloadCodeBundleAuthenticated)(config, agentId);
66
138
  }
67
139
  async function checkDependencies(config, dependencies) {
68
140
  const results = [];
@@ -70,7 +142,7 @@ async function checkDependencies(config, dependencies) {
70
142
  const [org, agent] = dep.id.split('/');
71
143
  try {
72
144
  const agentData = await downloadAgent(config, org, agent, dep.version);
73
- const downloadable = !!(agentData.source_url || agentData.pip_package || agentData.type === 'prompt');
145
+ const downloadable = !!(agentData.source_url || agentData.pip_package || agentData.has_bundle || agentData.type === 'prompt');
74
146
  results.push({ dep, downloadable, agentData });
75
147
  }
76
148
  catch {
@@ -117,6 +189,12 @@ async function promptUserForDeps(depStatuses) {
117
189
  });
118
190
  });
119
191
  }
192
+ async function downloadSkillDependency(config, ref, defaultOrg) {
193
+ const parsed = parseSkillRef(ref);
194
+ const org = parsed.org ?? defaultOrg;
195
+ const skillData = await (0, api_1.publicRequest)(config, `/public/agents/${org}/${parsed.skill}/${parsed.version}/download`);
196
+ await saveAgentLocally(org, parsed.skill, skillData);
197
+ }
120
198
  async function downloadDependenciesRecursively(config, depStatuses, visited = new Set()) {
121
199
  for (const status of depStatuses) {
122
200
  if (!status.downloadable || !status.agentData)
@@ -127,12 +205,27 @@ async function downloadDependenciesRecursively(config, depStatuses, visited = ne
127
205
  visited.add(depRef);
128
206
  const [org, agent] = status.dep.id.split('/');
129
207
  process.stderr.write(`\nDownloading dependency: ${depRef}...\n`);
130
- // Save the dependency locally
208
+ // Save the dependency metadata locally
131
209
  await saveAgentLocally(org, agent, status.agentData);
132
- // Install if it's a code agent
210
+ // For bundle-based agents, also extract the bundle
211
+ if (status.agentData.has_bundle) {
212
+ await saveBundleLocally(config, org, agent, status.dep.version, status.agentData.id);
213
+ }
214
+ // Install if it's a pip/source code agent
133
215
  if (status.agentData.type === 'code' && (status.agentData.source_url || status.agentData.pip_package)) {
134
216
  await installCodeAgent(status.agentData);
135
217
  }
218
+ // Download default skills
219
+ const defaultSkills = status.agentData.default_skills || [];
220
+ for (const skillRef of defaultSkills) {
221
+ try {
222
+ await downloadSkillDependency(config, skillRef, org);
223
+ }
224
+ catch {
225
+ // Skill download failed - not critical, continue
226
+ process.stderr.write(` Warning: Failed to download skill ${skillRef}\n`);
227
+ }
228
+ }
136
229
  // Recursively download its dependencies
137
230
  if (status.agentData.dependencies && status.agentData.dependencies.length > 0) {
138
231
  const nestedStatuses = await checkDependencies(config, status.agentData.dependencies);
@@ -300,7 +393,7 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
300
393
  try {
301
394
  // Download the bundle
302
395
  process.stderr.write(`Downloading bundle...\n`);
303
- const bundleBuffer = await (0, api_1.downloadCodeBundle)(config, org, agentName, version);
396
+ const bundleBuffer = await downloadBundleWithFallback(config, org, agentName, version, agentData.id);
304
397
  await promises_1.default.writeFile(bundleZip, bundleBuffer);
305
398
  process.stderr.write(`Bundle downloaded (${bundleBuffer.length} bytes)\n`);
306
399
  // Extract the bundle
@@ -393,9 +486,32 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
393
486
  }
394
487
  // Run the entrypoint with input via stdin
395
488
  process.stderr.write(`\nRunning: python3 ${entrypoint}\n\n`);
489
+ // Pass auth credentials to subprocess for orchestrator agents calling sub-agents
490
+ const subprocessEnv = { ...process.env };
491
+ if (config.apiKey) {
492
+ subprocessEnv.ORCHAGENT_SERVICE_KEY = config.apiKey;
493
+ subprocessEnv.ORCHAGENT_API_URL = config.apiUrl;
494
+ }
495
+ // For orchestrator agents with dependencies, enable local execution mode
496
+ if (agentData.dependencies && agentData.dependencies.length > 0) {
497
+ subprocessEnv[LOCAL_EXECUTION_ENV] = 'true';
498
+ subprocessEnv[AGENTS_DIR_ENV] = AGENTS_DIR;
499
+ // Initialize call chain with this agent
500
+ const agentRef = `${org}/${agentName}@${version}`;
501
+ subprocessEnv[CALL_CHAIN_ENV] = agentRef;
502
+ // Set deadline from manifest timeout (default 120s)
503
+ const manifest = agentData;
504
+ const timeoutMs = manifest.manifest?.timeout_ms || 120000;
505
+ subprocessEnv[DEADLINE_MS_ENV] = String(Date.now() + timeoutMs);
506
+ // Set max hops from manifest (default 10)
507
+ subprocessEnv[MAX_HOPS_ENV] = String(manifest.manifest?.max_hops || 10);
508
+ // Set downstream cap
509
+ subprocessEnv[DOWNSTREAM_REMAINING_ENV] = String(manifest.manifest?.per_call_downstream_cap || 100);
510
+ }
396
511
  const proc = (0, child_process_1.spawn)('python3', [entrypointPath], {
397
512
  cwd: extractDir,
398
513
  stdio: ['pipe', 'pipe', 'pipe'],
514
+ env: subprocessEnv,
399
515
  });
400
516
  // Send input JSON via stdin
401
517
  proc.stdin.write(inputJson);
@@ -484,6 +600,51 @@ async function saveAgentLocally(org, agent, agentData) {
484
600
  }
485
601
  return agentDir;
486
602
  }
603
+ async function saveBundleLocally(config, org, agent, version, agentId) {
604
+ const agentDir = path_1.default.join(AGENTS_DIR, org, agent);
605
+ const bundleDir = path_1.default.join(agentDir, 'bundle');
606
+ // Check if already extracted with same version
607
+ const metaPath = path_1.default.join(agentDir, 'agent.json');
608
+ try {
609
+ const existingMeta = await promises_1.default.readFile(metaPath, 'utf-8');
610
+ const existing = JSON.parse(existingMeta);
611
+ if (existing.version === version) {
612
+ // Check if bundle dir exists
613
+ try {
614
+ await promises_1.default.access(bundleDir);
615
+ return bundleDir; // Already cached
616
+ }
617
+ catch {
618
+ // Bundle dir doesn't exist, need to extract
619
+ }
620
+ }
621
+ }
622
+ catch {
623
+ // Metadata doesn't exist, need to download
624
+ }
625
+ // Download and extract bundle
626
+ process.stderr.write(`Downloading bundle for ${org}/${agent}@${version}...\n`);
627
+ const bundleBuffer = await downloadBundleWithFallback(config, org, agent, version, agentId);
628
+ const tempZip = path_1.default.join(os_1.default.tmpdir(), `bundle-${Date.now()}.zip`);
629
+ await promises_1.default.writeFile(tempZip, bundleBuffer);
630
+ // Clean and recreate bundle directory
631
+ try {
632
+ await promises_1.default.rm(bundleDir, { recursive: true, force: true });
633
+ }
634
+ catch {
635
+ // Directory might not exist
636
+ }
637
+ await promises_1.default.mkdir(bundleDir, { recursive: true });
638
+ await unzipBundle(tempZip, bundleDir);
639
+ // Clean up temp file
640
+ try {
641
+ await promises_1.default.rm(tempZip);
642
+ }
643
+ catch {
644
+ // Ignore cleanup errors
645
+ }
646
+ return bundleDir;
647
+ }
487
648
  function registerRunCommand(program) {
488
649
  program
489
650
  .command('run <agent> [args...]')
@@ -49,6 +49,61 @@ function parseSkillRef(value) {
49
49
  }
50
50
  throw new errors_1.CliError('Invalid skill reference. Use org/skill or skill format.');
51
51
  }
52
+ async function downloadSkillWithFallback(config, org, skill, version) {
53
+ // Try public endpoint first
54
+ try {
55
+ const meta = await (0, api_1.publicRequest)(config, `/public/agents/${org}/${skill}/${version}`);
56
+ // Verify it's a skill type before downloading
57
+ const skillType = meta.type;
58
+ if (skillType !== 'skill') {
59
+ throw new errors_1.CliError(`${org}/${skill} is not a skill (type: ${skillType || 'prompt'})`);
60
+ }
61
+ // Download content from public endpoint
62
+ return await (0, api_1.publicRequest)(config, `/public/agents/${org}/${skill}/${version}/download`);
63
+ }
64
+ catch (err) {
65
+ if (!(err instanceof api_1.ApiError) || err.status !== 404)
66
+ throw err;
67
+ }
68
+ // Fallback to authenticated endpoint for private skills
69
+ if (!config.apiKey) {
70
+ throw new api_1.ApiError(`Skill '${org}/${skill}@${version}' not found`, 404);
71
+ }
72
+ const userOrg = await (0, api_1.getOrg)(config);
73
+ if (userOrg.slug !== org) {
74
+ throw new api_1.ApiError(`Skill '${org}/${skill}@${version}' not found`, 404);
75
+ }
76
+ // Find skill in user's list
77
+ const agents = await (0, api_1.listMyAgents)(config);
78
+ const matching = agents.filter(a => a.name === skill && a.type === 'skill');
79
+ if (matching.length === 0) {
80
+ throw new api_1.ApiError(`Skill '${org}/${skill}@${version}' not found`, 404);
81
+ }
82
+ let targetAgent = matching[0];
83
+ if (version !== 'v1' && version !== 'latest') {
84
+ const found = matching.find(a => a.version === version);
85
+ if (!found) {
86
+ throw new api_1.ApiError(`Skill '${org}/${skill}@${version}' not found`, 404);
87
+ }
88
+ targetAgent = found;
89
+ }
90
+ else {
91
+ // Get most recent
92
+ targetAgent = matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
93
+ }
94
+ // Verify it's a skill type
95
+ if (targetAgent.type !== 'skill') {
96
+ throw new errors_1.CliError(`${org}/${skill} is not a skill (type: ${targetAgent.type || 'prompt'})`);
97
+ }
98
+ // Convert Agent to SkillDownload format
99
+ return {
100
+ type: targetAgent.type,
101
+ name: targetAgent.name,
102
+ version: targetAgent.version,
103
+ description: targetAgent.description,
104
+ prompt: targetAgent.prompt,
105
+ };
106
+ }
52
107
  function registerSkillCommand(program) {
53
108
  const skill = program.command('skill').description('Manage and install skills');
54
109
  // orch skill list (deprecated)
@@ -65,6 +120,40 @@ function registerSkillCommand(program) {
65
120
  'Browse all skills at: https://orchagent.io/explore\n');
66
121
  process.exit(0);
67
122
  });
123
+ // orch skill create [name]
124
+ skill
125
+ .command('create [name]')
126
+ .description('Create a new skill from template')
127
+ .action(async (name) => {
128
+ const cwd = process.cwd();
129
+ const skillName = name || path_1.default.basename(cwd);
130
+ const skillPath = path_1.default.join(cwd, 'SKILL.md');
131
+ // Check if SKILL.md already exists
132
+ try {
133
+ await promises_1.default.access(skillPath);
134
+ throw new errors_1.CliError('SKILL.md already exists');
135
+ }
136
+ catch (err) {
137
+ if (err.code !== 'ENOENT')
138
+ throw err;
139
+ }
140
+ const template = `---
141
+ name: ${skillName}
142
+ description: When to use this skill
143
+ license: MIT
144
+ ---
145
+
146
+ # ${skillName}
147
+
148
+ Instructions and guidance for AI agents...
149
+ `;
150
+ await promises_1.default.writeFile(skillPath, template);
151
+ await (0, analytics_1.track)('cli_skill_create', { name: skillName });
152
+ process.stdout.write(`Created skill: ${skillPath}\n`);
153
+ process.stdout.write(`\nNext steps:\n`);
154
+ process.stdout.write(` 1. Edit SKILL.md with your skill content\n`);
155
+ process.stdout.write(` 2. Run: orchagent publish\n`);
156
+ });
68
157
  // orch skill install <skill>
69
158
  skill
70
159
  .command('install <skill>')
@@ -77,14 +166,8 @@ function registerSkillCommand(program) {
77
166
  if (!org) {
78
167
  throw new errors_1.CliError('Missing org. Use org/skill or set default org.');
79
168
  }
80
- // Fetch skill metadata to verify it's a skill
81
- const skillMeta = await (0, api_1.publicRequest)(resolved, `/public/agents/${org}/${parsed.skill}/${parsed.version}`);
82
- const skillType = skillMeta.type;
83
- if (skillType !== 'skill') {
84
- throw new errors_1.CliError(`${org}/${parsed.skill} is not a skill (type: ${skillType || 'prompt'})`);
85
- }
86
- // Download skill content
87
- const skillData = await (0, api_1.publicRequest)(resolved, `/public/agents/${org}/${parsed.skill}/${parsed.version}/download`);
169
+ // Download skill (tries public first, falls back to authenticated for private)
170
+ const skillData = await downloadSkillWithFallback(resolved, org, parsed.skill, parsed.version);
88
171
  if (!skillData.prompt) {
89
172
  throw new errors_1.CliError('Skill has no content');
90
173
  }
package/dist/lib/api.js CHANGED
@@ -49,6 +49,9 @@ exports.searchAgents = searchAgents;
49
49
  exports.fetchLlmKeys = fetchLlmKeys;
50
50
  exports.downloadCodeBundle = downloadCodeBundle;
51
51
  exports.uploadCodeBundle = uploadCodeBundle;
52
+ exports.getMyAgent = getMyAgent;
53
+ exports.getAgentWithFallback = getAgentWithFallback;
54
+ exports.downloadCodeBundleAuthenticated = downloadCodeBundleAuthenticated;
52
55
  class ApiError extends Error {
53
56
  status;
54
57
  payload;
@@ -166,7 +169,7 @@ async function downloadCodeBundle(config, org, agent, version) {
166
169
  /**
167
170
  * Upload a code bundle for a hosted code agent.
168
171
  */
169
- async function uploadCodeBundle(config, agentId, bundlePath) {
172
+ async function uploadCodeBundle(config, agentId, bundlePath, entrypoint) {
170
173
  if (!config.apiKey) {
171
174
  throw new ApiError('Missing API key. Run `orchagent login` first.', 401);
172
175
  }
@@ -177,11 +180,16 @@ async function uploadCodeBundle(config, agentId, bundlePath) {
177
180
  // Create form data
178
181
  const formData = new FormData();
179
182
  formData.append('file', blob, 'bundle.zip');
183
+ // Build headers
184
+ const headers = {
185
+ Authorization: `Bearer ${config.apiKey}`,
186
+ };
187
+ if (entrypoint) {
188
+ headers['x-entrypoint'] = entrypoint;
189
+ }
180
190
  const response = await fetch(`${config.apiUrl.replace(/\/$/, '')}/agents/${agentId}/upload`, {
181
191
  method: 'POST',
182
- headers: {
183
- Authorization: `Bearer ${config.apiKey}`,
184
- },
192
+ headers,
185
193
  body: formData,
186
194
  });
187
195
  if (!response.ok) {
@@ -189,3 +197,67 @@ async function uploadCodeBundle(config, agentId, bundlePath) {
189
197
  }
190
198
  return (await response.json());
191
199
  }
200
+ /**
201
+ * Get single agent by name/version from authenticated endpoint.
202
+ */
203
+ async function getMyAgent(config, agentName, version) {
204
+ const agents = await listMyAgents(config);
205
+ const matching = agents.filter(a => a.name === agentName);
206
+ if (matching.length === 0)
207
+ return null;
208
+ if (version === 'latest') {
209
+ return matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
210
+ }
211
+ return matching.find(a => a.version === version) ?? null;
212
+ }
213
+ /**
214
+ * Try public endpoint first, fallback to authenticated for private agents.
215
+ */
216
+ async function getAgentWithFallback(config, org, agentName, version) {
217
+ try {
218
+ return await getPublicAgent(config, org, agentName, version);
219
+ }
220
+ catch (err) {
221
+ if (!(err instanceof ApiError) || err.status !== 404)
222
+ throw err;
223
+ }
224
+ if (!config.apiKey) {
225
+ throw new ApiError(`Agent '${org}/${agentName}@${version}' not found`, 404);
226
+ }
227
+ const userOrg = await getOrg(config);
228
+ if (userOrg.slug !== org) {
229
+ throw new ApiError(`Agent '${org}/${agentName}@${version}' not found`, 404);
230
+ }
231
+ const myAgent = await getMyAgent(config, agentName, version);
232
+ if (!myAgent) {
233
+ throw new ApiError(`Agent '${org}/${agentName}@${version}' not found`, 404);
234
+ }
235
+ return myAgent;
236
+ }
237
+ /**
238
+ * Download a code bundle for a private agent using authenticated endpoint.
239
+ */
240
+ async function downloadCodeBundleAuthenticated(config, agentId) {
241
+ if (!config.apiKey) {
242
+ throw new ApiError('Missing API key for authenticated bundle download', 401);
243
+ }
244
+ const response = await fetch(`${config.apiUrl.replace(/\/$/, '')}/agents/${agentId}/bundle`, {
245
+ headers: {
246
+ Authorization: `Bearer ${config.apiKey}`,
247
+ },
248
+ });
249
+ if (!response.ok) {
250
+ const text = await response.text();
251
+ let message = response.statusText;
252
+ try {
253
+ const payload = JSON.parse(text);
254
+ message = payload.error?.message || payload.message || message;
255
+ }
256
+ catch {
257
+ // Use default message
258
+ }
259
+ throw new ApiError(message, response.status);
260
+ }
261
+ const arrayBuffer = await response.arrayBuffer();
262
+ return Buffer.from(arrayBuffer);
263
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Command-line interface for the OrchAgent AI agent marketplace",
5
5
  "license": "MIT",
6
6
  "author": "OrchAgent <hello@orchagent.io>",