@openagents-org/agent-launcher 0.2.182 → 0.2.184
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/icons/codearts.svg +9 -0
- package/package.json +1 -1
- package/registry.json +68 -0
- package/src/adapters/codearts.js +316 -0
- package/src/adapters/index.js +4 -1
- package/src/adapters/kimi-stream.js +44 -2
- package/src/adapters/kimi.js +116 -16
- package/src/adapters/opencode.js +69 -22
- package/src/daemon.js +22 -1
- package/src/installer.js +48 -16
- package/src/paths.js +187 -92
- package/src/probe-mode.js +36 -0
- package/src/wsl.js +217 -45
package/src/adapters/opencode.js
CHANGED
|
@@ -114,12 +114,54 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
114
114
|
|
|
115
115
|
this._opencodeBinary = this._findOpencodeBinary();
|
|
116
116
|
if (this._opencodeBinary) {
|
|
117
|
-
this._log(`Using
|
|
117
|
+
this._log(`Using ${this._cliLabel()} subprocess mode: ${this._opencodeBinary}`);
|
|
118
118
|
} else {
|
|
119
|
-
this._log(
|
|
119
|
+
this._log(`${this._cliLabel()} binary not found. ${this._installHint()}`);
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// ------------------------------------------------------------------
|
|
124
|
+
// What differs between OpenCode and the CLIs built on it
|
|
125
|
+
// ------------------------------------------------------------------
|
|
126
|
+
//
|
|
127
|
+
// Vendors ship their own agents on top of OpenCode (Huawei's CodeArts Agent
|
|
128
|
+
// is one — see codearts.js). They keep its `run --format json` stream, its
|
|
129
|
+
// sessions and its failure shapes, and change how the CLI is found, launched
|
|
130
|
+
// and authenticated. These hooks are exactly that difference.
|
|
131
|
+
|
|
132
|
+
/** Product name used in logs and user-facing errors. */
|
|
133
|
+
_cliLabel() {
|
|
134
|
+
return 'OpenCode';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_installHint() {
|
|
138
|
+
return `Install with: npm install -g opencode-ai@${OPENCODE_PINNED_VERSION}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** User-facing message per failure category. */
|
|
142
|
+
_failureMessages() {
|
|
143
|
+
return FAILURE_MESSAGES;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Whether a run may not start without a model (OpenCode hangs without one). */
|
|
147
|
+
_modelRequired() {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Arguments after the binary for one `run`. */
|
|
152
|
+
_buildRunArgs({ runCwd, model, sessionId }) {
|
|
153
|
+
// Preflight guarantees a resolvable model; pin it explicitly — without it
|
|
154
|
+
// opencode hangs waiting for interactive provider/model selection.
|
|
155
|
+
const args = ['run', '--format', 'json', '--dir', runCwd, '--model', model];
|
|
156
|
+
if (sessionId) args.push('--session', sessionId);
|
|
157
|
+
return args;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Environment the CLI is spawned with. */
|
|
161
|
+
_spawnEnv() {
|
|
162
|
+
return { ...(this.agentEnv || process.env) };
|
|
163
|
+
}
|
|
164
|
+
|
|
123
165
|
/**
|
|
124
166
|
* Migrate sessions file from old location to agent home.
|
|
125
167
|
*/
|
|
@@ -793,30 +835,25 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
793
835
|
try { this._ensureCustomProviderConfig(runCwd); } catch (e) {
|
|
794
836
|
this._log(`Gateway provider config failed (continuing): ${e.message}`);
|
|
795
837
|
}
|
|
796
|
-
const cmd = [binary, 'run', '--format', 'json', '--dir', runCwd];
|
|
797
|
-
|
|
798
|
-
// Preflight guarantees a resolvable model; pin it explicitly — without it
|
|
799
|
-
// opencode hangs waiting for interactive provider/model selection.
|
|
800
838
|
const model = this._resolveModel();
|
|
801
|
-
cmd.push('--model', model);
|
|
802
|
-
|
|
803
839
|
const sessionId = this._channelSessions[msgChannel];
|
|
840
|
+
const args = this._buildRunArgs({ runCwd, model, sessionId });
|
|
841
|
+
|
|
804
842
|
let fullPrompt;
|
|
805
843
|
if (sessionId) {
|
|
806
844
|
fullPrompt = content;
|
|
807
|
-
cmd.push('--session', sessionId);
|
|
808
845
|
} else {
|
|
809
846
|
this._ensureWorkspaceSkill(msgChannel);
|
|
810
847
|
const context = this._buildSystemContext(msgChannel);
|
|
811
848
|
fullPrompt = `${context}\n\n---\n\n${content}`;
|
|
812
849
|
}
|
|
813
850
|
|
|
814
|
-
this._log(`CLI: ${binary}
|
|
851
|
+
this._log(`CLI: ${binary} ${args.join(' ')}`);
|
|
815
852
|
|
|
816
|
-
const spawnEnv =
|
|
853
|
+
const spawnEnv = this._spawnEnv();
|
|
817
854
|
|
|
818
|
-
let spawnBinary =
|
|
819
|
-
let spawnArgs =
|
|
855
|
+
let spawnBinary = binary;
|
|
856
|
+
let spawnArgs = args;
|
|
820
857
|
// Only the npm `.cmd` shim needs a cmd.exe host. The native opencode.exe
|
|
821
858
|
// (preferred by _findOpencodeBinary) is spawned directly — no console host.
|
|
822
859
|
if (IS_WINDOWS && spawnBinary.toLowerCase().endsWith('.cmd')) {
|
|
@@ -905,7 +942,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
905
942
|
|
|
906
943
|
if (signal) {
|
|
907
944
|
this._log(`opencode killed by signal ${signal} after ${TIMEOUT_MS / 1000}s (output: ${!!stdout})`);
|
|
908
|
-
const cls =
|
|
945
|
+
const cls = this.constructor._classifyFailure({ code, signal, stdout, stderr, stdoutErr });
|
|
909
946
|
// A signal almost always means our own timeout fired; only override to
|
|
910
947
|
// a more specific provider error if stdout/stderr clearly show one.
|
|
911
948
|
const category = (cls.category === 'unknown_error' || cls.category === 'process_crashed')
|
|
@@ -914,14 +951,14 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
914
951
|
}
|
|
915
952
|
|
|
916
953
|
if (code !== 0) {
|
|
917
|
-
const cls =
|
|
954
|
+
const cls = this.constructor._classifyFailure({ code, signal, stdout, stderr, stdoutErr });
|
|
918
955
|
this._log(`opencode exited code ${code} → [${cls.category}]: ${OpenCodeAdapter._redact(cls.diagnostic).slice(0, 300)}`);
|
|
919
956
|
return finish(reject, this._failure(cls.category, cls.diagnostic, cls.detail));
|
|
920
957
|
}
|
|
921
958
|
|
|
922
959
|
// Exit 0.
|
|
923
960
|
if (stdout) this._persistSessionId(msgChannel, stdout);
|
|
924
|
-
const outcome =
|
|
961
|
+
const outcome = this.constructor._outcomeForCleanExit({ stdout, stderr, stdoutErr, responseState });
|
|
925
962
|
if (outcome.text) return finish(resolve, outcome.text);
|
|
926
963
|
const f = outcome.failure;
|
|
927
964
|
this._log(`opencode exit 0 → [${f.category}]: ${OpenCodeAdapter._redact(f.diagnostic).slice(0, 300)}`);
|
|
@@ -951,7 +988,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
951
988
|
|
|
952
989
|
if (endedOnTool) {
|
|
953
990
|
if (stdoutErr) {
|
|
954
|
-
const cls =
|
|
991
|
+
const cls = this._classifyFailure({ code: 0, signal: null, stdout, stderr, stdoutErr });
|
|
955
992
|
const vague = ['unknown_error', 'process_crashed', 'empty_response'].includes(cls.category);
|
|
956
993
|
return {
|
|
957
994
|
failure: {
|
|
@@ -977,7 +1014,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
977
1014
|
// provider — it can be a structured error on stdout, an incomplete
|
|
978
1015
|
// stream, or a genuinely empty completion. Classify rather than guess.
|
|
979
1016
|
if (stdoutErr) {
|
|
980
|
-
const cls =
|
|
1017
|
+
const cls = this._classifyFailure({ code: 0, signal: null, stdout, stderr, stdoutErr });
|
|
981
1018
|
return { failure: { category: cls.category, diagnostic: `exit 0 with error event: ${cls.diagnostic || ''}`, detail: cls.detail } };
|
|
982
1019
|
}
|
|
983
1020
|
const emptyCat = OpenCodeAdapter._emptyExitCategory(stdout);
|
|
@@ -1010,7 +1047,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
1010
1047
|
if (!probe.executable) {
|
|
1011
1048
|
return { ok: false, category: 'cli_not_executable', diagnostic: 'opencode --version did not run' };
|
|
1012
1049
|
}
|
|
1013
|
-
const vclass =
|
|
1050
|
+
const vclass = this.constructor._classifyVersion(probe.version);
|
|
1014
1051
|
if (vclass === 'unsupported') {
|
|
1015
1052
|
return {
|
|
1016
1053
|
ok: false,
|
|
@@ -1022,7 +1059,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
1022
1059
|
// 'unknown' (unparseable) and 'degraded' (newer than tested) still run.
|
|
1023
1060
|
|
|
1024
1061
|
const model = this._resolveModel();
|
|
1025
|
-
if (!model) return { ok: false, category: 'model_missing' };
|
|
1062
|
+
if (!model && this._modelRequired()) return { ok: false, category: 'model_missing' };
|
|
1026
1063
|
|
|
1027
1064
|
const cred = this._credentialState();
|
|
1028
1065
|
if (cred === 'missing') return { ok: false, category: 'credential_missing' };
|
|
@@ -1105,6 +1142,14 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
1105
1142
|
];
|
|
1106
1143
|
if (stores.some(fileNonEmpty)) return 'present';
|
|
1107
1144
|
|
|
1145
|
+
// A model on a provider OpenCode sets up for itself is not ours to judge:
|
|
1146
|
+
// OpenCode Zen's `opencode/…` free models need no sign-in at all, and any
|
|
1147
|
+
// provider id other than openai/anthropic comes from OpenCode's own config.
|
|
1148
|
+
// Blocking those would refuse a run the CLI can make.
|
|
1149
|
+
const model = val('OPENCODE_MODEL') || val('LLM_MODEL');
|
|
1150
|
+
const provider = model.includes('/') ? model.split('/')[0].toLowerCase() : '';
|
|
1151
|
+
if (provider && provider !== 'openai' && provider !== 'anthropic') return 'unknown';
|
|
1152
|
+
|
|
1108
1153
|
if (val('OPENAI_BASE_URL') || val('LLM_BASE_URL')) return 'unknown';
|
|
1109
1154
|
if (Object.keys(env).some((k) => /(_API_KEY|_API_TOKEN|_TOKEN)$/.test(k) && val(k))) return 'unknown';
|
|
1110
1155
|
return 'missing';
|
|
@@ -1132,10 +1177,12 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
1132
1177
|
* normal reply. Carries `error_category` in metadata so the UI can route it.
|
|
1133
1178
|
*/
|
|
1134
1179
|
async _sendClassifiedError(channel, category, detail) {
|
|
1135
|
-
const
|
|
1180
|
+
const messages = this._failureMessages();
|
|
1181
|
+
const base = messages[category] || messages.unknown_error;
|
|
1136
1182
|
const safe = detail ? OpenCodeAdapter._redact(detail).trim() : '';
|
|
1137
1183
|
const body = safe ? `${base}\n\n> ${safe}` : base;
|
|
1138
|
-
const
|
|
1184
|
+
const label = this._cliLabel();
|
|
1185
|
+
const headline = category === 'incomplete_run' ? `${label} stopped mid-task` : `${label} couldn't run`;
|
|
1139
1186
|
const content = `⚠️ **${headline}** — ${body}`;
|
|
1140
1187
|
try {
|
|
1141
1188
|
await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
|
package/src/daemon.js
CHANGED
|
@@ -369,11 +369,32 @@ class Daemon {
|
|
|
369
369
|
*/
|
|
370
370
|
async _setModelEnv(type, model) {
|
|
371
371
|
const val = String(model || '');
|
|
372
|
-
const nativeVar = { claude: 'ANTHROPIC_MODEL', gemini: 'GEMINI_MODEL' }[type];
|
|
373
372
|
await this._runAgn(['env', type, '--set', `LLM_MODEL=${val}`]);
|
|
373
|
+
const nativeVar = { claude: 'ANTHROPIC_MODEL', gemini: 'GEMINI_MODEL' }[type] || this._nativeModelVar(type);
|
|
374
374
|
if (nativeVar) await this._runAgn(['env', type, '--set', `${nativeVar}=${val}`]);
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
/**
|
|
378
|
+
* The agent's own model setting, for a type whose registry declares one and
|
|
379
|
+
* does not map LLM_MODEL onto anything itself (CodeArts reads CODEARTS_MODEL
|
|
380
|
+
* and nothing else). A type with a resolve_env rule for LLM_MODEL already
|
|
381
|
+
* gets there through env resolution, so it is left alone.
|
|
382
|
+
*/
|
|
383
|
+
_nativeModelVar(type) {
|
|
384
|
+
try {
|
|
385
|
+
const entry = this.registry.getEntry(type);
|
|
386
|
+
if (!entry) return null;
|
|
387
|
+
const rules = (entry.resolve_env && entry.resolve_env.rules) || [];
|
|
388
|
+
if (rules.some((r) => r && r.from === 'LLM_MODEL')) return null;
|
|
389
|
+
const field = (entry.env_config || []).find(
|
|
390
|
+
(f) => f && /_MODEL$/.test(f.name || '') && f.name !== 'LLM_MODEL',
|
|
391
|
+
);
|
|
392
|
+
return field ? field.name : null;
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
377
398
|
async _refreshRuntimes() {
|
|
378
399
|
try {
|
|
379
400
|
const r = await this._runAgn(['runtimes', '--json']);
|
package/src/installer.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const { execSync, exec } = require('child_process');
|
|
7
7
|
const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, aiderBinDirs, resolveBinaryInKnownDirs } = require('./paths');
|
|
8
8
|
const { isWslBinary, bridgedCommandString, wslHomeUnc } = require('./wsl');
|
|
9
|
+
const { canBlock } = require('./probe-mode');
|
|
9
10
|
const { EnvManager } = require('./env');
|
|
10
11
|
const { nodeDistUrls, installRegistry } = require('./mirrors');
|
|
11
12
|
const { readinessReason, REASON } = require('./adapters/health-status');
|
|
@@ -39,6 +40,19 @@ const STALL_POLL_MS = Math.max(50, Math.min(15000, Math.floor(INSTALL_STALL_MS /
|
|
|
39
40
|
// results are cached briefly and invalidated on install/uninstall.
|
|
40
41
|
const VERSION_CACHE_TTL_MS = 60000;
|
|
41
42
|
const versionCache = new Map();
|
|
43
|
+
/** Version probes running in the background, by cache key. See probe-mode.js. */
|
|
44
|
+
const versionRefresh = new Set();
|
|
45
|
+
|
|
46
|
+
/** The version token in a `--version` output, or its first line. */
|
|
47
|
+
function parseVersionOutput(raw) {
|
|
48
|
+
// The optional `-prerelease` tail is NOT decoration. An agent pinned to a
|
|
49
|
+
// preview release (`@deepseek-ai/dsh@0.1.0-rc.6`) is gated on the exact
|
|
50
|
+
// version, and dropping the suffix reported a correctly installed rc.6 as
|
|
51
|
+
// plain `0.1.0` — which then failed its own gate. Agents without a
|
|
52
|
+
// prerelease segment match exactly as before.
|
|
53
|
+
const match = raw.match(/(\d+[\d.]+\d+(?:-[0-9A-Za-z.-]+)?)/);
|
|
54
|
+
return match ? match[1] : (raw.split('\n')[0] || null);
|
|
55
|
+
}
|
|
42
56
|
|
|
43
57
|
/**
|
|
44
58
|
* Compare two dotted version strings numerically, ignoring any pre-release /
|
|
@@ -616,15 +630,8 @@ class Installer {
|
|
|
616
630
|
]) {
|
|
617
631
|
if (fs.existsSync(candidate)) return candidate;
|
|
618
632
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
encoding: 'utf-8',
|
|
622
|
-
timeout: 5000,
|
|
623
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
624
|
-
env: getEnhancedEnv(),
|
|
625
|
-
}).split(/\r?\n/)[0].trim();
|
|
626
|
-
if (found) return found;
|
|
627
|
-
} catch {}
|
|
633
|
+
const found = whichBinary(nodeName, { allowWsl: false });
|
|
634
|
+
if (found) return found;
|
|
628
635
|
return 'node';
|
|
629
636
|
}
|
|
630
637
|
|
|
@@ -636,6 +643,14 @@ class Installer {
|
|
|
636
643
|
const cached = versionCache.get(key);
|
|
637
644
|
if (cached && (Date.now() - cached.ts) < VERSION_CACHE_TTL_MS) return cached.version;
|
|
638
645
|
|
|
646
|
+
if (!canBlock()) {
|
|
647
|
+
// A node CLI's `--version` takes 0.5s+ (seconds for one inside WSL).
|
|
648
|
+
// Report the last known version — or unknown, which healthCheck already
|
|
649
|
+
// handles — and let the next check pick up the answer.
|
|
650
|
+
this._refreshVersion(key, versionCmd);
|
|
651
|
+
return cached ? cached.version : null;
|
|
652
|
+
}
|
|
653
|
+
|
|
639
654
|
let version = null;
|
|
640
655
|
try {
|
|
641
656
|
const raw = execSync(versionCmd, {
|
|
@@ -644,19 +659,36 @@ class Installer {
|
|
|
644
659
|
env: getEnhancedEnv(),
|
|
645
660
|
timeout: 10000,
|
|
646
661
|
}).trim();
|
|
647
|
-
|
|
648
|
-
// preview release (`@deepseek-ai/dsh@0.1.0-rc.6`) is gated on the exact
|
|
649
|
-
// version, and dropping the suffix reported a correctly installed rc.6 as
|
|
650
|
-
// plain `0.1.0` — which then failed its own gate. Agents without a
|
|
651
|
-
// prerelease segment match exactly as before.
|
|
652
|
-
const match = raw.match(/(\d+[\d.]+\d+(?:-[0-9A-Za-z.-]+)?)/);
|
|
653
|
-
version = match ? match[1] : (raw.split('\n')[0] || null);
|
|
662
|
+
version = parseVersionOutput(raw);
|
|
654
663
|
} catch {}
|
|
655
664
|
|
|
656
665
|
versionCache.set(key, { version, ts: Date.now() });
|
|
657
666
|
return version;
|
|
658
667
|
}
|
|
659
668
|
|
|
669
|
+
/** _detectVersion's probe, off the calling thread. */
|
|
670
|
+
_refreshVersion(key, versionCmd) {
|
|
671
|
+
if (versionRefresh.has(key)) return;
|
|
672
|
+
versionRefresh.add(key);
|
|
673
|
+
try {
|
|
674
|
+
const child = exec(versionCmd, {
|
|
675
|
+
encoding: 'utf-8',
|
|
676
|
+
env: getEnhancedEnv(),
|
|
677
|
+
timeout: 10000,
|
|
678
|
+
windowsHide: true,
|
|
679
|
+
}, (err, stdout) => {
|
|
680
|
+
versionRefresh.delete(key);
|
|
681
|
+
versionCache.set(key, {
|
|
682
|
+
version: err ? null : parseVersionOutput(String(stdout).trim()),
|
|
683
|
+
ts: Date.now(),
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
if (child.stdin) child.stdin.end();
|
|
687
|
+
} catch {
|
|
688
|
+
versionRefresh.delete(key);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
660
692
|
/**
|
|
661
693
|
* Evaluate a registry version gate against a detected version. Generic for any
|
|
662
694
|
* agent. Returns { compatible: true|false|null, minVersion, message }.
|