@openagents-org/agent-launcher 0.2.173 → 0.2.175

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.
@@ -428,6 +428,62 @@ class HermesAdapter extends BaseAdapter {
428
428
  await this.sendError(msgChannel, `Error processing message: ${e.message}`);
429
429
  }
430
430
  }
431
+
432
+ // ------------------------------------------------------------------
433
+ // Static: configure Hermes's native model config from LLM env vars
434
+ // ------------------------------------------------------------------
435
+
436
+ /**
437
+ * Point Hermes at a custom OpenAI-compatible endpoint from user-provided
438
+ * LLM_API_KEY / LLM_BASE_URL / LLM_MODEL values. Called by saveAgentEnv when
439
+ * type === 'hermes', same pattern as OpenClaw's configureNativeAuth.
440
+ *
441
+ * Hermes has no env-var path for a custom endpoint (OPENAI_BASE_URL is
442
+ * honored only for its built-in openai provider) — the supported form is the
443
+ * `model:` section of its config.yaml:
444
+ * model: { default: <id>, provider: custom, base_url: <url>, api_key: <key> }
445
+ * Rather than hand-writing that YAML (and clobbering user config), drive
446
+ * Hermes's own `hermes config set` CLI: it owns the schema, merges into the
447
+ * existing file, and resolves its platform config path itself (~/.hermes on
448
+ * Unix, %LOCALAPPDATA%\hermes on native Windows, in-distro under WSL).
449
+ *
450
+ * Without this, a remotely-created hermes agent died on every message with
451
+ * "No inference provider configured" — the workspace's API-key field was a
452
+ * silent no-op for this type.
453
+ *
454
+ * Only acts when LLM_BASE_URL is set: a user signed in via `hermes setup`
455
+ * (the Nous login) must keep their own provider selection untouched.
456
+ */
457
+ static configureNativeAuth(env) {
458
+ const vals = env || {};
459
+ const baseUrl = String(vals.LLM_BASE_URL || '').trim().replace(/\/+$/, '');
460
+ if (!baseUrl) return { skipped: true, reason: 'no LLM_BASE_URL' };
461
+ const model = String(vals.LLM_MODEL || '').trim();
462
+ const apiKey = String(vals.LLM_API_KEY || '').trim();
463
+
464
+ // Borrow the adapter's binary resolution without running the constructor.
465
+ const probe = Object.create(HermesAdapter.prototype);
466
+ probe._log = () => {};
467
+ const bin = probe._findHermesBinary();
468
+ if (!bin) return { skipped: true, reason: 'hermes binary not found' };
469
+
470
+ const { execFileSync } = require('child_process');
471
+ const run = (args) => {
472
+ const spawnBin = probe._hermesViaWsl ? 'wsl.exe' : bin;
473
+ const spawnArgs = probe._hermesViaWsl ? ['-e', bin, ...args] : args;
474
+ execFileSync(spawnBin, spawnArgs, {
475
+ stdio: 'ignore',
476
+ timeout: 20000,
477
+ windowsHide: true,
478
+ });
479
+ };
480
+
481
+ run(['config', 'set', 'model.provider', 'custom']);
482
+ run(['config', 'set', 'model.base_url', baseUrl]);
483
+ if (model) run(['config', 'set', 'model.default', model]);
484
+ if (apiKey) run(['config', 'set', 'model.api_key', apiKey]);
485
+ return { configured: true };
486
+ }
431
487
  }
432
488
 
433
489
  module.exports = HermesAdapter;
@@ -23,6 +23,7 @@ const MiniSweAgentAdapter = require('./mini');
23
23
  const PiAdapter = require('./pi');
24
24
  const DeepSeekAdapter = require('./deepseek');
25
25
  const AntigravityAdapter = require('./antigravity');
26
+ const CommandCodeAdapter = require('./commandcode');
26
27
 
27
28
  const ADAPTER_MAP = {
28
29
  openclaw: OpenClawAdapter,
@@ -43,11 +44,12 @@ const ADAPTER_MAP = {
43
44
  pi: PiAdapter,
44
45
  deepseek: DeepSeekAdapter,
45
46
  antigravity: AntigravityAdapter,
47
+ commandcode: CommandCodeAdapter,
46
48
  };
47
49
 
48
50
  /**
49
51
  * Create an adapter instance for the given agent type.
50
- * @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent, pi, deepseek, antigravity)
52
+ * @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent, pi, deepseek, antigravity, commandcode)
51
53
  * @param {object} opts - Adapter constructor options
52
54
  * @returns {BaseAdapter}
53
55
  */
@@ -79,6 +81,7 @@ module.exports = {
79
81
  PiAdapter,
80
82
  DeepSeekAdapter,
81
83
  AntigravityAdapter,
84
+ CommandCodeAdapter,
82
85
  createAdapter,
83
86
  ADAPTER_MAP,
84
87
  };
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Pure, side-effect-free helpers for the Kimi Code CLI adapter.
3
+ *
4
+ * Everything here is I/O-free and deterministic so it can be unit-tested
5
+ * without spawning the `kimi` binary or touching the network: the JSONL
6
+ * stream parser, the message interpreter, secret redaction, error/version
7
+ * classification, the argument builder, and the env mapping that turns the
8
+ * launcher's KIMI_* fields into the CLI's KIMI_MODEL_* provider variables.
9
+ *
10
+ * Verified against Kimi Code CLI v0.39.1 (`kimi -p --output-format
11
+ * stream-json`). The stream is JSONL on stdout where each line is a message:
12
+ * {role:"assistant", content, tool_calls?:[{type:"function",id,function:{name,arguments}}]}
13
+ * {role:"tool", tool_call_id, content}
14
+ * {role:"meta", type:"system.version", version}
15
+ * {role:"meta", type:"session.resume_hint", session_id, command, content}
16
+ * {role:"meta", type:"turn.step.retrying", failed_attempt, next_attempt,
17
+ * max_attempts, delay_ms, error_name, error_message}
18
+ * Fatal errors arrive on stderr as `error: ...` lines. Exit codes: 0 success,
19
+ * 1 permanent failure (config/auth/quota), 75 transient (retryable).
20
+ *
21
+ * IMPORTANT product distinction: two Moonshot products install a `kimi`
22
+ * binary. Kimi Code CLI (npm `@moonshot-ai/kimi-code`) versions are 0.x;
23
+ * the legacy Python `kimi-cli` (PyPI) is 1.x and has a DIFFERENT headless
24
+ * interface. A 1.x `kimi --version` therefore means the WRONG product.
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Version / product classification
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /** Pull a dotted version (e.g. "0.39.1") out of `kimi --version` output. */
34
+ function parseKimiVersion(raw) {
35
+ if (!raw || typeof raw !== 'string') return null;
36
+ const m = raw.match(/(\d+)\.(\d+)\.(\d+)(?:[-.][0-9A-Za-z.]+)?/);
37
+ return m ? m[0] : null;
38
+ }
39
+
40
+ /**
41
+ * Classify `kimi --version` output into a product identity.
42
+ * @returns {{version: string|null, product: 'kimi-code'|'legacy'|null}}
43
+ * 'kimi-code' → the real Kimi Code CLI (0.x) this adapter drives
44
+ * 'legacy' → the wound-down Python kimi-cli (1.x) — incompatible
45
+ * null → undetermined (unparseable output); proceed leniently
46
+ */
47
+ function classifyKimiVersion(rawVersion) {
48
+ const version = parseKimiVersion(rawVersion);
49
+ if (!version) return { version: null, product: null };
50
+ const major = parseInt(version.split('.')[0], 10);
51
+ return { version, product: major === 0 ? 'kimi-code' : 'legacy' };
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Secret redaction (same patterns as the other CLI adapters)
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const SECRET_PATTERNS = [
59
+ /\bsk-[A-Za-z0-9._-]{8,}\b/g,
60
+ /\b(?:or|rk|gsk|ghp|gho|ghu|ghs|github_pat)[-_][A-Za-z0-9._-]{12,}\b/g,
61
+ /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,
62
+ /\bAKIA[0-9A-Z]{16}\b/g,
63
+ ];
64
+
65
+ /** Redact obvious secret material from a free-text string. */
66
+ function redactSecrets(text) {
67
+ if (text == null) return text;
68
+ let out = String(text);
69
+ for (const re of SECRET_PATTERNS) out = out.replace(re, '«redacted»');
70
+ return out;
71
+ }
72
+
73
+ /** Redact an argv array for logging. The prompt (arg after -p) is elided. */
74
+ function redactArgs(args) {
75
+ const out = [];
76
+ for (let i = 0; i < args.length; i++) {
77
+ const a = args[i];
78
+ if (a === '-p' || a === '--prompt') {
79
+ out.push(a, '«prompt»');
80
+ i++;
81
+ continue;
82
+ }
83
+ out.push(redactSecrets(a));
84
+ }
85
+ return out;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // JSONL stream parser
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /** Line-buffered JSONL parser. push() returns complete parsed messages. */
93
+ class KimiStreamParser {
94
+ constructor() {
95
+ this._buf = '';
96
+ }
97
+
98
+ /** @param {Buffer|string} chunk @returns {object[]} parsed messages */
99
+ push(chunk) {
100
+ this._buf += chunk.toString('utf-8');
101
+ const lines = this._buf.split('\n');
102
+ this._buf = lines.pop();
103
+ return parseLines(lines);
104
+ }
105
+
106
+ /** Flush any trailing partial line. */
107
+ flush() {
108
+ const rest = this._buf;
109
+ this._buf = '';
110
+ return parseLines([rest]);
111
+ }
112
+ }
113
+
114
+ function parseLines(lines) {
115
+ const out = [];
116
+ for (const line of lines) {
117
+ const t = line.trim();
118
+ if (!t || t[0] !== '{') continue;
119
+ try {
120
+ const obj = JSON.parse(t);
121
+ if (obj && typeof obj === 'object') out.push(obj);
122
+ } catch {
123
+ // Partial or non-JSON diagnostic line — skip.
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Message interpretation
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /** Flatten a message `content` (string or array of text blocks) to a string. */
134
+ function contentText(content) {
135
+ if (typeof content === 'string') return content;
136
+ if (Array.isArray(content)) {
137
+ return content
138
+ .map((b) => (b && typeof b === 'object' && typeof b.text === 'string' ? b.text : ''))
139
+ .join('');
140
+ }
141
+ return '';
142
+ }
143
+
144
+ // Argument keys worth surfacing in a tool-status preview, best first.
145
+ const PREVIEW_KEYS = ['command', 'path', 'file_path', 'pattern', 'url', 'query', 'prompt', 'description'];
146
+
147
+ /** A short, redacted human preview for a tool call's JSON arguments. */
148
+ function toolPreview(argsJson) {
149
+ let args;
150
+ try {
151
+ args = JSON.parse(argsJson);
152
+ } catch {
153
+ return '';
154
+ }
155
+ if (!args || typeof args !== 'object') return '';
156
+ for (const key of PREVIEW_KEYS) {
157
+ const v = args[key];
158
+ if (typeof v === 'string' && v.trim()) {
159
+ const one = redactSecrets(v.replace(/\s+/g, ' ').trim());
160
+ return one.length > 80 ? one.slice(0, 80) + '…' : one;
161
+ }
162
+ }
163
+ return '';
164
+ }
165
+
166
+ /**
167
+ * Map one parsed stream message to zero or more adapter events:
168
+ * {kind:'text', text} assistant prose
169
+ * {kind:'tool_start', name, preview} a tool call was issued
170
+ * {kind:'tool_result', id, text} a tool finished
171
+ * {kind:'session', sessionId} resume hint (inline session id)
172
+ * {kind:'retrying', attempt, maxAttempts, message} provider retry
173
+ * {kind:'version', version} stream preamble
174
+ */
175
+ function interpretKimiMessage(msg) {
176
+ if (!msg || typeof msg !== 'object') return [];
177
+ const events = [];
178
+
179
+ if (msg.role === 'assistant') {
180
+ const text = contentText(msg.content).trim();
181
+ if (text) events.push({ kind: 'text', text });
182
+ if (Array.isArray(msg.tool_calls)) {
183
+ for (const tc of msg.tool_calls) {
184
+ const fn = tc && typeof tc === 'object' ? tc.function : null;
185
+ const name = fn && typeof fn.name === 'string' ? fn.name : 'tool';
186
+ events.push({ kind: 'tool_start', name, preview: toolPreview(fn && fn.arguments) });
187
+ }
188
+ }
189
+ return events;
190
+ }
191
+
192
+ if (msg.role === 'tool') {
193
+ events.push({
194
+ kind: 'tool_result',
195
+ id: typeof msg.tool_call_id === 'string' ? msg.tool_call_id : '',
196
+ text: contentText(msg.content),
197
+ });
198
+ return events;
199
+ }
200
+
201
+ if (msg.role === 'meta') {
202
+ if (msg.type === 'session.resume_hint' && typeof msg.session_id === 'string' && msg.session_id) {
203
+ events.push({ kind: 'session', sessionId: msg.session_id });
204
+ } else if (msg.type === 'turn.step.retrying') {
205
+ events.push({
206
+ kind: 'retrying',
207
+ attempt: typeof msg.failed_attempt === 'number' ? msg.failed_attempt : 0,
208
+ maxAttempts: typeof msg.max_attempts === 'number' ? msg.max_attempts : 0,
209
+ message: typeof msg.error_message === 'string' ? redactSecrets(msg.error_message) : '',
210
+ });
211
+ } else if (msg.type === 'system.version') {
212
+ events.push({ kind: 'version', version: typeof msg.version === 'string' ? msg.version : '' });
213
+ }
214
+ return events;
215
+ }
216
+
217
+ return events;
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Argument + environment builders
222
+ // ---------------------------------------------------------------------------
223
+
224
+ /**
225
+ * Build the argv for one non-interactive run. Print mode (`-p`) already
226
+ * auto-approves tools — the CLI REJECTS combining it with --yolo/--auto/--plan
227
+ * (verified v0.39.1), so permission/plan flags must never be added here.
228
+ */
229
+ function buildKimiArgs({ prompt, sessionId }) {
230
+ const args = [];
231
+ if (sessionId) args.push('-S', sessionId);
232
+ args.push('-p', prompt, '--output-format', 'stream-json');
233
+ return args;
234
+ }
235
+
236
+ const DEFAULT_KIMI_MODEL = 'kimi-k2.6';
237
+ // The CLI defaults max_completion_tokens to the model's full context size
238
+ // (262144), which OpenAI-compatible gateways reject (input+output > context).
239
+ // A fixed, generous output budget works against both Moonshot and gateways.
240
+ const DEFAULT_MAX_COMPLETION_TOKENS = '32768';
241
+
242
+ /**
243
+ * Map the launcher's saved KIMI_* fields onto the env-var provider contract of
244
+ * Kimi Code CLI: setting KIMI_MODEL_NAME synthesizes an in-memory provider
245
+ * from KIMI_MODEL_API_KEY (required) + KIMI_MODEL_BASE_URL +
246
+ * KIMI_MODEL_PROVIDER_TYPE, bypassing `kimi login`.
247
+ *
248
+ * With no API key configured, the env is passed through untouched so the
249
+ * CLI's own `kimi login` credentials / config.toml apply.
250
+ *
251
+ * @returns {{env: object, viaEnvProvider: boolean}}
252
+ */
253
+ function buildKimiEnv(agentEnv) {
254
+ const env = { ...(agentEnv || {}) };
255
+ const apiKey =
256
+ env.KIMI_MODEL_API_KEY ||
257
+ env.KIMI_API_KEY ||
258
+ env.MOONSHOT_API_KEY ||
259
+ '';
260
+ if (!apiKey) return { env, viaEnvProvider: false };
261
+
262
+ env.KIMI_MODEL_API_KEY = apiKey;
263
+ if (!env.KIMI_MODEL_NAME) env.KIMI_MODEL_NAME = env.KIMI_MODEL || DEFAULT_KIMI_MODEL;
264
+ const baseUrl = env.KIMI_MODEL_BASE_URL || env.KIMI_BASE_URL;
265
+ if (baseUrl) env.KIMI_MODEL_BASE_URL = baseUrl.replace(/\/$/, '');
266
+ if (!env.KIMI_MODEL_PROVIDER_TYPE) env.KIMI_MODEL_PROVIDER_TYPE = 'kimi';
267
+ if (!env.KIMI_MODEL_MAX_COMPLETION_TOKENS) {
268
+ env.KIMI_MODEL_MAX_COMPLETION_TOKENS = DEFAULT_MAX_COMPLETION_TOKENS;
269
+ }
270
+ return { env, viaEnvProvider: true };
271
+ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Error classification
275
+ // ---------------------------------------------------------------------------
276
+
277
+ // Kimi Code CLI's documented exit codes.
278
+ const EXIT_PERMANENT = 1;
279
+ const EXIT_TRANSIENT = 75;
280
+
281
+ /** Extract the message from `error: ...` lines on stderr (last one wins). */
282
+ function extractStderrError(stderrText) {
283
+ if (!stderrText) return '';
284
+ let last = '';
285
+ for (const line of String(stderrText).split('\n')) {
286
+ const m = line.match(/^\s*error:\s*(.+)$/i);
287
+ if (m) last = m[1].trim();
288
+ }
289
+ // Strip the CLI's wrapper prefix so the user sees the actual cause.
290
+ return redactSecrets(last.replace(/^failed to run prompt:\s*/i, ''));
291
+ }
292
+
293
+ /**
294
+ * Classify a failed run into a user-facing message.
295
+ * @param {{code: number|null, signal: string|null, stderrText: string, retryMessage: string}} info
296
+ * @returns {{kind: string, userMessage: string}}
297
+ */
298
+ function classifyKimiError(info) {
299
+ const { code, signal, stderrText, retryMessage } = info || {};
300
+ const detail = extractStderrError(stderrText) || redactSecrets(retryMessage || '');
301
+
302
+ if (/auth_error|401|unauthorized|invalid.{0,20}(api.?key|token)|credential/i.test(detail)) {
303
+ return {
304
+ kind: 'auth',
305
+ userMessage:
306
+ 'Kimi authentication failed. Check KIMI_API_KEY in the launcher, or run `kimi login` in a terminal.' +
307
+ (detail ? `\n\nDetails: ${detail}` : ''),
308
+ };
309
+ }
310
+ if (/no model configured|llm not set|kimi_model_api_key is missing/i.test(detail)) {
311
+ return {
312
+ kind: 'config',
313
+ userMessage:
314
+ 'Kimi Code CLI is not configured. Set KIMI_API_KEY (plus optional KIMI_BASE_URL / KIMI_MODEL) ' +
315
+ 'in the launcher, or run `kimi login` in a terminal.',
316
+ };
317
+ }
318
+ if (/rate.?limit|429|quota|insufficient.{0,20}balance/i.test(detail)) {
319
+ return {
320
+ kind: 'rate_limit',
321
+ userMessage: `Kimi hit a rate/quota limit. Please retry shortly.${detail ? `\n\nDetails: ${detail}` : ''}`,
322
+ };
323
+ }
324
+ if (/context length|maximum context|too many tokens/i.test(detail)) {
325
+ return {
326
+ kind: 'context',
327
+ userMessage: `The request exceeded the model's context limits.${detail ? `\n\nDetails: ${detail}` : ''}`,
328
+ };
329
+ }
330
+ if (code === EXIT_TRANSIENT) {
331
+ return {
332
+ kind: 'transient',
333
+ userMessage: `Kimi hit a temporary provider error — please try again.${detail ? `\n\nDetails: ${detail}` : ''}`,
334
+ };
335
+ }
336
+ if (detail) {
337
+ return { kind: 'error', userMessage: `Kimi failed: ${detail}` };
338
+ }
339
+ const why = signal ? `terminated by signal ${signal}` : `exited with code ${code}`;
340
+ return { kind: 'error', userMessage: `Kimi Code CLI ${why}.` };
341
+ }
342
+
343
+ module.exports = {
344
+ parseKimiVersion,
345
+ classifyKimiVersion,
346
+ redactSecrets,
347
+ redactArgs,
348
+ KimiStreamParser,
349
+ interpretKimiMessage,
350
+ contentText,
351
+ toolPreview,
352
+ buildKimiArgs,
353
+ buildKimiEnv,
354
+ extractStderrError,
355
+ classifyKimiError,
356
+ DEFAULT_KIMI_MODEL,
357
+ DEFAULT_MAX_COMPLETION_TOKENS,
358
+ EXIT_PERMANENT,
359
+ EXIT_TRANSIENT,
360
+ };