@orchagent/cli 0.2.13 → 0.2.15

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.
@@ -18,9 +18,8 @@ function registerDoctorCommand(program) {
18
18
  else {
19
19
  (0, doctor_1.printHumanOutput)(results, summary, options.verbose ?? false);
20
20
  }
21
- // Exit with code 1 if any errors or warnings (enables scripting)
22
- const hasIssues = summary.errors > 0 || summary.warnings > 0;
23
- if (hasIssues) {
21
+ // Exit with code 1 only if there are errors (not warnings)
22
+ if (summary.errors > 0) {
24
23
  process.exit(1);
25
24
  }
26
25
  });
@@ -160,6 +160,7 @@ Instructions and guidance for AI agents...
160
160
  .command('install <skill>')
161
161
  .description('Install skill to local AI tool directories (Claude Code, Cursor, etc.)')
162
162
  .option('--global', 'Install to home directory (default: current directory)')
163
+ .option('--dry-run', 'Show what would be installed without making changes')
163
164
  .action(async (skillRef, options) => {
164
165
  const resolved = await (0, config_1.getResolvedConfig)();
165
166
  const parsed = parseSkillRef(skillRef);
@@ -185,6 +186,18 @@ ${skillData.description || ''}
185
186
 
186
187
  ${skillData.prompt}
187
188
  `;
189
+ // Dry run - show what would be installed
190
+ if (options.dryRun) {
191
+ process.stdout.write(`Would install ${org}/${parsed.skill}@${parsed.version}\n\n`);
192
+ process.stdout.write(`Target directories:\n`);
193
+ for (const tool of AI_TOOL_SKILL_DIRS) {
194
+ const skillDir = path_1.default.join(baseDir, tool.path);
195
+ const skillFile = path_1.default.join(skillDir, `${parsed.skill}.md`);
196
+ process.stdout.write(` - ${tool.name}: ${skillFile}\n`);
197
+ }
198
+ process.stdout.write(`\nNo changes made (dry run)\n`);
199
+ return;
200
+ }
188
201
  // Install to all AI tool directories
189
202
  const installed = [];
190
203
  for (const tool of AI_TOOL_SKILL_DIRS) {
@@ -77,6 +77,10 @@ function registerStatusCommand(program) {
77
77
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
78
78
  }
79
79
  const data = (await response.json());
80
+ // Handle missing or malformed services array
81
+ if (!data.services || !Array.isArray(data.services)) {
82
+ data.services = [];
83
+ }
80
84
  if (options.json) {
81
85
  (0, output_1.printJson)(data);
82
86
  return;
@@ -101,8 +101,17 @@ async function checkLocalLlmEnvVars() {
101
101
  }
102
102
  /**
103
103
  * Run all LLM configuration checks.
104
+ * If server keys are configured, local keys warning becomes informational.
104
105
  */
105
106
  async function runLlmChecks() {
106
- const results = await Promise.all([checkServerLlmKeys(), checkLocalLlmEnvVars()]);
107
- return results;
107
+ const serverResult = await checkServerLlmKeys();
108
+ const localResult = await checkLocalLlmEnvVars();
109
+ // If server keys are configured, downgrade local keys warning to info
110
+ // Users who only use server-side calls don't need local keys
111
+ if (serverResult.status === 'success' && localResult.status === 'warning') {
112
+ localResult.status = 'info';
113
+ localResult.message = 'No local LLM API keys (using server keys)';
114
+ localResult.fix = undefined;
115
+ }
116
+ return [serverResult, localResult];
108
117
  }
@@ -11,6 +11,7 @@ const SYMBOLS = {
11
11
  success: chalk_1.default.green('\u2713'), // checkmark
12
12
  warning: chalk_1.default.yellow('\u26a0'), // warning sign
13
13
  error: chalk_1.default.red('\u2717'), // X mark
14
+ info: chalk_1.default.blue('\u2139'), // info sign
14
15
  };
15
16
  // Category display names
16
17
  const CATEGORY_NAMES = {
@@ -65,10 +66,10 @@ function printHumanOutput(results, summary, verbose) {
65
66
  const displayName = CATEGORY_NAMES[category] || category;
66
67
  process.stdout.write(chalk_1.default.bold(`${displayName}\n`));
67
68
  for (const check of checks) {
68
- const symbol = SYMBOLS[check.status];
69
+ const symbol = SYMBOLS[check.status] || SYMBOLS.info;
69
70
  process.stdout.write(` ${symbol} ${check.message}\n`);
70
- // Show fix suggestion for warnings/errors
71
- if (check.fix && check.status !== 'success') {
71
+ // Show fix suggestion for warnings/errors (not for success/info)
72
+ if (check.fix && (check.status === 'warning' || check.status === 'error')) {
72
73
  process.stdout.write(chalk_1.default.dim(` \u2192 ${check.fix}\n`));
73
74
  }
74
75
  // Show details in verbose mode
@@ -58,10 +58,11 @@ async function runAllChecks() {
58
58
  }
59
59
  /**
60
60
  * Calculate summary statistics from check results.
61
+ * 'info' status counts as passed (informational, not a problem).
61
62
  */
62
63
  function calculateSummary(results) {
63
64
  return {
64
- passed: results.filter((r) => r.status === 'success').length,
65
+ passed: results.filter((r) => r.status === 'success' || r.status === 'info').length,
65
66
  warnings: results.filter((r) => r.status === 'warning').length,
66
67
  errors: results.filter((r) => r.status === 'error').length,
67
68
  };
@@ -28,6 +28,12 @@ const FALLBACKS = {
28
28
  502: 'Gemini is temporarily unavailable.',
29
29
  503: 'Gemini is overloaded. Try again later.',
30
30
  },
31
+ ollama: {
32
+ 401: 'Authentication error (Ollama typically does not require auth)',
33
+ 404: 'Model not found. Run: ollama pull <model>',
34
+ 500: 'Ollama server error',
35
+ 502: 'Cannot connect to Ollama. Is it running?',
36
+ },
31
37
  };
32
38
  const DEFAULT = 'LLM provider error. Check your API key and try again.';
33
39
  function isHtml(text) {
@@ -67,13 +73,25 @@ function parseGemini(text, status) {
67
73
  catch { }
68
74
  return FALLBACKS.gemini[status] || DEFAULT;
69
75
  }
76
+ function parseOllama(text, status) {
77
+ // Ollama uses OpenAI-compatible error format
78
+ try {
79
+ const p = JSON.parse(text);
80
+ const msg = p.error?.message || (typeof p.error === 'string' ? p.error : null);
81
+ if (msg)
82
+ return sanitize(msg);
83
+ }
84
+ catch { }
85
+ return FALLBACKS.ollama[status] || DEFAULT;
86
+ }
70
87
  function parseLlmError(provider, text, status) {
71
88
  if (isHtml(text)) {
72
89
  return new errors_1.CliError(`${provider} error: ${FALLBACKS[provider][status] || DEFAULT}`);
73
90
  }
74
91
  const msg = provider === 'openai' ? parseOpenAI(text, status)
75
92
  : provider === 'anthropic' ? parseAnthropic(text, status)
76
- : parseGemini(text, status);
93
+ : provider === 'ollama' ? parseOllama(text, status)
94
+ : parseGemini(text, status);
77
95
  const display = provider.charAt(0).toUpperCase() + provider.slice(1);
78
96
  return new errors_1.CliError(`${display} API error: ${msg}`);
79
97
  }
package/dist/lib/llm.js CHANGED
@@ -53,12 +53,14 @@ exports.PROVIDER_ENV_VARS = {
53
53
  openai: 'OPENAI_API_KEY',
54
54
  anthropic: 'ANTHROPIC_API_KEY',
55
55
  gemini: 'GEMINI_API_KEY',
56
+ ollama: 'OLLAMA_HOST',
56
57
  };
57
58
  // Default models for each provider
58
59
  exports.DEFAULT_MODELS = {
59
60
  openai: 'gpt-4o',
60
61
  anthropic: 'claude-sonnet-4-20250514',
61
62
  gemini: 'gemini-1.5-pro',
63
+ ollama: 'llama3.2',
62
64
  };
63
65
  /**
64
66
  * Detect LLM API key from environment variables based on supported providers.
@@ -162,6 +164,9 @@ async function callLlm(provider, apiKey, model, prompt, outputSchema) {
162
164
  else if (provider === 'gemini') {
163
165
  return callGemini(apiKey, model, prompt, outputSchema);
164
166
  }
167
+ else if (provider === 'ollama') {
168
+ return callOllama(apiKey, model, prompt, outputSchema);
169
+ }
165
170
  throw new errors_1.CliError(`Unsupported provider: ${provider}`);
166
171
  }
167
172
  async function callOpenAI(apiKey, model, prompt, outputSchema) {
@@ -242,11 +247,42 @@ async function callGemini(apiKey, model, prompt, _outputSchema) {
242
247
  return { result: content };
243
248
  }
244
249
  }
250
+ async function callOllama(endpoint, model, prompt, _outputSchema) {
251
+ // endpoint is passed via apiKey param for Ollama (it's the OLLAMA_HOST)
252
+ const baseUrl = endpoint || 'http://localhost:11434';
253
+ // Ensure /v1 path
254
+ const normalizedBase = baseUrl.endsWith('/v1') ? baseUrl : `${baseUrl.replace(/\/$/, '')}/v1`;
255
+ const url = `${normalizedBase}/chat/completions`;
256
+ const response = await fetch(url, {
257
+ method: 'POST',
258
+ headers: { 'Content-Type': 'application/json' },
259
+ body: JSON.stringify({
260
+ model,
261
+ messages: [{ role: 'user', content: prompt }],
262
+ options: { num_ctx: 8192 },
263
+ }),
264
+ });
265
+ if (!response.ok) {
266
+ if (response.status === 404) {
267
+ throw new errors_1.CliError(`Model '${model}' not found. Run: ollama pull ${model}`);
268
+ }
269
+ const text = await response.text();
270
+ throw (0, llm_errors_1.parseLlmError)('ollama', text, response.status);
271
+ }
272
+ const data = await response.json();
273
+ const content = data.choices?.[0]?.message?.content || '';
274
+ try {
275
+ return JSON.parse(content);
276
+ }
277
+ catch {
278
+ return { result: content };
279
+ }
280
+ }
245
281
  /**
246
282
  * Validate a provider string against known providers.
247
283
  */
248
284
  function validateProvider(provider) {
249
- const validProviders = ['openai', 'anthropic', 'gemini'];
285
+ const validProviders = ['openai', 'anthropic', 'gemini', 'ollama'];
250
286
  if (!validProviders.includes(provider)) {
251
287
  throw new errors_1.CliError(`Invalid provider: ${provider}. Valid: ${validProviders.join(', ')}`);
252
288
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "Command-line interface for the OrchAgent AI agent marketplace",
5
5
  "license": "MIT",
6
6
  "author": "OrchAgent <hello@orchagent.io>",