@orchagent/cli 0.3.62 → 0.3.63

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.
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ if (process.env.SENTRY_DSN) {
44
44
  const commander_1 = require("commander");
45
45
  const commands_1 = require("./commands");
46
46
  const errors_1 = require("./lib/errors");
47
+ const suggest_1 = require("./lib/suggest");
47
48
  const analytics_1 = require("./lib/analytics");
48
49
  const config_1 = require("./lib/config");
49
50
  const spinner_1 = require("./lib/spinner");
@@ -73,6 +74,7 @@ Documentation: https://docs.orchagent.io
73
74
  orchagent docs agents Building agents guide
74
75
  `);
75
76
  (0, commands_1.registerCommands)(program);
77
+ (0, suggest_1.enhanceUnknownOptionSuggestions)(program);
76
78
  // Initialize progress setting before parsing
77
79
  async function main() {
78
80
  // Check config for no_progress setting
package/dist/lib/api.js CHANGED
@@ -109,13 +109,38 @@ async function safeFetchWithRetryForCalls(url, options) {
109
109
  }
110
110
  // Retry on 5xx or 429
111
111
  if (response.status >= 500 || response.status === 429) {
112
+ // Read body to check if error is retryable
113
+ const bodyText = await response.text().catch(() => '');
114
+ let parsed = null;
115
+ try {
116
+ parsed = JSON.parse(bodyText);
117
+ }
118
+ catch { /* ignore */ }
119
+ const detail = parsed?.error?.message ||
120
+ parsed?.message || '';
121
+ const isRetryable = parsed?.error?.is_retryable;
122
+ // Don't retry if server explicitly says error is not retryable
123
+ if (isRetryable === false) {
124
+ return new Response(bodyText, {
125
+ status: response.status,
126
+ statusText: response.statusText,
127
+ headers: response.headers,
128
+ });
129
+ }
112
130
  if (attempt < MAX_RETRIES) {
113
131
  const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
114
132
  const jitter = Math.random() * 500;
115
- process.stderr.write(`Request failed (${response.status}), retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
133
+ const detailSuffix = detail ? `: ${detail}` : '';
134
+ process.stderr.write(`Request failed (${response.status}${detailSuffix}), retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
116
135
  await new Promise(r => setTimeout(r, delay + jitter));
117
136
  continue;
118
137
  }
138
+ // Last attempt — return reconstructed response (body was consumed)
139
+ return new Response(bodyText, {
140
+ status: response.status,
141
+ statusText: response.statusText,
142
+ headers: response.headers,
143
+ });
119
144
  }
120
145
  return response;
121
146
  }
@@ -142,13 +167,38 @@ async function safeFetchWithRetry(url, options) {
142
167
  }
143
168
  // Retry on 5xx or 429
144
169
  if (response.status >= 500 || response.status === 429) {
170
+ // Read body to check if error is retryable
171
+ const bodyText = await response.text().catch(() => '');
172
+ let parsed = null;
173
+ try {
174
+ parsed = JSON.parse(bodyText);
175
+ }
176
+ catch { /* ignore */ }
177
+ const detail = parsed?.error?.message ||
178
+ parsed?.message || '';
179
+ const isRetryable = parsed?.error?.is_retryable;
180
+ // Don't retry if server explicitly says error is not retryable
181
+ if (isRetryable === false) {
182
+ return new Response(bodyText, {
183
+ status: response.status,
184
+ statusText: response.statusText,
185
+ headers: response.headers,
186
+ });
187
+ }
145
188
  if (attempt < MAX_RETRIES) {
146
189
  const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
147
190
  const jitter = Math.random() * 500;
148
- process.stderr.write(`Request failed (${response.status}), retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
191
+ const detailSuffix = detail ? `: ${detail}` : '';
192
+ process.stderr.write(`Request failed (${response.status}${detailSuffix}), retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
149
193
  await new Promise(r => setTimeout(r, delay + jitter));
150
194
  continue;
151
195
  }
196
+ // Last attempt — return reconstructed response (body was consumed)
197
+ return new Response(bodyText, {
198
+ status: response.status,
199
+ statusText: response.statusText,
200
+ headers: response.headers,
201
+ });
152
202
  }
153
203
  return response;
154
204
  }
@@ -0,0 +1,64 @@
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.parseDotEnv = parseDotEnv;
7
+ exports.loadDotEnv = loadDotEnv;
8
+ exports.mergeEnv = mergeEnv;
9
+ const promises_1 = __importDefault(require("fs/promises"));
10
+ const path_1 = __importDefault(require("path"));
11
+ /**
12
+ * Parse a .env file into key-value pairs.
13
+ * Handles: comments (#), blank lines, KEY=VALUE, single/double quoted values.
14
+ * Does NOT support multiline values or variable expansion.
15
+ */
16
+ function parseDotEnv(content) {
17
+ const vars = {};
18
+ for (const rawLine of content.split('\n')) {
19
+ const line = rawLine.trim();
20
+ if (!line || line.startsWith('#'))
21
+ continue;
22
+ const eqIndex = line.indexOf('=');
23
+ if (eqIndex === -1)
24
+ continue;
25
+ const key = line.slice(0, eqIndex).trim();
26
+ if (!key)
27
+ continue;
28
+ let value = line.slice(eqIndex + 1).trim();
29
+ // Strip matching quotes
30
+ if ((value.startsWith('"') && value.endsWith('"')) ||
31
+ (value.startsWith("'") && value.endsWith("'"))) {
32
+ value = value.slice(1, -1);
33
+ }
34
+ vars[key] = value;
35
+ }
36
+ return vars;
37
+ }
38
+ /**
39
+ * Load .env file from a directory. Returns the parsed vars (empty object if no file).
40
+ * Does NOT modify process.env — caller decides how to merge.
41
+ */
42
+ async function loadDotEnv(dir) {
43
+ const envPath = path_1.default.join(dir, '.env');
44
+ try {
45
+ const content = await promises_1.default.readFile(envPath, 'utf-8');
46
+ return parseDotEnv(content);
47
+ }
48
+ catch {
49
+ return {};
50
+ }
51
+ }
52
+ /**
53
+ * Merge .env vars into an env object. Existing keys take precedence
54
+ * (process.env wins over .env file, matching standard dotenv behaviour).
55
+ */
56
+ function mergeEnv(base, dotEnvVars) {
57
+ const merged = { ...base };
58
+ for (const [key, value] of Object.entries(dotEnvVars)) {
59
+ if (!(key in merged) || merged[key] === undefined) {
60
+ merged[key] = value;
61
+ }
62
+ }
63
+ return merged;
64
+ }
@@ -58,7 +58,13 @@ function formatError(err) {
58
58
  if (err instanceof Error) {
59
59
  const anyErr = err;
60
60
  if (anyErr.status && anyErr.payload) {
61
- return `${anyErr.message} (status ${anyErr.status})`;
61
+ const p = anyErr.payload;
62
+ const code = p.error?.code;
63
+ const detail = p.error?.detail || p.detail;
64
+ let msg = `${anyErr.message} (status ${anyErr.status}${code ? `, ${code}` : ''})`;
65
+ if (detail)
66
+ msg += `\n${detail}`;
67
+ return msg;
62
68
  }
63
69
  return anyErr.message;
64
70
  }
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.editDistance = editDistance;
4
+ exports.findBestMatch = findBestMatch;
5
+ exports.enhanceUnknownOptionSuggestions = enhanceUnknownOptionSuggestions;
6
+ // ---------------------------------------------------------------------------
7
+ // Hints for flags that aren't typos but semantic misunderstandings.
8
+ // Keyed by command name → flag → helpful message.
9
+ // ---------------------------------------------------------------------------
10
+ const COMMAND_HINTS = {
11
+ run: {
12
+ '--cloud': 'Cloud execution is the default. Use --local for local execution.',
13
+ },
14
+ };
15
+ // ---------------------------------------------------------------------------
16
+ // Damerau-Levenshtein distance (same algorithm commander uses internally)
17
+ // ---------------------------------------------------------------------------
18
+ function editDistance(a, b) {
19
+ const MAX = Math.max(a.length, b.length);
20
+ if (Math.abs(a.length - b.length) > MAX)
21
+ return MAX;
22
+ const d = [];
23
+ for (let i = 0; i <= a.length; i++)
24
+ d[i] = [i];
25
+ for (let j = 0; j <= b.length; j++)
26
+ d[0][j] = j;
27
+ for (let j = 1; j <= b.length; j++) {
28
+ for (let i = 1; i <= a.length; i++) {
29
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
30
+ d[i][j] = Math.min(d[i - 1][j] + 1, // deletion
31
+ d[i][j - 1] + 1, // insertion
32
+ d[i - 1][j - 1] + cost // substitution
33
+ );
34
+ // transposition
35
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
36
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
37
+ }
38
+ }
39
+ }
40
+ return d[a.length][b.length];
41
+ }
42
+ // ---------------------------------------------------------------------------
43
+ // Find the best matching option flag for an unknown flag.
44
+ //
45
+ // Enhancements over commander's built-in:
46
+ // - For --no-X options, also compares the unknown against the bare X name.
47
+ // This lets --strem match --no-stream (comparing "strem" vs "stream").
48
+ // ---------------------------------------------------------------------------
49
+ const MIN_SIMILARITY = 0.4;
50
+ const MAX_DISTANCE = 3;
51
+ function isSimilarEnough(dist, wordLen, candidateLen) {
52
+ if (dist > MAX_DISTANCE)
53
+ return false;
54
+ const len = Math.max(wordLen, candidateLen);
55
+ return (len - dist) / len > MIN_SIMILARITY;
56
+ }
57
+ function findBestMatch(unknownFlag, candidateFlags) {
58
+ if (!candidateFlags.length)
59
+ return null;
60
+ const unknown = unknownFlag.replace(/^--?/, '');
61
+ let bestFlag = null;
62
+ let bestDist = MAX_DISTANCE + 1;
63
+ for (const candidate of candidateFlags) {
64
+ const name = candidate.replace(/^--?/, '');
65
+ if (name.length <= 1)
66
+ continue;
67
+ // Standard comparison
68
+ const dist = editDistance(unknown, name);
69
+ if (dist < bestDist && isSimilarEnough(dist, unknown.length, name.length)) {
70
+ bestDist = dist;
71
+ bestFlag = candidate;
72
+ }
73
+ // Negation-aware: for --no-X, also compare unknown against X
74
+ if (name.startsWith('no-')) {
75
+ const baseName = name.slice(3);
76
+ if (baseName.length <= 1)
77
+ continue;
78
+ const baseDist = editDistance(unknown, baseName);
79
+ if (baseDist < bestDist && isSimilarEnough(baseDist, unknown.length, baseName.length)) {
80
+ bestDist = baseDist;
81
+ bestFlag = candidate;
82
+ }
83
+ }
84
+ }
85
+ return bestFlag;
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Override unknownOption on a command tree to provide enhanced suggestions.
89
+ // ---------------------------------------------------------------------------
90
+ function getHint(commandName, flag) {
91
+ return COMMAND_HINTS[commandName]?.[flag] ?? null;
92
+ }
93
+ function gatherCandidateFlags(cmd) {
94
+ const flags = [];
95
+ let current = cmd;
96
+ do {
97
+ const moreFlags = current
98
+ .createHelp()
99
+ .visibleOptions(current)
100
+ .filter((opt) => opt.long)
101
+ .map((opt) => opt.long);
102
+ flags.push(...moreFlags);
103
+ current = current.parent;
104
+ } while (current && !current._enablePositionalOptions);
105
+ return [...new Set(flags)];
106
+ }
107
+ function overrideUnknownOption(cmd) {
108
+ ;
109
+ cmd.unknownOption = function (flag) {
110
+ if (this._allowUnknownOption)
111
+ return;
112
+ // 1. Check for a context-aware hint
113
+ const hint = getHint(this.name(), flag);
114
+ if (hint) {
115
+ this.error(`error: unknown option '${flag}'\n${hint}`, {
116
+ code: 'commander.unknownOption',
117
+ });
118
+ return;
119
+ }
120
+ // 2. Gather candidate flags and find best match (enhanced)
121
+ let suggestion = '';
122
+ if (flag.startsWith('--')) {
123
+ const candidates = gatherCandidateFlags(this);
124
+ const match = findBestMatch(flag, candidates);
125
+ if (match) {
126
+ suggestion = `\n(Did you mean ${match}?)`;
127
+ }
128
+ }
129
+ this.error(`error: unknown option '${flag}'${suggestion}`, {
130
+ code: 'commander.unknownOption',
131
+ });
132
+ };
133
+ }
134
+ /**
135
+ * Walk the full command tree and override unknownOption on every command.
136
+ * Call this after all commands have been registered.
137
+ */
138
+ function enhanceUnknownOptionSuggestions(program) {
139
+ function walk(cmd) {
140
+ overrideUnknownOption(cmd);
141
+ for (const sub of cmd.commands) {
142
+ walk(sub);
143
+ }
144
+ }
145
+ walk(program);
146
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.62",
3
+ "version": "0.3.63",
4
4
  "description": "Command-line interface for orchagent — deploy and run AI agents for your team",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",