@orchagent/cli 0.3.6 → 0.3.7

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.
@@ -229,7 +229,7 @@ Note: Use 'call' for server-side execution (requires login), 'run' for local exe
229
229
  sourceLabel = multipart.sourceLabel;
230
230
  }
231
231
  const url = `${resolved.apiUrl.replace(/\/$/, '')}/${org}/${parsed.agent}/${parsed.version}/${endpoint}`;
232
- const response = await (0, api_1.safeFetch)(url, {
232
+ const response = await (0, api_1.safeFetchWithRetryForCalls)(url, {
233
233
  method: 'POST',
234
234
  headers,
235
235
  body,
@@ -70,20 +70,50 @@ function registerInstallCommand(program) {
70
70
  .option('--format <formats>', 'Comma-separated format IDs (e.g., claude-code,cursor)')
71
71
  .option('--scope <scope>', 'Install scope: user (home dir) or project (current dir)', 'user')
72
72
  .option('--dry-run', 'Show what would be installed without making changes')
73
+ .option('--json', 'Output result as JSON (for automation/tooling)')
73
74
  .action(async (agentArg, options) => {
75
+ const jsonMode = options.json === true;
76
+ const log = (msg) => { if (!jsonMode)
77
+ process.stdout.write(msg); };
78
+ const logErr = (msg) => { if (!jsonMode)
79
+ process.stderr.write(msg); };
80
+ // Result tracking for JSON output
81
+ const result = {
82
+ success: false,
83
+ agent: '',
84
+ version: '',
85
+ scope: '',
86
+ formats: [],
87
+ files: [],
88
+ warnings: [],
89
+ errors: [],
90
+ };
74
91
  const resolved = await (0, config_1.getResolvedConfig)();
75
92
  const parsed = parseAgentRef(agentArg);
76
93
  const org = parsed.org ?? resolved.defaultOrg;
77
94
  if (!org) {
95
+ if (jsonMode) {
96
+ result.errors.push('Missing org. Use org/agent format or set default org.');
97
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
98
+ process.exit(1);
99
+ }
78
100
  throw new errors_1.CliError('Missing org. Use org/agent format or set default org.');
79
101
  }
102
+ result.agent = `${org}/${parsed.name}`;
103
+ result.version = parsed.version;
80
104
  // Determine target formats
81
105
  let targetFormats = [];
82
106
  if (options.format) {
83
107
  targetFormats = options.format.split(',').map(f => f.trim());
84
108
  const invalid = targetFormats.filter(f => !adapters_1.adapterRegistry.has(f));
85
109
  if (invalid.length > 0) {
86
- throw new errors_1.CliError(`Unknown format(s): ${invalid.join(', ')}. Available: ${adapters_1.adapterRegistry.getIds().join(', ')}`);
110
+ const errMsg = `Unknown format(s): ${invalid.join(', ')}. Available: ${adapters_1.adapterRegistry.getIds().join(', ')}`;
111
+ if (jsonMode) {
112
+ result.errors.push(errMsg);
113
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
114
+ process.exit(1);
115
+ }
116
+ throw new errors_1.CliError(errMsg);
87
117
  }
88
118
  }
89
119
  else {
@@ -96,41 +126,65 @@ function registerInstallCommand(program) {
96
126
  targetFormats = ['claude-code'];
97
127
  }
98
128
  }
129
+ result.formats = targetFormats;
99
130
  // Validate scope
100
131
  let scope = options.scope;
101
132
  if (scope !== 'user' && scope !== 'project') {
102
- throw new errors_1.CliError('Scope must be "user" or "project"');
133
+ const errMsg = 'Scope must be "user" or "project"';
134
+ if (jsonMode) {
135
+ result.errors.push(errMsg);
136
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
137
+ process.exit(1);
138
+ }
139
+ throw new errors_1.CliError(errMsg);
103
140
  }
141
+ result.scope = scope;
104
142
  // Download agent
105
- process.stdout.write(`Fetching ${org}/${parsed.name}@${parsed.version}...\n`);
106
- const agent = await downloadAgentWithFallback(resolved, org, parsed.name, parsed.version);
143
+ log(`Fetching ${org}/${parsed.name}@${parsed.version}...\n`);
144
+ let agent;
145
+ try {
146
+ agent = await downloadAgentWithFallback(resolved, org, parsed.name, parsed.version);
147
+ }
148
+ catch (err) {
149
+ if (jsonMode) {
150
+ result.errors.push(err instanceof Error ? err.message : String(err));
151
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
152
+ process.exit(1);
153
+ }
154
+ throw err;
155
+ }
107
156
  // Install for each format
108
157
  let filesWritten = 0;
109
158
  for (const formatId of targetFormats) {
110
159
  const adapter = adapters_1.adapterRegistry.get(formatId);
111
160
  if (!adapter) {
112
- process.stderr.write(`Warning: Unknown format '${formatId}', skipping\n`);
161
+ const warn = `Unknown format '${formatId}', skipping`;
162
+ result.warnings.push(warn);
163
+ logErr(`Warning: ${warn}\n`);
113
164
  continue;
114
165
  }
115
166
  // Check if can convert
116
167
  const checkResult = adapter.canConvert(agent);
117
168
  if (!checkResult.canConvert) {
118
- process.stderr.write(`Cannot convert to ${adapter.name}:\n`);
169
+ logErr(`Cannot convert to ${adapter.name}:\n`);
119
170
  for (const err of checkResult.errors) {
120
- process.stderr.write(` - ${err}\n`);
171
+ result.errors.push(`${adapter.name}: ${err}`);
172
+ logErr(` - ${err}\n`);
121
173
  }
122
174
  continue;
123
175
  }
124
176
  // Show warnings
125
177
  for (const warn of checkResult.warnings) {
126
- process.stdout.write(`Warning (${formatId}): ${warn}\n`);
178
+ result.warnings.push(`${formatId}: ${warn}`);
179
+ log(`Warning (${formatId}): ${warn}\n`);
127
180
  }
128
181
  // Determine scope for this adapter (use local variable to not affect other formats)
129
182
  let effectiveScope = scope;
130
183
  const supportedScopes = adapter.installPaths.map(p => p.scope);
131
184
  if (!supportedScopes.includes(effectiveScope)) {
132
- process.stderr.write(`Warning: ${adapter.name} doesn't support '${scope}' scope. ` +
133
- `Supported: ${supportedScopes.join(', ')}. Using '${supportedScopes[0]}' instead.\n`);
185
+ const warn = `${adapter.name} doesn't support '${scope}' scope. Using '${supportedScopes[0]}' instead.`;
186
+ result.warnings.push(warn);
187
+ logErr(`Warning: ${warn}\n`);
134
188
  effectiveScope = supportedScopes[0];
135
189
  }
136
190
  // Convert
@@ -142,8 +196,9 @@ function registerInstallCommand(program) {
142
196
  const fullDir = path_1.default.join(baseDir, file.installPath);
143
197
  const fullPath = path_1.default.join(fullDir, file.filename);
144
198
  if (options.dryRun) {
145
- process.stdout.write(`Would install: ${fullPath}\n`);
146
- process.stdout.write(`Content preview:\n${file.content.slice(0, 500)}...\n\n`);
199
+ log(`Would install: ${fullPath}\n`);
200
+ log(`Content preview:\n${file.content.slice(0, 500)}...\n\n`);
201
+ result.files.push({ path: fullPath, format: formatId });
147
202
  continue;
148
203
  }
149
204
  // Create directory and write file
@@ -174,7 +229,8 @@ function registerInstallCommand(program) {
174
229
  contentHash: (0, installed_1.computeHash)(file.content),
175
230
  };
176
231
  await (0, installed_1.trackInstall)(installedAgent);
177
- process.stdout.write(`Installed: ${fullPath}\n`);
232
+ result.files.push({ path: fullPath, format: formatId });
233
+ log(`Installed: ${fullPath}\n`);
178
234
  }
179
235
  }
180
236
  if (!options.dryRun) {
@@ -184,18 +240,33 @@ function registerInstallCommand(program) {
184
240
  formats: targetFormats,
185
241
  scope,
186
242
  });
187
- process.stdout.write(`\nAgent installed successfully!\n`);
243
+ result.success = true;
244
+ log(`\nAgent installed successfully!\n`);
188
245
  if (scope === 'user') {
189
- process.stdout.write(`Available in all your projects.\n`);
246
+ log(`Available in all your projects.\n`);
190
247
  }
191
248
  else {
192
- process.stdout.write(`Available in this project only.\n`);
249
+ log(`Available in this project only.\n`);
193
250
  }
194
251
  }
195
252
  else {
196
- process.stderr.write(`\nNo files were installed. Check warnings above.\n`);
253
+ result.errors.push('No files were installed. Check warnings.');
254
+ logErr(`\nNo files were installed. Check warnings above.\n`);
255
+ }
256
+ }
257
+ else {
258
+ // Dry run is considered success if we got file list
259
+ result.success = result.files.length > 0;
260
+ }
261
+ // Output JSON result
262
+ if (jsonMode) {
263
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
264
+ if (!result.success) {
197
265
  process.exit(1);
198
266
  }
199
267
  }
268
+ else if (!result.success && !options.dryRun) {
269
+ process.exit(1);
270
+ }
200
271
  });
201
272
  }
@@ -164,7 +164,24 @@ Instructions and guidance for AI agents...
164
164
  .option('--scope <scope>', 'Install scope: user or project', 'project')
165
165
  .option('--dry-run', 'Show what would be installed without making changes')
166
166
  .option('--format <formats>', 'Comma-separated format IDs (e.g., claude-code,cursor)')
167
+ .option('--json', 'Output result as JSON (for automation/tooling)')
167
168
  .action(async (skillRef, options) => {
169
+ const jsonMode = options.json === true;
170
+ const log = (msg) => { if (!jsonMode)
171
+ process.stdout.write(msg); };
172
+ const logErr = (msg) => { if (!jsonMode)
173
+ process.stderr.write(msg); };
174
+ // Result tracking for JSON output
175
+ const result = {
176
+ success: false,
177
+ skill: '',
178
+ version: '',
179
+ scope: '',
180
+ tools: [],
181
+ files: [],
182
+ warnings: [],
183
+ errors: [],
184
+ };
168
185
  const resolved = await (0, config_1.getResolvedConfig)();
169
186
  // Determine target formats
170
187
  let targetFormats = [];
@@ -173,7 +190,13 @@ Instructions and guidance for AI agents...
173
190
  // Validate format IDs
174
191
  const invalid = targetFormats.filter(f => !config_1.VALID_FORMAT_IDS.includes(f));
175
192
  if (invalid.length > 0) {
176
- throw new errors_1.CliError(`Invalid format ID(s): ${invalid.join(', ')}. Valid: ${config_1.VALID_FORMAT_IDS.join(', ')}`);
193
+ const errMsg = `Invalid format ID(s): ${invalid.join(', ')}. Valid: ${config_1.VALID_FORMAT_IDS.join(', ')}`;
194
+ if (jsonMode) {
195
+ result.errors.push(errMsg);
196
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
197
+ process.exit(1);
198
+ }
199
+ throw new errors_1.CliError(errMsg);
177
200
  }
178
201
  }
179
202
  else {
@@ -183,7 +206,9 @@ Instructions and guidance for AI agents...
183
206
  targetFormats = defaults.filter(f => config_1.VALID_FORMAT_IDS.includes(f));
184
207
  const skipped = defaults.filter(f => !config_1.VALID_FORMAT_IDS.includes(f));
185
208
  if (skipped.length > 0) {
186
- process.stderr.write(`Note: Skipping ${skipped.join(', ')} (no skill directory)\n`);
209
+ const warn = `Skipping ${skipped.join(', ')} (no skill directory)`;
210
+ result.warnings.push(warn);
211
+ logErr(`Note: ${warn}\n`);
187
212
  }
188
213
  }
189
214
  }
@@ -194,17 +219,43 @@ Instructions and guidance for AI agents...
194
219
  const parsed = parseSkillRef(skillRef);
195
220
  const org = parsed.org ?? resolved.defaultOrg;
196
221
  if (!org) {
197
- throw new errors_1.CliError('Missing org. Use org/skill or set default org.');
222
+ const errMsg = 'Missing org. Use org/skill or set default org.';
223
+ if (jsonMode) {
224
+ result.errors.push(errMsg);
225
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
226
+ process.exit(1);
227
+ }
228
+ throw new errors_1.CliError(errMsg);
198
229
  }
230
+ result.skill = `${org}/${parsed.skill}`;
231
+ result.version = parsed.version;
199
232
  // Download skill (tries public first, falls back to authenticated for private)
200
- const skillData = await downloadSkillWithFallback(resolved, org, parsed.skill, parsed.version);
233
+ let skillData;
234
+ try {
235
+ skillData = await downloadSkillWithFallback(resolved, org, parsed.skill, parsed.version);
236
+ }
237
+ catch (err) {
238
+ if (jsonMode) {
239
+ result.errors.push(err instanceof Error ? err.message : String(err));
240
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
241
+ process.exit(1);
242
+ }
243
+ throw err;
244
+ }
201
245
  if (!skillData.prompt) {
246
+ const errMsg = 'Skill has no content. The skill exists but has an empty prompt.';
247
+ if (jsonMode) {
248
+ result.errors.push(errMsg);
249
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
250
+ process.exit(1);
251
+ }
202
252
  throw new errors_1.CliError('Skill has no content.\n\n' +
203
253
  'The skill exists but has an empty prompt. This may be a publishing issue.\n' +
204
254
  'Try re-publishing the skill or contact the skill author.');
205
255
  }
206
256
  // Determine scope (--global is legacy alias for --scope user)
207
257
  const scope = options.global ? 'user' : (options.scope || 'project');
258
+ result.scope = scope;
208
259
  // Build skill content with header
209
260
  const skillContent = `# ${skillData.name}
210
261
 
@@ -216,16 +267,21 @@ ${skillData.prompt}
216
267
  `;
217
268
  // Dry run - show what would be installed
218
269
  if (options.dryRun) {
219
- process.stdout.write(`Would install ${org}/${parsed.skill}@${parsed.version}\n\n`);
220
- process.stdout.write(`Target directories (scope: ${scope}):\n`);
270
+ log(`Would install ${org}/${parsed.skill}@${parsed.version}\n\n`);
271
+ log(`Target directories (scope: ${scope}):\n`);
221
272
  for (const tool of toolDirs) {
222
273
  const baseDir = scope === 'user' ? os_1.default.homedir() : process.cwd();
223
274
  const toolPath = scope === 'user' ? tool.userPath : tool.projectPath;
224
275
  const skillDir = path_1.default.join(baseDir, toolPath);
225
276
  const skillFile = path_1.default.join(skillDir, `${parsed.skill}.md`);
226
- process.stdout.write(` - ${tool.name}: ${skillFile}\n`);
277
+ result.files.push({ path: skillFile, tool: tool.name });
278
+ log(` - ${tool.name}: ${skillFile}\n`);
279
+ }
280
+ log(`\nNo changes made (dry run)\n`);
281
+ result.success = true;
282
+ if (jsonMode) {
283
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
227
284
  }
228
- process.stdout.write(`\nNo changes made (dry run)\n`);
229
285
  return;
230
286
  }
231
287
  // Install to target AI tool directories
@@ -239,15 +295,26 @@ ${skillData.prompt}
239
295
  await promises_1.default.mkdir(skillDir, { recursive: true });
240
296
  await promises_1.default.writeFile(skillFile, skillContent);
241
297
  installed.push(tool.name);
298
+ result.files.push({ path: skillFile, tool: tool.name });
242
299
  }
243
300
  catch (err) {
244
301
  // Skip if we can't write (e.g., permission issues)
245
- process.stderr.write(`Warning: Could not install to ${toolPath}: ${err.message}\n`);
302
+ const warn = `Could not install to ${toolPath}: ${err.message}`;
303
+ result.warnings.push(warn);
304
+ logErr(`Warning: ${warn}\n`);
246
305
  }
247
306
  }
248
307
  if (installed.length === 0) {
249
- throw new errors_1.CliError('Failed to install skill to any directory');
308
+ const errMsg = 'Failed to install skill to any directory';
309
+ if (jsonMode) {
310
+ result.errors.push(errMsg);
311
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
312
+ process.exit(1);
313
+ }
314
+ throw new errors_1.CliError(errMsg);
250
315
  }
316
+ result.tools = installed;
317
+ result.success = true;
251
318
  await (0, analytics_1.track)('cli_skill_install', {
252
319
  skill: `${org}/${parsed.skill}`,
253
320
  scope,
@@ -257,11 +324,16 @@ ${skillData.prompt}
257
324
  if (resolved.apiKey) {
258
325
  (0, api_1.reportInstall)(resolved, org, parsed.skill, parsed.version, package_json_1.default.version).catch(() => { });
259
326
  }
260
- process.stdout.write(`Installed ${org}/${parsed.skill}@${parsed.version}\n`);
261
- process.stdout.write(`\nAvailable for:\n`);
262
- for (const tool of installed) {
263
- process.stdout.write(` - ${tool}\n`);
327
+ if (jsonMode) {
328
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
329
+ }
330
+ else {
331
+ log(`Installed ${org}/${parsed.skill}@${parsed.version}\n`);
332
+ log(`\nAvailable for:\n`);
333
+ for (const tool of installed) {
334
+ log(` - ${tool}\n`);
335
+ }
336
+ log(`\nLocation: ${scope === 'user' ? '~/' : './'}\n`);
264
337
  }
265
- process.stdout.write(`\nLocation: ${scope === 'user' ? '~/' : './'}\n`);
266
338
  });
267
339
  }
package/dist/lib/api.js CHANGED
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ApiError = void 0;
37
37
  exports.safeFetch = safeFetch;
38
+ exports.safeFetchWithRetryForCalls = safeFetchWithRetryForCalls;
38
39
  exports.request = request;
39
40
  exports.publicRequest = publicRequest;
40
41
  exports.getOrg = getOrg;
@@ -60,13 +61,16 @@ exports.reportInstall = reportInstall;
60
61
  exports.fetchUserProfile = fetchUserProfile;
61
62
  const errors_1 = require("./errors");
62
63
  const DEFAULT_TIMEOUT_MS = 15000;
64
+ const CALL_TIMEOUT_MS = 120000; // 2 minutes for agent calls (can take time)
63
65
  const MAX_RETRIES = 3;
64
66
  const BASE_DELAY_MS = 1000;
65
67
  async function safeFetch(url, options) {
68
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
69
+ const { timeoutMs: _, ...fetchOptions } = options ?? {};
66
70
  try {
67
71
  return await fetch(url, {
68
- ...options,
69
- signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
72
+ ...fetchOptions,
73
+ signal: AbortSignal.timeout(timeoutMs),
70
74
  });
71
75
  }
72
76
  catch (err) {
@@ -77,6 +81,44 @@ async function safeFetch(url, options) {
77
81
  throw new errors_1.NetworkError(url, err instanceof Error ? err : undefined);
78
82
  }
79
83
  }
84
+ /**
85
+ * safeFetch with retry logic for connection failures.
86
+ * Use for important operations that should retry on transient errors.
87
+ */
88
+ async function safeFetchWithRetryForCalls(url, options) {
89
+ let lastError;
90
+ const timeoutMs = options?.timeoutMs ?? CALL_TIMEOUT_MS;
91
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
92
+ try {
93
+ const response = await safeFetch(url, { ...options, timeoutMs });
94
+ // Don't retry client errors (except 429)
95
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
96
+ return response;
97
+ }
98
+ // Retry on 5xx or 429
99
+ if (response.status >= 500 || response.status === 429) {
100
+ if (attempt < MAX_RETRIES) {
101
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
102
+ const jitter = Math.random() * 500;
103
+ process.stderr.write(`Request failed (${response.status}), retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
104
+ await new Promise(r => setTimeout(r, delay + jitter));
105
+ continue;
106
+ }
107
+ }
108
+ return response;
109
+ }
110
+ catch (error) {
111
+ lastError = error;
112
+ if (attempt < MAX_RETRIES) {
113
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
114
+ const jitter = Math.random() * 500;
115
+ process.stderr.write(`Connection error, retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
116
+ await new Promise(r => setTimeout(r, delay + jitter));
117
+ }
118
+ }
119
+ }
120
+ throw lastError ?? new errors_1.NetworkError(url);
121
+ }
80
122
  async function safeFetchWithRetry(url, options) {
81
123
  let lastError;
82
124
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Command-line interface for the orchagent AI agent marketplace",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",