@orchagent/cli 0.2.25 → 0.3.0

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.
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentsMdAdapter = void 0;
4
+ exports.agentsMdAdapter = {
5
+ id: 'agents-md',
6
+ name: 'AGENTS.md (Universal)',
7
+ version: '1.0.0',
8
+ formatVersion: '1.0',
9
+ supportsAgentTypes: ['prompt', 'skill'],
10
+ installPaths: [
11
+ {
12
+ scope: 'project',
13
+ path: './',
14
+ description: 'Project root (AGENTS.md)',
15
+ },
16
+ ],
17
+ canConvert(agent) {
18
+ const warnings = [];
19
+ const errors = [];
20
+ if (agent.type === 'code') {
21
+ errors.push('Code agents cannot be converted to AGENTS.md');
22
+ return { canConvert: false, warnings, errors };
23
+ }
24
+ if (!agent.prompt) {
25
+ errors.push('Agent has no prompt content');
26
+ return { canConvert: false, warnings, errors };
27
+ }
28
+ warnings.push('AGENTS.md content should be appended to existing file, not replaced');
29
+ return { canConvert: true, warnings, errors };
30
+ },
31
+ convert(agent) {
32
+ const agentRef = `${agent.org_slug}/${agent.name}`;
33
+ const description = agent.description || '';
34
+ // Use orchagent markers for managed section
35
+ const content = `<!-- orchagent:${agentRef} -->
36
+ ## ${agent.name}
37
+
38
+ ${description}
39
+
40
+ ${agent.prompt || ''}
41
+ <!-- /orchagent:${agentRef} -->
42
+ `;
43
+ return [
44
+ {
45
+ filename: 'AGENTS.md',
46
+ content,
47
+ installPath: './',
48
+ },
49
+ ];
50
+ },
51
+ };
@@ -0,0 +1,104 @@
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.claudeCodeAdapter = void 0;
7
+ const yaml_1 = __importDefault(require("yaml"));
8
+ // Map Anthropic model names to Claude Code aliases
9
+ function mapModelToAlias(model) {
10
+ if (!model)
11
+ return 'inherit';
12
+ const lower = model.toLowerCase();
13
+ if (lower.includes('opus'))
14
+ return 'opus';
15
+ if (lower.includes('haiku'))
16
+ return 'haiku';
17
+ // Default to sonnet for any Claude model
18
+ if (lower.includes('claude') || lower.includes('sonnet'))
19
+ return 'sonnet';
20
+ return 'inherit';
21
+ }
22
+ // Convert agent name to valid Claude Code name (lowercase + hyphens)
23
+ function normalizeAgentName(name) {
24
+ return name
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9-]/g, '-')
27
+ .replace(/-+/g, '-')
28
+ .replace(/^-|-$/g, '');
29
+ }
30
+ exports.claudeCodeAdapter = {
31
+ id: 'claude-code',
32
+ name: 'Claude Code Sub-Agent',
33
+ version: '1.0.0',
34
+ formatVersion: '2026-01',
35
+ supportsAgentTypes: ['prompt', 'skill'],
36
+ installPaths: [
37
+ {
38
+ scope: 'user',
39
+ path: '~/.claude/agents/',
40
+ description: 'User-level (available in all projects)',
41
+ },
42
+ {
43
+ scope: 'project',
44
+ path: '.claude/agents/',
45
+ description: 'Project-level (current directory)',
46
+ },
47
+ ],
48
+ canConvert(agent) {
49
+ const warnings = [];
50
+ const errors = [];
51
+ // Code agents cannot be converted
52
+ if (agent.type === 'code') {
53
+ errors.push('Code agents cannot be converted to Claude Code sub-agents (they require execution)');
54
+ return { canConvert: false, warnings, errors };
55
+ }
56
+ // Check for prompt
57
+ if (!agent.prompt) {
58
+ errors.push('Agent has no prompt content');
59
+ return { canConvert: false, warnings, errors };
60
+ }
61
+ // Warnings for fields that can't be fully mapped
62
+ if (agent.input_schema) {
63
+ warnings.push('input_schema will be described in the prompt body, not enforced');
64
+ }
65
+ if (agent.output_schema) {
66
+ warnings.push('output_schema will be described in the prompt body, not enforced');
67
+ }
68
+ return { canConvert: true, warnings, errors };
69
+ },
70
+ convert(agent) {
71
+ const normalizedName = normalizeAgentName(agent.name);
72
+ // Build frontmatter
73
+ const frontmatter = {
74
+ name: normalizedName,
75
+ description: agent.description || `Delegatable agent: ${agent.name}`,
76
+ tools: 'Read, Glob, Grep', // Safe defaults - read-only
77
+ };
78
+ // Map model if specified
79
+ if (agent.default_models?.anthropic) {
80
+ const modelAlias = mapModelToAlias(agent.default_models.anthropic);
81
+ if (modelAlias !== 'inherit') {
82
+ frontmatter.model = modelAlias;
83
+ }
84
+ }
85
+ // Build body
86
+ let body = agent.prompt || '';
87
+ // Add schema descriptions if present
88
+ if (agent.input_schema) {
89
+ body += `\n\n## Input Schema\n\nThis agent expects input matching:\n\`\`\`json\n${JSON.stringify(agent.input_schema, null, 2)}\n\`\`\``;
90
+ }
91
+ if (agent.output_schema) {
92
+ body += `\n\n## Output Schema\n\nThis agent should return output matching:\n\`\`\`json\n${JSON.stringify(agent.output_schema, null, 2)}\n\`\`\``;
93
+ }
94
+ // Combine frontmatter + body
95
+ const content = `---\n${yaml_1.default.stringify(frontmatter).trim()}\n---\n\n${body.trim()}\n`;
96
+ return [
97
+ {
98
+ filename: `${normalizedName}.md`,
99
+ content,
100
+ installPath: '.claude/agents/',
101
+ },
102
+ ];
103
+ },
104
+ };
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cursorAdapter = void 0;
4
+ // Convert agent name to valid filename (lowercase + hyphens)
5
+ function normalizeAgentName(name) {
6
+ return name
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9-]/g, '-')
9
+ .replace(/-+/g, '-')
10
+ .replace(/^-|-$/g, '');
11
+ }
12
+ exports.cursorAdapter = {
13
+ id: 'cursor',
14
+ name: 'Cursor Rules',
15
+ version: '1.0.0',
16
+ formatVersion: '2026-01',
17
+ supportsAgentTypes: ['prompt', 'skill'],
18
+ installPaths: [
19
+ {
20
+ scope: 'project',
21
+ path: '.cursor/rules/',
22
+ description: 'Project-level Cursor rules',
23
+ },
24
+ ],
25
+ canConvert(agent) {
26
+ const warnings = [];
27
+ const errors = [];
28
+ if (agent.type === 'code') {
29
+ errors.push('Code agents cannot be converted to Cursor rules');
30
+ return { canConvert: false, warnings, errors };
31
+ }
32
+ if (!agent.prompt) {
33
+ errors.push('Agent has no prompt content');
34
+ return { canConvert: false, warnings, errors };
35
+ }
36
+ // Cursor doesn't support input/output schemas
37
+ if (agent.input_schema) {
38
+ warnings.push('input_schema is not supported in Cursor rules');
39
+ }
40
+ if (agent.output_schema) {
41
+ warnings.push('output_schema is not supported in Cursor rules');
42
+ }
43
+ return { canConvert: true, warnings, errors };
44
+ },
45
+ convert(agent) {
46
+ const normalizedName = normalizeAgentName(agent.name);
47
+ const description = agent.description || `Rules from ${agent.name}`;
48
+ // Cursor .mdc format
49
+ const content = `---
50
+ description: ${description}
51
+ globs:
52
+ alwaysApply: false
53
+ ---
54
+
55
+ # ${agent.name}
56
+
57
+ ${agent.prompt || ''}
58
+ `;
59
+ return [
60
+ {
61
+ filename: `${normalizedName}.mdc`,
62
+ content,
63
+ installPath: '.cursor/rules/',
64
+ },
65
+ ];
66
+ },
67
+ };
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.agentsMdAdapter = exports.cursorAdapter = exports.claudeCodeAdapter = exports.adapterRegistry = exports.AdapterRegistry = void 0;
18
+ // Export types
19
+ __exportStar(require("./types"), exports);
20
+ // Export registry
21
+ const registry_1 = require("./registry");
22
+ var registry_2 = require("./registry");
23
+ Object.defineProperty(exports, "AdapterRegistry", { enumerable: true, get: function () { return registry_2.AdapterRegistry; } });
24
+ Object.defineProperty(exports, "adapterRegistry", { enumerable: true, get: function () { return registry_2.adapterRegistry; } });
25
+ // Import and register adapters
26
+ const claude_code_1 = require("./claude-code");
27
+ var claude_code_2 = require("./claude-code");
28
+ Object.defineProperty(exports, "claudeCodeAdapter", { enumerable: true, get: function () { return claude_code_2.claudeCodeAdapter; } });
29
+ const cursor_1 = require("./cursor");
30
+ var cursor_2 = require("./cursor");
31
+ Object.defineProperty(exports, "cursorAdapter", { enumerable: true, get: function () { return cursor_2.cursorAdapter; } });
32
+ const agents_md_1 = require("./agents-md");
33
+ var agents_md_2 = require("./agents-md");
34
+ Object.defineProperty(exports, "agentsMdAdapter", { enumerable: true, get: function () { return agents_md_2.agentsMdAdapter; } });
35
+ // Register built-in adapters
36
+ registry_1.adapterRegistry.register(claude_code_1.claudeCodeAdapter);
37
+ registry_1.adapterRegistry.register(cursor_1.cursorAdapter);
38
+ registry_1.adapterRegistry.register(agents_md_1.agentsMdAdapter);
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.adapterRegistry = exports.AdapterRegistry = void 0;
4
+ class AdapterRegistry {
5
+ adapters = new Map();
6
+ /**
7
+ * Register an adapter
8
+ */
9
+ register(adapter) {
10
+ if (this.adapters.has(adapter.id)) {
11
+ throw new Error(`Adapter '${adapter.id}' is already registered`);
12
+ }
13
+ this.adapters.set(adapter.id, adapter);
14
+ }
15
+ /**
16
+ * Get adapter by ID
17
+ */
18
+ get(id) {
19
+ return this.adapters.get(id);
20
+ }
21
+ /**
22
+ * List all registered adapters
23
+ */
24
+ list() {
25
+ return Array.from(this.adapters.values());
26
+ }
27
+ /**
28
+ * Find adapters compatible with an agent
29
+ */
30
+ findCompatible(agent) {
31
+ return this.list().filter(adapter => {
32
+ const result = adapter.canConvert(agent);
33
+ return result.canConvert;
34
+ });
35
+ }
36
+ /**
37
+ * Check if an adapter ID is valid
38
+ */
39
+ has(id) {
40
+ return this.adapters.has(id);
41
+ }
42
+ /**
43
+ * Get all adapter IDs
44
+ */
45
+ getIds() {
46
+ return Array.from(this.adapters.keys());
47
+ }
48
+ }
49
+ exports.AdapterRegistry = AdapterRegistry;
50
+ // Global registry instance
51
+ exports.adapterRegistry = new AdapterRegistry();
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerConfigCommand = registerConfigCommand;
4
+ const config_1 = require("../lib/config");
5
+ const errors_1 = require("../lib/errors");
6
+ const SUPPORTED_KEYS = ['default-format'];
7
+ function isValidKey(key) {
8
+ return SUPPORTED_KEYS.includes(key);
9
+ }
10
+ async function setConfigValue(key, value) {
11
+ if (!isValidKey(key)) {
12
+ throw new errors_1.CliError(`Unknown config key: ${key}. Supported keys: ${SUPPORTED_KEYS.join(', ')}`);
13
+ }
14
+ if (key === 'default-format') {
15
+ const formats = value.split(',').map((f) => f.trim()).filter(Boolean);
16
+ // Validate all format IDs
17
+ const invalidFormats = formats.filter((f) => !config_1.VALID_FORMAT_IDS.includes(f));
18
+ if (invalidFormats.length > 0) {
19
+ throw new errors_1.CliError(`Invalid format ID(s): ${invalidFormats.join(', ')}. Valid formats: ${config_1.VALID_FORMAT_IDS.join(', ')}`);
20
+ }
21
+ await (0, config_1.setDefaultFormats)(formats);
22
+ process.stdout.write(`Set default-format to: ${formats.join(',')}\n`);
23
+ }
24
+ }
25
+ async function getConfigValue(key) {
26
+ if (!isValidKey(key)) {
27
+ throw new errors_1.CliError(`Unknown config key: ${key}. Supported keys: ${SUPPORTED_KEYS.join(', ')}`);
28
+ }
29
+ if (key === 'default-format') {
30
+ const formats = await (0, config_1.getDefaultFormats)();
31
+ if (formats.length === 0) {
32
+ process.stdout.write('(not set)\n');
33
+ }
34
+ else {
35
+ process.stdout.write(`${formats.join(',')}\n`);
36
+ }
37
+ }
38
+ }
39
+ async function listConfigValues() {
40
+ const config = await (0, config_1.loadConfig)();
41
+ process.stdout.write('CLI Configuration:\n\n');
42
+ // default-format
43
+ const formats = config.default_formats ?? [];
44
+ if (formats.length > 0) {
45
+ process.stdout.write(` default-format: ${formats.join(',')}\n`);
46
+ }
47
+ else {
48
+ process.stdout.write(' default-format: (not set)\n');
49
+ }
50
+ process.stdout.write('\n');
51
+ }
52
+ function registerConfigCommand(program) {
53
+ const config = program
54
+ .command('config')
55
+ .description('Manage CLI configuration');
56
+ config
57
+ .command('set <key> <value>')
58
+ .description('Set a configuration value')
59
+ .action(async (key, value) => {
60
+ await setConfigValue(key, value);
61
+ });
62
+ config
63
+ .command('get <key>')
64
+ .description('Get a configuration value')
65
+ .action(async (key) => {
66
+ await getConfigValue(key);
67
+ });
68
+ config
69
+ .command('list')
70
+ .description('List all configuration values')
71
+ .action(async () => {
72
+ await listConfigValues();
73
+ });
74
+ }
@@ -0,0 +1,44 @@
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.registerFormatsCommand = registerFormatsCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const adapters_1 = require("../adapters");
9
+ function registerFormatsCommand(program) {
10
+ program
11
+ .command('formats')
12
+ .description('List available export formats for agents')
13
+ .option('--json', 'Output raw JSON')
14
+ .action(async (options) => {
15
+ const adapters = adapters_1.adapterRegistry.list();
16
+ if (options.json) {
17
+ const data = adapters.map(a => ({
18
+ id: a.id,
19
+ name: a.name,
20
+ version: a.version,
21
+ formatVersion: a.formatVersion,
22
+ supportsAgentTypes: a.supportsAgentTypes,
23
+ installPaths: a.installPaths,
24
+ }));
25
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
26
+ return;
27
+ }
28
+ process.stdout.write('\nAvailable export formats:\n\n');
29
+ for (const adapter of adapters) {
30
+ process.stdout.write(` ${chalk_1.default.cyan(adapter.id)} ${adapter.name}\n`);
31
+ process.stdout.write(` Supports: ${adapter.supportsAgentTypes.join(', ')} agents\n`);
32
+ process.stdout.write(` Format version: ${adapter.formatVersion}\n`);
33
+ for (const installPath of adapter.installPaths) {
34
+ const pathDisplay = installPath.scope === 'user'
35
+ ? installPath.path
36
+ : installPath.path;
37
+ process.stdout.write(` ${installPath.scope}: ${pathDisplay}\n`);
38
+ }
39
+ process.stdout.write('\n');
40
+ }
41
+ process.stdout.write('Use with: orch install <agent> --format <id>\n');
42
+ process.stdout.write('Set default: orch config set default-format <ids>\n\n');
43
+ });
44
+ }
@@ -21,6 +21,10 @@ const status_1 = require("./status");
21
21
  const workspace_1 = require("./workspace");
22
22
  const tree_1 = require("./tree");
23
23
  const docs_1 = require("./docs");
24
+ const config_1 = require("./config");
25
+ const install_1 = require("./install");
26
+ const formats_1 = require("./formats");
27
+ const update_1 = require("./update");
24
28
  function registerCommands(program) {
25
29
  (0, login_1.registerLoginCommand)(program);
26
30
  (0, whoami_1.registerWhoamiCommand)(program);
@@ -42,4 +46,8 @@ function registerCommands(program) {
42
46
  (0, workspace_1.registerWorkspaceCommand)(program);
43
47
  (0, tree_1.registerTreeCommand)(program);
44
48
  (0, docs_1.registerDocsCommand)(program);
49
+ (0, config_1.registerConfigCommand)(program);
50
+ (0, install_1.registerInstallCommand)(program);
51
+ (0, formats_1.registerFormatsCommand)(program);
52
+ (0, update_1.registerUpdateCommand)(program);
45
53
  }
@@ -0,0 +1,172 @@
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.registerInstallCommand = registerInstallCommand;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const config_1 = require("../lib/config");
11
+ const api_1 = require("../lib/api");
12
+ const errors_1 = require("../lib/errors");
13
+ const analytics_1 = require("../lib/analytics");
14
+ const adapters_1 = require("../adapters");
15
+ const installed_1 = require("../lib/installed");
16
+ const DEFAULT_VERSION = 'v1';
17
+ function parseAgentRef(value) {
18
+ const [ref, versionPart] = value.split('@');
19
+ const version = versionPart?.trim() || DEFAULT_VERSION;
20
+ const segments = ref.split('/');
21
+ if (segments.length === 1) {
22
+ return { name: segments[0], version };
23
+ }
24
+ if (segments.length === 2) {
25
+ return { org: segments[0], name: segments[1], version };
26
+ }
27
+ throw new errors_1.CliError('Invalid agent reference. Use org/agent or agent format.');
28
+ }
29
+ async function downloadAgentWithFallback(config, org, name, version) {
30
+ // Try public endpoint first
31
+ try {
32
+ const agent = await (0, api_1.publicRequest)(config, `/public/agents/${org}/${name}/${version}`);
33
+ return { ...agent, org_slug: org };
34
+ }
35
+ catch (err) {
36
+ if (!(err instanceof api_1.ApiError) || err.status !== 404)
37
+ throw err;
38
+ }
39
+ // Fallback to authenticated endpoint for private agents
40
+ if (!config.apiKey) {
41
+ throw new api_1.ApiError(`Agent '${org}/${name}@${version}' not found`, 404);
42
+ }
43
+ const userOrg = await (0, api_1.getOrg)(config);
44
+ if (userOrg.slug !== org) {
45
+ throw new api_1.ApiError(`Agent '${org}/${name}@${version}' not found`, 404);
46
+ }
47
+ const agents = await (0, api_1.listMyAgents)(config);
48
+ const matching = agents.filter(a => a.name === name);
49
+ if (matching.length === 0) {
50
+ throw new api_1.ApiError(`Agent '${org}/${name}@${version}' not found`, 404);
51
+ }
52
+ let targetAgent;
53
+ if (version === 'latest') {
54
+ targetAgent = matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
55
+ }
56
+ else {
57
+ const found = matching.find(a => a.version === version);
58
+ if (!found) {
59
+ throw new api_1.ApiError(`Agent '${org}/${name}@${version}' not found`, 404);
60
+ }
61
+ targetAgent = found;
62
+ }
63
+ return { ...targetAgent, org_slug: org };
64
+ }
65
+ function registerInstallCommand(program) {
66
+ program
67
+ .command('install <agent>')
68
+ .description('Install agent as sub-agent (Claude Code, Cursor, etc.)')
69
+ .option('--format <formats>', 'Comma-separated format IDs (e.g., claude-code,cursor)')
70
+ .option('--scope <scope>', 'Install scope: user (home dir) or project (current dir)', 'user')
71
+ .option('--dry-run', 'Show what would be installed without making changes')
72
+ .action(async (agentArg, options) => {
73
+ const resolved = await (0, config_1.getResolvedConfig)();
74
+ const parsed = parseAgentRef(agentArg);
75
+ const org = parsed.org ?? resolved.defaultOrg;
76
+ if (!org) {
77
+ throw new errors_1.CliError('Missing org. Use org/agent format or set default org.');
78
+ }
79
+ // Determine target formats
80
+ let targetFormats = [];
81
+ if (options.format) {
82
+ targetFormats = options.format.split(',').map(f => f.trim());
83
+ const invalid = targetFormats.filter(f => !adapters_1.adapterRegistry.has(f));
84
+ if (invalid.length > 0) {
85
+ throw new errors_1.CliError(`Unknown format(s): ${invalid.join(', ')}. Available: ${adapters_1.adapterRegistry.getIds().join(', ')}`);
86
+ }
87
+ }
88
+ else {
89
+ const defaults = await (0, config_1.getDefaultFormats)();
90
+ if (defaults.length > 0) {
91
+ targetFormats = defaults;
92
+ }
93
+ else {
94
+ // No default configured - use claude-code as sensible default
95
+ targetFormats = ['claude-code'];
96
+ }
97
+ }
98
+ // Validate scope
99
+ const scope = options.scope;
100
+ if (scope !== 'user' && scope !== 'project') {
101
+ throw new errors_1.CliError('Scope must be "user" or "project"');
102
+ }
103
+ // Download agent
104
+ process.stdout.write(`Fetching ${org}/${parsed.name}@${parsed.version}...\n`);
105
+ const agent = await downloadAgentWithFallback(resolved, org, parsed.name, parsed.version);
106
+ // Install for each format
107
+ for (const formatId of targetFormats) {
108
+ const adapter = adapters_1.adapterRegistry.get(formatId);
109
+ if (!adapter) {
110
+ process.stderr.write(`Warning: Unknown format '${formatId}', skipping\n`);
111
+ continue;
112
+ }
113
+ // Check if can convert
114
+ const checkResult = adapter.canConvert(agent);
115
+ if (!checkResult.canConvert) {
116
+ process.stderr.write(`Cannot convert to ${adapter.name}:\n`);
117
+ for (const err of checkResult.errors) {
118
+ process.stderr.write(` - ${err}\n`);
119
+ }
120
+ continue;
121
+ }
122
+ // Show warnings
123
+ for (const warn of checkResult.warnings) {
124
+ process.stdout.write(`Warning (${formatId}): ${warn}\n`);
125
+ }
126
+ // Convert
127
+ const files = adapter.convert(agent);
128
+ // Determine base directory
129
+ const baseDir = scope === 'user' ? os_1.default.homedir() : process.cwd();
130
+ // Install each file
131
+ for (const file of files) {
132
+ const fullDir = path_1.default.join(baseDir, file.installPath);
133
+ const fullPath = path_1.default.join(fullDir, file.filename);
134
+ if (options.dryRun) {
135
+ process.stdout.write(`Would install: ${fullPath}\n`);
136
+ process.stdout.write(`Content preview:\n${file.content.slice(0, 500)}...\n\n`);
137
+ continue;
138
+ }
139
+ // Create directory and write file
140
+ await promises_1.default.mkdir(fullDir, { recursive: true });
141
+ await promises_1.default.writeFile(fullPath, file.content);
142
+ // Track installation
143
+ const installedAgent = {
144
+ agent: `${org}/${parsed.name}`,
145
+ version: parsed.version,
146
+ format: formatId,
147
+ scope,
148
+ path: fullPath,
149
+ installedAt: new Date().toISOString(),
150
+ adapterVersion: adapter.version,
151
+ contentHash: (0, installed_1.computeHash)(file.content),
152
+ };
153
+ await (0, installed_1.trackInstall)(installedAgent);
154
+ process.stdout.write(`Installed: ${fullPath}\n`);
155
+ }
156
+ }
157
+ if (!options.dryRun) {
158
+ await (0, analytics_1.track)('cli_agent_install', {
159
+ agent: `${org}/${parsed.name}`,
160
+ formats: targetFormats,
161
+ scope,
162
+ });
163
+ process.stdout.write(`\nAgent installed successfully!\n`);
164
+ if (scope === 'user') {
165
+ process.stdout.write(`Available in all your projects.\n`);
166
+ }
167
+ else {
168
+ process.stdout.write(`Available in this project only.\n`);
169
+ }
170
+ }
171
+ });
172
+ }
@@ -234,6 +234,58 @@ async function downloadDependenciesRecursively(config, depStatuses, visited = ne
234
234
  }
235
235
  }
236
236
  }
237
+ /**
238
+ * Detect all available LLM providers from environment and server.
239
+ * Returns array of provider configs for fallback support.
240
+ */
241
+ async function detectAllLlmKeys(supportedProviders, config) {
242
+ const providers = [];
243
+ const seen = new Set();
244
+ // Check environment variables for all providers
245
+ for (const provider of supportedProviders) {
246
+ if (provider === 'any') {
247
+ // Check all known providers
248
+ for (const [p, envVar] of Object.entries(llm_1.PROVIDER_ENV_VARS)) {
249
+ const key = process.env[envVar];
250
+ if (key && !seen.has(p)) {
251
+ seen.add(p);
252
+ providers.push({ provider: p, apiKey: key, model: (0, llm_1.getDefaultModel)(p) });
253
+ }
254
+ }
255
+ }
256
+ else {
257
+ const envVar = llm_1.PROVIDER_ENV_VARS[provider];
258
+ if (envVar) {
259
+ const key = process.env[envVar];
260
+ if (key && !seen.has(provider)) {
261
+ seen.add(provider);
262
+ providers.push({ provider, apiKey: key, model: (0, llm_1.getDefaultModel)(provider) });
263
+ }
264
+ }
265
+ }
266
+ }
267
+ // Also check server keys if available
268
+ if (config?.apiKey) {
269
+ try {
270
+ const { fetchLlmKeys } = await Promise.resolve().then(() => __importStar(require('../lib/api')));
271
+ const serverKeys = await fetchLlmKeys(config);
272
+ for (const serverKey of serverKeys) {
273
+ if (!seen.has(serverKey.provider)) {
274
+ seen.add(serverKey.provider);
275
+ providers.push({
276
+ provider: serverKey.provider,
277
+ apiKey: serverKey.api_key,
278
+ model: serverKey.model || (0, llm_1.getDefaultModel)(serverKey.provider),
279
+ });
280
+ }
281
+ }
282
+ }
283
+ catch {
284
+ // Server fetch failed, continue with what we have
285
+ }
286
+ }
287
+ return providers;
288
+ }
237
289
  async function executePromptLocally(agentData, inputData, skillPrompts = [], config, providerOverride) {
238
290
  // If provider override specified, validate and use only that provider
239
291
  if (providerOverride) {
@@ -243,6 +295,44 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
243
295
  const providersToCheck = providerOverride
244
296
  ? [providerOverride]
245
297
  : agentData.supported_providers;
298
+ // Combine skill prompts with agent prompt (skills first, then agent)
299
+ let basePrompt = agentData.prompt || '';
300
+ if (skillPrompts.length > 0) {
301
+ basePrompt = [...skillPrompts, basePrompt].join('\n\n---\n\n');
302
+ }
303
+ // Build the prompt with input data (matches server behavior)
304
+ const prompt = (0, llm_1.buildPrompt)(basePrompt, inputData);
305
+ // When no provider override, detect all available providers for fallback support
306
+ if (!providerOverride) {
307
+ const allProviders = await detectAllLlmKeys(providersToCheck, config);
308
+ if (allProviders.length === 0) {
309
+ const providers = providersToCheck.join(', ');
310
+ throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
311
+ `Set an environment variable (e.g., OPENAI_API_KEY) or configure in web dashboard`);
312
+ }
313
+ // Apply agent default models to each provider config
314
+ const providersWithModels = allProviders.map((p) => ({
315
+ ...p,
316
+ model: agentData.default_models?.[p.provider] || p.model,
317
+ }));
318
+ // Show which provider is being used (primary)
319
+ const primary = providersWithModels[0];
320
+ if (providersWithModels.length > 1) {
321
+ process.stderr.write(`Running with ${primary.provider} (${primary.model}), ` +
322
+ `${providersWithModels.length - 1} fallback(s) available...\n`);
323
+ }
324
+ else {
325
+ process.stderr.write(`Running with ${primary.provider} (${primary.model})...\n`);
326
+ }
327
+ // Use fallback if multiple providers, otherwise single call
328
+ if (providersWithModels.length > 1) {
329
+ return await (0, llm_1.callLlmWithFallback)(providersWithModels, prompt, agentData.output_schema);
330
+ }
331
+ else {
332
+ return await (0, llm_1.callLlm)(primary.provider, primary.apiKey, primary.model, prompt, agentData.output_schema);
333
+ }
334
+ }
335
+ // Provider override: use single provider (existing behavior)
246
336
  const detected = await (0, llm_1.detectLlmKey)(providersToCheck, config);
247
337
  if (!detected) {
248
338
  const providers = providersToCheck.join(', ');
@@ -254,13 +344,6 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
254
344
  const model = serverModel || agentData.default_models?.[provider] || (0, llm_1.getDefaultModel)(provider);
255
345
  // Show which provider is being used (helpful for debugging rate limits)
256
346
  process.stderr.write(`Running with ${provider} (${model})...\n`);
257
- // Combine skill prompts with agent prompt (skills first, then agent)
258
- let basePrompt = agentData.prompt || '';
259
- if (skillPrompts.length > 0) {
260
- basePrompt = [...skillPrompts, basePrompt].join('\n\n---\n\n');
261
- }
262
- // Build the prompt with input data (matches server behavior)
263
- const prompt = (0, llm_1.buildPrompt)(basePrompt, inputData);
264
347
  // Call the LLM directly
265
348
  const response = await (0, llm_1.callLlm)(provider, key, model, prompt, agentData.output_schema);
266
349
  return response;
@@ -162,8 +162,29 @@ Instructions and guidance for AI agents...
162
162
  .description('Install skill to local AI tool directories (Claude Code, Cursor, etc.)')
163
163
  .option('--global', 'Install to home directory (default: current directory)')
164
164
  .option('--dry-run', 'Show what would be installed without making changes')
165
+ .option('--format <formats>', 'Comma-separated format IDs (e.g., claude-code,cursor)')
165
166
  .action(async (skillRef, options) => {
166
167
  const resolved = await (0, config_1.getResolvedConfig)();
168
+ // Determine target formats
169
+ let targetFormats = [];
170
+ if (options.format) {
171
+ targetFormats = options.format.split(',').map((f) => f.trim());
172
+ // Validate format IDs
173
+ const invalid = targetFormats.filter(f => !config_1.VALID_FORMAT_IDS.includes(f));
174
+ if (invalid.length > 0) {
175
+ throw new errors_1.CliError(`Invalid format ID(s): ${invalid.join(', ')}. Valid: ${config_1.VALID_FORMAT_IDS.join(', ')}`);
176
+ }
177
+ }
178
+ else {
179
+ const defaults = await (0, config_1.getDefaultFormats)();
180
+ if (defaults.length > 0) {
181
+ targetFormats = defaults;
182
+ }
183
+ }
184
+ // Determine target directories
185
+ const toolDirs = targetFormats.length > 0
186
+ ? targetFormats.map(f => config_1.FORMAT_SKILL_DIRS[f])
187
+ : AI_TOOL_SKILL_DIRS; // fallback to all
167
188
  const parsed = parseSkillRef(skillRef);
168
189
  const org = parsed.org ?? resolved.defaultOrg;
169
190
  if (!org) {
@@ -191,7 +212,7 @@ ${skillData.prompt}
191
212
  if (options.dryRun) {
192
213
  process.stdout.write(`Would install ${org}/${parsed.skill}@${parsed.version}\n\n`);
193
214
  process.stdout.write(`Target directories:\n`);
194
- for (const tool of AI_TOOL_SKILL_DIRS) {
215
+ for (const tool of toolDirs) {
195
216
  const skillDir = path_1.default.join(baseDir, tool.path);
196
217
  const skillFile = path_1.default.join(skillDir, `${parsed.skill}.md`);
197
218
  process.stdout.write(` - ${tool.name}: ${skillFile}\n`);
@@ -199,9 +220,9 @@ ${skillData.prompt}
199
220
  process.stdout.write(`\nNo changes made (dry run)\n`);
200
221
  return;
201
222
  }
202
- // Install to all AI tool directories
223
+ // Install to target AI tool directories
203
224
  const installed = [];
204
- for (const tool of AI_TOOL_SKILL_DIRS) {
225
+ for (const tool of toolDirs) {
205
226
  const skillDir = path_1.default.join(baseDir, tool.path);
206
227
  const skillFile = path_1.default.join(skillDir, `${parsed.skill}.md`);
207
228
  try {
@@ -0,0 +1,146 @@
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.registerUpdateCommand = registerUpdateCommand;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const config_1 = require("../lib/config");
11
+ const api_1 = require("../lib/api");
12
+ const analytics_1 = require("../lib/analytics");
13
+ const adapters_1 = require("../adapters");
14
+ const installed_1 = require("../lib/installed");
15
+ async function fetchLatestAgent(config, agentRef) {
16
+ const [org, name] = agentRef.split('/');
17
+ if (!org || !name)
18
+ return null;
19
+ try {
20
+ // Try to get latest version
21
+ const agent = await (0, api_1.publicRequest)(config, `/public/agents/${org}/${name}/latest`);
22
+ return {
23
+ agent: { ...agent, org_slug: org },
24
+ latestVersion: agent.version
25
+ };
26
+ }
27
+ catch (err) {
28
+ if (err instanceof api_1.ApiError && err.status === 404) {
29
+ return null;
30
+ }
31
+ throw err;
32
+ }
33
+ }
34
+ function registerUpdateCommand(program) {
35
+ program
36
+ .command('update [agent]')
37
+ .description('Update installed agents to latest versions')
38
+ .option('--check', 'Check for updates without installing')
39
+ .option('--force', 'Force update even if file was modified locally')
40
+ .action(async (agentRef, options) => {
41
+ const resolved = await (0, config_1.getResolvedConfig)();
42
+ const installed = await (0, installed_1.getInstalled)();
43
+ if (installed.length === 0) {
44
+ process.stdout.write('No agents installed. Use "orch install <agent>" to install agents.\n');
45
+ return;
46
+ }
47
+ // Filter to specific agent if provided
48
+ const toCheck = agentRef
49
+ ? installed.filter(i => i.agent === agentRef)
50
+ : installed;
51
+ if (toCheck.length === 0) {
52
+ process.stdout.write(`Agent "${agentRef}" is not installed.\n`);
53
+ return;
54
+ }
55
+ process.stdout.write(`Checking ${toCheck.length} installed agent(s) for updates...\n\n`);
56
+ let updatesAvailable = 0;
57
+ let updatesApplied = 0;
58
+ let skippedModified = 0;
59
+ for (const item of toCheck) {
60
+ // Check if file was modified locally
61
+ const wasModified = await (0, installed_1.checkModified)(item);
62
+ // Fetch latest version
63
+ const latest = await fetchLatestAgent(resolved, item.agent);
64
+ if (!latest) {
65
+ process.stdout.write(` ${chalk_1.default.yellow('?')} ${item.agent} - could not fetch latest\n`);
66
+ continue;
67
+ }
68
+ const hasUpdate = latest.latestVersion !== item.version;
69
+ if (!hasUpdate && !wasModified) {
70
+ process.stdout.write(` ${chalk_1.default.green('✓')} ${item.agent}@${item.version} - up to date\n`);
71
+ continue;
72
+ }
73
+ if (wasModified && !options.force) {
74
+ process.stdout.write(` ${chalk_1.default.yellow('!')} ${item.agent} - local modifications (use --force to overwrite)\n`);
75
+ skippedModified++;
76
+ continue;
77
+ }
78
+ if (hasUpdate) {
79
+ updatesAvailable++;
80
+ process.stdout.write(` ${chalk_1.default.blue('↑')} ${item.agent}@${item.version} → ${latest.latestVersion}`);
81
+ if (wasModified) {
82
+ process.stdout.write(` ${chalk_1.default.yellow('(modified)')}`);
83
+ }
84
+ process.stdout.write('\n');
85
+ if (options.check) {
86
+ continue;
87
+ }
88
+ // Apply update
89
+ const adapter = adapters_1.adapterRegistry.get(item.format);
90
+ if (!adapter) {
91
+ process.stderr.write(` Skipped: Unknown format "${item.format}"\n`);
92
+ continue;
93
+ }
94
+ const checkResult = adapter.canConvert(latest.agent);
95
+ if (!checkResult.canConvert) {
96
+ process.stderr.write(` Skipped: ${checkResult.errors.join(', ')}\n`);
97
+ continue;
98
+ }
99
+ const files = adapter.convert(latest.agent);
100
+ for (const file of files) {
101
+ // Use the original path from tracking
102
+ const fullPath = item.path;
103
+ try {
104
+ const dir = path_1.default.dirname(fullPath);
105
+ await promises_1.default.mkdir(dir, { recursive: true });
106
+ await promises_1.default.writeFile(fullPath, file.content);
107
+ // Update tracking
108
+ const updatedItem = {
109
+ ...item,
110
+ version: latest.latestVersion,
111
+ installedAt: new Date().toISOString(),
112
+ adapterVersion: adapter.version,
113
+ contentHash: (0, installed_1.computeHash)(file.content),
114
+ };
115
+ await (0, installed_1.trackInstall)(updatedItem);
116
+ process.stdout.write(` Updated: ${fullPath}\n`);
117
+ updatesApplied++;
118
+ }
119
+ catch (err) {
120
+ process.stderr.write(` Error: ${err.message}\n`);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ process.stdout.write('\n');
126
+ if (options.check) {
127
+ process.stdout.write(`Found ${updatesAvailable} update(s) available.\n`);
128
+ if (skippedModified > 0) {
129
+ process.stdout.write(`${skippedModified} agent(s) have local modifications.\n`);
130
+ }
131
+ process.stdout.write('Run "orch update" without --check to apply updates.\n');
132
+ }
133
+ else {
134
+ process.stdout.write(`Applied ${updatesApplied} update(s).\n`);
135
+ if (skippedModified > 0) {
136
+ process.stdout.write(`Skipped ${skippedModified} modified agent(s). Use --force to overwrite.\n`);
137
+ }
138
+ }
139
+ await (0, analytics_1.track)('cli_agent_update', {
140
+ checked: toCheck.length,
141
+ updatesAvailable,
142
+ updatesApplied,
143
+ skippedModified,
144
+ });
145
+ });
146
+ }
package/dist/lib/api.js CHANGED
@@ -59,6 +59,8 @@ exports.previewAgentVersion = previewAgentVersion;
59
59
  exports.reportInstall = reportInstall;
60
60
  const errors_1 = require("./errors");
61
61
  const DEFAULT_TIMEOUT_MS = 15000;
62
+ const MAX_RETRIES = 3;
63
+ const BASE_DELAY_MS = 1000;
62
64
  async function safeFetch(url, options) {
63
65
  try {
64
66
  return await fetch(url, {
@@ -74,6 +76,39 @@ async function safeFetch(url, options) {
74
76
  throw new errors_1.NetworkError(url, err instanceof Error ? err : undefined);
75
77
  }
76
78
  }
79
+ async function safeFetchWithRetry(url, options) {
80
+ let lastError;
81
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
82
+ try {
83
+ const response = await safeFetch(url, options);
84
+ // Don't retry client errors (except 429)
85
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
86
+ return response;
87
+ }
88
+ // Retry on 5xx or 429
89
+ if (response.status >= 500 || response.status === 429) {
90
+ if (attempt < MAX_RETRIES) {
91
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
92
+ const jitter = Math.random() * 500;
93
+ process.stderr.write(`Request failed (${response.status}), retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
94
+ await new Promise(r => setTimeout(r, delay + jitter));
95
+ continue;
96
+ }
97
+ }
98
+ return response;
99
+ }
100
+ catch (error) {
101
+ lastError = error;
102
+ if (attempt < MAX_RETRIES) {
103
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
104
+ const jitter = Math.random() * 500;
105
+ process.stderr.write(`Network error, retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
106
+ await new Promise(r => setTimeout(r, delay + jitter));
107
+ }
108
+ }
109
+ }
110
+ throw lastError ?? new errors_1.NetworkError(url);
111
+ }
77
112
  class ApiError extends Error {
78
113
  status;
79
114
  payload;
@@ -108,7 +143,7 @@ async function request(config, method, path, options = {}) {
108
143
  if (!config.apiKey) {
109
144
  throw new ApiError('Missing API key. Run `orchagent login` first.', 401);
110
145
  }
111
- const response = await safeFetch(buildUrl(config.apiUrl, path), {
146
+ const response = await safeFetchWithRetry(buildUrl(config.apiUrl, path), {
112
147
  method,
113
148
  headers: {
114
149
  Authorization: `Bearer ${config.apiKey}`,
@@ -122,7 +157,7 @@ async function request(config, method, path, options = {}) {
122
157
  return (await response.json());
123
158
  }
124
159
  async function publicRequest(config, path) {
125
- const response = await safeFetch(buildUrl(config.apiUrl, path));
160
+ const response = await safeFetchWithRetry(buildUrl(config.apiUrl, path));
126
161
  if (!response.ok) {
127
162
  throw await parseError(response);
128
163
  }
@@ -3,16 +3,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.FORMAT_SKILL_DIRS = exports.VALID_FORMAT_IDS = void 0;
6
7
  exports.loadConfig = loadConfig;
7
8
  exports.saveConfig = saveConfig;
8
9
  exports.getResolvedConfig = getResolvedConfig;
9
10
  exports.getConfigPath = getConfigPath;
11
+ exports.getDefaultFormats = getDefaultFormats;
12
+ exports.setDefaultFormats = setDefaultFormats;
10
13
  const promises_1 = __importDefault(require("fs/promises"));
11
14
  const path_1 = __importDefault(require("path"));
12
15
  const os_1 = __importDefault(require("os"));
13
16
  const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.orchagent');
14
17
  const CONFIG_PATH = path_1.default.join(CONFIG_DIR, 'config.json');
15
18
  const DEFAULT_API_URL = 'https://api.orchagent.io';
19
+ // Valid format IDs for multi-format agent export
20
+ exports.VALID_FORMAT_IDS = ['claude-code', 'cursor', 'codex', 'amp', 'opencode', 'antigravity'];
21
+ // Map format IDs to skill directories (used by skill install and agent install)
22
+ exports.FORMAT_SKILL_DIRS = {
23
+ 'claude-code': { name: 'Claude Code', path: '.claude/skills' },
24
+ 'cursor': { name: 'Cursor', path: '.cursor/skills' },
25
+ 'codex': { name: 'Codex', path: '.codex/skills' },
26
+ 'amp': { name: 'Amp', path: '.agents/skills' },
27
+ 'opencode': { name: 'OpenCode', path: '.opencode/skill' },
28
+ 'antigravity': { name: 'Antigravity', path: '.agent/skills' },
29
+ };
16
30
  async function loadConfig() {
17
31
  try {
18
32
  const raw = await promises_1.default.readFile(CONFIG_PATH, 'utf-8');
@@ -56,3 +70,12 @@ async function getResolvedConfig(overrides = {}, profile) {
56
70
  function getConfigPath() {
57
71
  return CONFIG_PATH;
58
72
  }
73
+ async function getDefaultFormats() {
74
+ const config = await loadConfig();
75
+ return config.default_formats ?? [];
76
+ }
77
+ async function setDefaultFormats(formats) {
78
+ const config = await loadConfig();
79
+ config.default_formats = formats;
80
+ await saveConfig(config);
81
+ }
@@ -0,0 +1,94 @@
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.computeHash = computeHash;
7
+ exports.loadInstalled = loadInstalled;
8
+ exports.saveInstalled = saveInstalled;
9
+ exports.trackInstall = trackInstall;
10
+ exports.getInstalled = getInstalled;
11
+ exports.getInstalledByFormat = getInstalledByFormat;
12
+ exports.checkModified = checkModified;
13
+ exports.untrackInstall = untrackInstall;
14
+ const promises_1 = __importDefault(require("fs/promises"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const os_1 = __importDefault(require("os"));
17
+ const crypto_1 = __importDefault(require("crypto"));
18
+ const ORCHAGENT_DIR = path_1.default.join(os_1.default.homedir(), '.orchagent');
19
+ const INSTALLED_PATH = path_1.default.join(ORCHAGENT_DIR, 'installed.json');
20
+ /**
21
+ * Compute SHA-256 hash of content
22
+ */
23
+ function computeHash(content) {
24
+ return crypto_1.default.createHash('sha256').update(content).digest('hex');
25
+ }
26
+ /**
27
+ * Load installed agents from tracking file
28
+ */
29
+ async function loadInstalled() {
30
+ try {
31
+ const raw = await promises_1.default.readFile(INSTALLED_PATH, 'utf-8');
32
+ return JSON.parse(raw);
33
+ }
34
+ catch (err) {
35
+ if (err.code === 'ENOENT') {
36
+ return { version: 1, installed: [] };
37
+ }
38
+ throw err;
39
+ }
40
+ }
41
+ /**
42
+ * Save installed agents to tracking file
43
+ */
44
+ async function saveInstalled(data) {
45
+ await promises_1.default.mkdir(ORCHAGENT_DIR, { recursive: true });
46
+ await promises_1.default.writeFile(INSTALLED_PATH, JSON.stringify(data, null, 2) + '\n');
47
+ }
48
+ /**
49
+ * Track a newly installed agent
50
+ */
51
+ async function trackInstall(agent) {
52
+ const data = await loadInstalled();
53
+ // Remove any existing entry for same agent/format/scope/path
54
+ data.installed = data.installed.filter(i => !(i.agent === agent.agent && i.format === agent.format && i.path === agent.path));
55
+ // Add new entry
56
+ data.installed.push(agent);
57
+ await saveInstalled(data);
58
+ }
59
+ /**
60
+ * Get all installed agents
61
+ */
62
+ async function getInstalled() {
63
+ const data = await loadInstalled();
64
+ return data.installed;
65
+ }
66
+ /**
67
+ * Get installed agents filtered by format
68
+ */
69
+ async function getInstalledByFormat(format) {
70
+ const data = await loadInstalled();
71
+ return data.installed.filter(i => i.format === format);
72
+ }
73
+ /**
74
+ * Check if a file has been modified since installation
75
+ */
76
+ async function checkModified(installed) {
77
+ try {
78
+ const content = await promises_1.default.readFile(installed.path, 'utf-8');
79
+ const currentHash = computeHash(content);
80
+ return currentHash !== installed.contentHash;
81
+ }
82
+ catch {
83
+ // File doesn't exist or can't be read
84
+ return true;
85
+ }
86
+ }
87
+ /**
88
+ * Remove an installed agent from tracking
89
+ */
90
+ async function untrackInstall(agentRef, format, filePath) {
91
+ const data = await loadInstalled();
92
+ data.installed = data.installed.filter(i => !(i.agent === agentRef && i.format === format && i.path === filePath));
93
+ await saveInstalled(data);
94
+ }
package/dist/lib/llm.js CHANGED
@@ -39,15 +39,28 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.DEFAULT_MODELS = exports.PROVIDER_ENV_VARS = void 0;
42
+ exports.DEFAULT_MODELS = exports.PROVIDER_ENV_VARS = exports.LlmError = void 0;
43
+ exports.isRateLimitError = isRateLimitError;
43
44
  exports.detectLlmKeyFromEnv = detectLlmKeyFromEnv;
44
45
  exports.detectLlmKey = detectLlmKey;
45
46
  exports.getDefaultModel = getDefaultModel;
46
47
  exports.buildPrompt = buildPrompt;
47
48
  exports.callLlm = callLlm;
49
+ exports.callLlmWithFallback = callLlmWithFallback;
48
50
  exports.validateProvider = validateProvider;
49
51
  const errors_1 = require("./errors");
50
52
  const llm_errors_1 = require("./llm-errors");
53
+ class LlmError extends errors_1.CliError {
54
+ statusCode;
55
+ constructor(message, statusCode) {
56
+ super(message);
57
+ this.statusCode = statusCode;
58
+ }
59
+ }
60
+ exports.LlmError = LlmError;
61
+ function isRateLimitError(error) {
62
+ return error instanceof LlmError && error.statusCode === 429;
63
+ }
51
64
  // Environment variable names for each provider
52
65
  exports.PROVIDER_ENV_VARS = {
53
66
  openai: 'OPENAI_API_KEY',
@@ -169,6 +182,27 @@ async function callLlm(provider, apiKey, model, prompt, outputSchema) {
169
182
  }
170
183
  throw new errors_1.CliError(`Unsupported provider: ${provider}`);
171
184
  }
185
+ /**
186
+ * Call LLM with automatic fallback to next provider on rate limit.
187
+ * Tries each provider in order until one succeeds.
188
+ */
189
+ async function callLlmWithFallback(providers, prompt, outputSchema) {
190
+ let lastError;
191
+ for (const { provider, apiKey, model } of providers) {
192
+ try {
193
+ return await callLlm(provider, apiKey, model, prompt, outputSchema);
194
+ }
195
+ catch (error) {
196
+ lastError = error;
197
+ if (isRateLimitError(error)) {
198
+ process.stderr.write(`${provider} rate-limited, trying next provider...\n`);
199
+ continue;
200
+ }
201
+ throw error; // Don't retry non-rate-limit errors
202
+ }
203
+ }
204
+ throw lastError ?? new errors_1.CliError('All LLM providers failed');
205
+ }
172
206
  async function callOpenAI(apiKey, model, prompt, outputSchema) {
173
207
  const body = {
174
208
  model,
@@ -187,7 +221,8 @@ async function callOpenAI(apiKey, model, prompt, outputSchema) {
187
221
  });
188
222
  if (!response.ok) {
189
223
  const text = await response.text();
190
- throw (0, llm_errors_1.parseLlmError)('openai', text, response.status);
224
+ const parsed = (0, llm_errors_1.parseLlmError)('openai', text, response.status);
225
+ throw new LlmError(parsed.message, response.status);
191
226
  }
192
227
  const data = (await response.json());
193
228
  const content = data.choices?.[0]?.message?.content || '';
@@ -214,7 +249,8 @@ async function callAnthropic(apiKey, model, prompt, _outputSchema) {
214
249
  });
215
250
  if (!response.ok) {
216
251
  const text = await response.text();
217
- throw (0, llm_errors_1.parseLlmError)('anthropic', text, response.status);
252
+ const parsed = (0, llm_errors_1.parseLlmError)('anthropic', text, response.status);
253
+ throw new LlmError(parsed.message, response.status);
218
254
  }
219
255
  const data = (await response.json());
220
256
  const content = data.content?.[0]?.text || '';
@@ -236,7 +272,8 @@ async function callGemini(apiKey, model, prompt, _outputSchema) {
236
272
  });
237
273
  if (!response.ok) {
238
274
  const text = await response.text();
239
- throw (0, llm_errors_1.parseLlmError)('gemini', text, response.status);
275
+ const parsed = (0, llm_errors_1.parseLlmError)('gemini', text, response.status);
276
+ throw new LlmError(parsed.message, response.status);
240
277
  }
241
278
  const data = (await response.json());
242
279
  const content = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
@@ -264,10 +301,11 @@ async function callOllama(endpoint, model, prompt, _outputSchema) {
264
301
  });
265
302
  if (!response.ok) {
266
303
  if (response.status === 404) {
267
- throw new errors_1.CliError(`Model '${model}' not found. Run: ollama pull ${model}`);
304
+ throw new LlmError(`Model '${model}' not found. Run: ollama pull ${model}`, 404);
268
305
  }
269
306
  const text = await response.text();
270
- throw (0, llm_errors_1.parseLlmError)('ollama', text, response.status);
307
+ const parsed = (0, llm_errors_1.parseLlmError)('ollama', text, response.status);
308
+ throw new LlmError(parsed.message, response.status);
271
309
  }
272
310
  const data = await response.json();
273
311
  const content = data.choices?.[0]?.message?.content || '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.2.25",
3
+ "version": "0.3.0",
4
4
  "description": "Command-line interface for the orchagent AI agent marketplace",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",