@wenrwa/marketplace-cli 0.8.1 → 0.8.3
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/__tests__/bridge-opencode-binary.test.d.ts +1 -0
- package/dist/__tests__/bridge-opencode-binary.test.js +128 -0
- package/dist/__tests__/bridge-opencode-launch.test.js +37 -0
- package/dist/__tests__/opencode-local-launch-parity.test.d.ts +1 -0
- package/dist/__tests__/opencode-local-launch-parity.test.js +129 -0
- package/dist/bridge/agentLaunch.js +17 -10
- package/dist/bridge/opencodeBinary.d.ts +47 -0
- package/dist/bridge/opencodeBinary.js +161 -0
- package/dist/bridge/opencodeConfig.d.ts +3 -0
- package/dist/bridge/opencodeConfig.js +8 -2
- package/dist/bridge/relayClient.d.ts +7 -0
- package/dist/bridge/relayClient.js +11 -3
- package/dist/bridge/server.d.ts +12 -0
- package/dist/bridge/server.js +16 -6
- package/dist/commands/bridge.js +43 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/** oc-25 — the bridge provides OpenCode (resolution order, shared install, failure text). */
|
|
4
|
+
const opencodeBinary_1 = require("../bridge/opencodeBinary");
|
|
5
|
+
const opencodeConfig_1 = require("../bridge/opencodeConfig");
|
|
6
|
+
const BASE = '/h/.wen-market/opencode';
|
|
7
|
+
const PREFIX = (0, opencodeBinary_1.managedPrefix)(BASE);
|
|
8
|
+
const BIN = (0, opencodeBinary_1.managedBin)(PREFIX);
|
|
9
|
+
const launch = { command: 'opencode', args: ['-s', 'ses_a'], env: { ...opencodeConfig_1.OPENCODE_QUIET_ENV, X: '1' } };
|
|
10
|
+
function harness(over = {}) {
|
|
11
|
+
const files = new Map();
|
|
12
|
+
const npmCalls = [];
|
|
13
|
+
let release;
|
|
14
|
+
const gate = new Promise((r) => { release = r; });
|
|
15
|
+
const deps = {
|
|
16
|
+
exists: (p) => files.has(p),
|
|
17
|
+
writeFile: (p, c) => { files.set(p, c); },
|
|
18
|
+
mkdirp: () => { },
|
|
19
|
+
rm: (p) => { files.delete(p); },
|
|
20
|
+
runNpm: async (args, env) => {
|
|
21
|
+
npmCalls.push({ args, env });
|
|
22
|
+
await gate;
|
|
23
|
+
if ((over.npmCode ?? 0) === 0)
|
|
24
|
+
files.set(BIN, 'bin');
|
|
25
|
+
return { code: over.npmCode ?? 0, stderr: '' };
|
|
26
|
+
},
|
|
27
|
+
pathOpencode: () => null,
|
|
28
|
+
versionOf: () => opencodeConfig_1.OPENCODE_PINNED_VERSION,
|
|
29
|
+
env: { PATH: '/usr/bin', npm_config_offline: 'true' },
|
|
30
|
+
platform: 'darwin',
|
|
31
|
+
...over,
|
|
32
|
+
};
|
|
33
|
+
return { files, npmCalls, release, deps };
|
|
34
|
+
}
|
|
35
|
+
describe('oc-25 — pinned version lives in one place', () => {
|
|
36
|
+
it('the minimum IS the pinned version', () => {
|
|
37
|
+
expect(opencodeConfig_1.OPENCODE_MIN_VERSION).toBe(opencodeConfig_1.OPENCODE_PINNED_VERSION);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('oc-25 — resolution order', () => {
|
|
41
|
+
it('1. managed pinned binary wins over a newer PATH one; absolute path, no install', () => {
|
|
42
|
+
const h = harness({ pathOpencode: () => ({ path: '/usr/local/bin/opencode', version: '9.9.9' }) });
|
|
43
|
+
h.files.set(BIN, 'x');
|
|
44
|
+
h.files.set(`${PREFIX}/.ready`, '1');
|
|
45
|
+
const r = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps).prepare(launch);
|
|
46
|
+
expect(r).toEqual({ ok: true, launch: { ...launch, command: BIN } });
|
|
47
|
+
expect(h.npmCalls).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
it('2. PATH opencode at/above the minimum is used by absolute path', () => {
|
|
50
|
+
const h = harness({ pathOpencode: () => ({ path: '/opt/bin/opencode', version: '1.20.0' }) });
|
|
51
|
+
const r = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps).prepare(launch);
|
|
52
|
+
expect(r).toEqual({ ok: true, launch: { ...launch, command: '/opt/bin/opencode' } });
|
|
53
|
+
expect(h.npmCalls).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
it('3. a PATH opencode below the minimum is ignored and the pinned one is installed', async () => {
|
|
56
|
+
const h = harness({ pathOpencode: () => ({ path: '/opt/bin/opencode', version: '1.2.0' }) });
|
|
57
|
+
const p = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps);
|
|
58
|
+
const r = p.prepare(launch);
|
|
59
|
+
expect(h.npmCalls).toHaveLength(1);
|
|
60
|
+
expect(r.ok && r.launch.command).toBe('/bin/sh');
|
|
61
|
+
h.release();
|
|
62
|
+
await p.settled();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('oc-25 — install', () => {
|
|
66
|
+
it('installs the pinned version into the bridge prefix with npm_config_offline scrubbed', async () => {
|
|
67
|
+
const h = harness();
|
|
68
|
+
const p = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps);
|
|
69
|
+
const r = p.prepare(launch);
|
|
70
|
+
expect(h.npmCalls[0].args).toEqual(['install', '--prefix', PREFIX, `opencode-ai@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`, '--no-audit', '--no-fund']);
|
|
71
|
+
expect(h.npmCalls[0].env.npm_config_offline).toBeUndefined();
|
|
72
|
+
expect(h.npmCalls[0].env.PATH).toBe('/usr/bin');
|
|
73
|
+
// the pane runs a waiter that execs the ABSOLUTE managed binary with the original args
|
|
74
|
+
expect(r.ok && r.launch.args).toEqual(['-c', opencodeBinary_1.OPENCODE_WAIT_SCRIPT, 'opencode-wait', `${PREFIX}/.ready`, `${PREFIX}/.install-failed`, BIN, '-s', 'ses_a']);
|
|
75
|
+
expect(r.ok && r.launch.env).toEqual(launch.env);
|
|
76
|
+
expect(h.files.has(`${PREFIX}/.ready`)).toBe(false);
|
|
77
|
+
h.release();
|
|
78
|
+
await p.settled();
|
|
79
|
+
expect(h.files.has(`${PREFIX}/.ready`)).toBe(true);
|
|
80
|
+
// and the next open uses it directly
|
|
81
|
+
expect(p.prepare(launch)).toEqual({ ok: true, launch: { ...launch, command: BIN } });
|
|
82
|
+
expect(h.npmCalls).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
it('two panes opening at once share ONE install', async () => {
|
|
85
|
+
const h = harness();
|
|
86
|
+
const p = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps);
|
|
87
|
+
p.prepare(launch);
|
|
88
|
+
p.prepare(launch);
|
|
89
|
+
expect(h.npmCalls).toHaveLength(1);
|
|
90
|
+
h.release();
|
|
91
|
+
await p.settled();
|
|
92
|
+
});
|
|
93
|
+
it('failure writes a short wrapped message with the manual command, no ready marker; a later open retries', async () => {
|
|
94
|
+
const h = harness({ npmCode: 1 });
|
|
95
|
+
const p = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps);
|
|
96
|
+
p.prepare(launch);
|
|
97
|
+
h.release();
|
|
98
|
+
await p.settled();
|
|
99
|
+
const msg = h.files.get(`${PREFIX}/.install-failed`);
|
|
100
|
+
expect(msg).toContain(`npm i -g opencode-ai@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`);
|
|
101
|
+
for (const line of msg.split('\n'))
|
|
102
|
+
expect(line.length).toBeLessThanOrEqual(40);
|
|
103
|
+
expect(h.files.has(`${PREFIX}/.ready`)).toBe(false);
|
|
104
|
+
p.prepare(launch);
|
|
105
|
+
expect(h.npmCalls).toHaveLength(2);
|
|
106
|
+
expect(h.files.has(`${PREFIX}/.install-failed`)).toBe(false); // stale verdict cleared
|
|
107
|
+
h.release();
|
|
108
|
+
await p.settled();
|
|
109
|
+
});
|
|
110
|
+
it('a thrown npm spawn error is a failure, not a crash', async () => {
|
|
111
|
+
const h = harness({ runNpm: async () => { throw new Error('spawn npm ENOENT'); } });
|
|
112
|
+
const p = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps);
|
|
113
|
+
p.prepare(launch);
|
|
114
|
+
await p.settled();
|
|
115
|
+
expect(h.files.get(`${PREFIX}/.install-failed`)).toContain('spawn npm ENOENT');
|
|
116
|
+
});
|
|
117
|
+
it('refuses with the manual command on win32 (no sh waiter)', () => {
|
|
118
|
+
const h = harness({ platform: 'win32' });
|
|
119
|
+
const r = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps).prepare(launch);
|
|
120
|
+
expect(r.ok).toBe(false);
|
|
121
|
+
expect(!r.ok && r.message).toContain(`npm i -g opencode-ai@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`);
|
|
122
|
+
expect(h.npmCalls).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
it('failure lines are short', () => {
|
|
125
|
+
for (const l of (0, opencodeBinary_1.opencodeInstallFailureLines)('x'.repeat(200)))
|
|
126
|
+
expect(l.length).toBeLessThanOrEqual(65);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -177,3 +177,40 @@ describe('relay lane carries the server-delivered opencode block', () => {
|
|
|
177
177
|
await new Promise((r) => wss.close(r));
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
|
+
describe('oc-25 — relay lane spawns what the provisioner prepared', () => {
|
|
181
|
+
it('swaps in the absolute binary; a refusal becomes the typed error with its own message', async () => {
|
|
182
|
+
const { wss, port, ptys, factory } = await setup();
|
|
183
|
+
const frames = [];
|
|
184
|
+
let conn;
|
|
185
|
+
const got = new Promise((r) => wss.on('connection', (ws) => { conn = ws; ws.on('message', (m) => frames.push(JSON.parse(String(m)))); r(); }));
|
|
186
|
+
let refuse = false;
|
|
187
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
188
|
+
relayUrl: `ws://127.0.0.1:${port}`, relayToken: 'tok',
|
|
189
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
190
|
+
resolveLaunch: (o) => (0, agentLaunch_1.resolveAgentLaunch)({ autonomous: true, opencode: o.opencode, connectToken: o.connectToken }),
|
|
191
|
+
commandExists: () => true,
|
|
192
|
+
prepareOpencodeLaunch: (l) => refuse
|
|
193
|
+
? { ok: false, message: 'short refusal' }
|
|
194
|
+
: { ok: true, launch: { ...l, command: '/h/.wen-market/opencode/1.18.35/node_modules/.bin/opencode' } },
|
|
195
|
+
});
|
|
196
|
+
await got;
|
|
197
|
+
const wait = async (pred) => { for (let i = 0; i < 100; i++) {
|
|
198
|
+
const f = frames.find(pred);
|
|
199
|
+
if (f)
|
|
200
|
+
return f;
|
|
201
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
202
|
+
} throw new Error('timeout'); };
|
|
203
|
+
conn.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, sessionId: 'b-1', workspaceId: 'w', opencode: oc() }));
|
|
204
|
+
await wait((f) => f.t === 'opened');
|
|
205
|
+
expect(ptys[0].opts.command).toBe('/h/.wen-market/opencode/1.18.35/node_modules/.bin/opencode');
|
|
206
|
+
refuse = true;
|
|
207
|
+
conn.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, sessionId: 'b-2', workspaceId: 'w', opencode: oc() }));
|
|
208
|
+
const err = await wait((f) => f.t === 'error' && f.sessionId === 'b-2');
|
|
209
|
+
expect(err.message).toBe('short refusal');
|
|
210
|
+
expect(ptys).toHaveLength(1);
|
|
211
|
+
handle.close();
|
|
212
|
+
for (const c of wss.clients)
|
|
213
|
+
c.terminate();
|
|
214
|
+
await new Promise((r) => wss.close(r));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
/**
|
|
40
|
+
* oc-27 — the ACTUAL local launch must carry the same keys the hosted config does:
|
|
41
|
+
* `plugin` (activity, titles, Needs-you, session-id capture), `mcp.wenrwa-marketplace`
|
|
42
|
+
* (when a connect token exists) and `theme`. A builder-only test missed this: the
|
|
43
|
+
* builder was fine, the local call sites never fed it a plugin or a token.
|
|
44
|
+
*/
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const url_1 = require("url");
|
|
48
|
+
const ws_1 = __importDefault(require("ws"));
|
|
49
|
+
const agentLaunch_1 = require("../bridge/agentLaunch");
|
|
50
|
+
const server_1 = require("../bridge/server");
|
|
51
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
52
|
+
const hosted = require(path.join(__dirname, '..', '..', '..', 'infra', 'terminal-image', 'opencode', 'build-config.js'));
|
|
53
|
+
const GW = 'https://api.example.test/api/v1/opencode/v1';
|
|
54
|
+
const oc = { engine: 'opencode', model: 'moonshotai/kimi-k2', gatewayUrl: GW, gatewayToken: 'hgw_abc' };
|
|
55
|
+
const cfgOf = (l) => JSON.parse(l.env.OPENCODE_CONFIG_CONTENT);
|
|
56
|
+
// The shape cli/src/commands/bridge.ts hands resolveAgentLaunch (mcpInstall + bridgeApiUrl).
|
|
57
|
+
const realDeps = {
|
|
58
|
+
mcpServer: { command: '/usr/bin/node', args: ['/h/.wen-market/mcp/dist/index.js'] },
|
|
59
|
+
apiUrl: 'https://api.example.test/api/v1',
|
|
60
|
+
activity: { settingsPath: '/h/.wen-market/hiveop-settings.json', sessionId: 'a1', activityFile: '/h/.wen-market/activity.jsonl' },
|
|
61
|
+
sessionPin: { agentId: 'a1', exists: false },
|
|
62
|
+
};
|
|
63
|
+
describe('local OpenCode launch carries the hosted keys', () => {
|
|
64
|
+
const hostedCfg = hosted.buildConfig({
|
|
65
|
+
WENRWA_AGENT_MODEL: oc.model, HIVEOP_GATEWAY_URL: GW, HIVEOP_GATEWAY_TOKEN: 't', WENRWA_CONNECT_TOKEN: 'c',
|
|
66
|
+
});
|
|
67
|
+
it('plugin is a file:// URL to a file that exists in the installed package', () => {
|
|
68
|
+
const cfg = cfgOf((0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc, connectToken: 'c', ...realDeps }));
|
|
69
|
+
expect(cfg.plugin).toHaveLength(1);
|
|
70
|
+
expect(cfg.plugin[0]).toMatch(/^file:\/\//);
|
|
71
|
+
expect(fs.existsSync((0, url_1.fileURLToPath)(cfg.plugin[0]))).toBe(true);
|
|
72
|
+
expect(path.basename((0, url_1.fileURLToPath)(cfg.plugin[0]))).toBe('opencode-plugin.mjs');
|
|
73
|
+
});
|
|
74
|
+
it('mcp.wenrwa-marketplace is present with the token by reference only', () => {
|
|
75
|
+
const l = (0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc, connectToken: 'secret-conn-token', ...realDeps });
|
|
76
|
+
expect(cfgOf(l).mcp['wenrwa-marketplace'].environment.WENRWA_CONNECT_TOKEN).toBe('{env:WENRWA_CONNECT_TOKEN}');
|
|
77
|
+
expect(l.env.OPENCODE_CONFIG_CONTENT).not.toContain('secret-conn-token');
|
|
78
|
+
expect(l.env.WENRWA_CONNECT_TOKEN).toBe('secret-conn-token');
|
|
79
|
+
});
|
|
80
|
+
it('env carries HIVEOP_GATEWAY_URL so the plugin can report the session id (resume)', () => {
|
|
81
|
+
const l = (0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc, connectToken: 'c', ...realDeps });
|
|
82
|
+
expect(l.env.HIVEOP_GATEWAY_URL).toBe(GW);
|
|
83
|
+
expect(l.env.HIVEOP_GATEWAY_TOKEN).toBe('hgw_abc');
|
|
84
|
+
});
|
|
85
|
+
it('theme matches the hosted config (terminal palette, no own background)', () => {
|
|
86
|
+
const cfg = cfgOf((0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc, ...realDeps }));
|
|
87
|
+
expect(cfg.theme).toBe('system');
|
|
88
|
+
expect(cfg.theme).toBe(hostedCfg.theme);
|
|
89
|
+
});
|
|
90
|
+
it('the same top-level keys as hosted (modulo the hosted-only plugin path)', () => {
|
|
91
|
+
const local = cfgOf((0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc, connectToken: 'c', ...realDeps }));
|
|
92
|
+
const keys = (o) => Object.keys(o).filter((k) => k !== 'instructions').sort();
|
|
93
|
+
expect(keys(local)).toEqual(keys(hostedCfg));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('loopback open (real bridge wiring) falls back to the bridge-config connect token', () => {
|
|
97
|
+
let handle;
|
|
98
|
+
let spawns;
|
|
99
|
+
beforeEach(async () => {
|
|
100
|
+
spawns = [];
|
|
101
|
+
const pty = { write() { }, resize() { }, kill() { }, onData() { }, onExit() { } };
|
|
102
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
103
|
+
sessionToken: 'tok', allowedOrigin: 'https://app.hiveop.io',
|
|
104
|
+
sessions: new sessionManager_1.SessionManager({ spawn: (o) => { spawns.push(o); return pty; } }),
|
|
105
|
+
resolveLocalAgentCwd: () => '/tmp/proj',
|
|
106
|
+
commandExists: () => true,
|
|
107
|
+
activityFile: '/h/activity.jsonl', hiveOpSettingsPath: '/h/settings.json',
|
|
108
|
+
mcpServer: realDeps.mcpServer, mcpApiUrl: realDeps.apiUrl,
|
|
109
|
+
connectToken: 'bridge-cfg-token',
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
afterEach(async () => { await handle.close(); });
|
|
113
|
+
it('open frame without connectToken still yields plugin + mcp + theme', async () => {
|
|
114
|
+
const ws = new ws_1.default(`ws://127.0.0.1:${handle.port}`, { headers: { origin: 'https://app.hiveop.io' } });
|
|
115
|
+
await new Promise((r) => ws.on('open', r));
|
|
116
|
+
ws.send(JSON.stringify({ v: 1, token: 'tok' }));
|
|
117
|
+
const opened = new Promise((r) => ws.on('message', (m) => { if (JSON.parse(String(m)).t === 'opened')
|
|
118
|
+
r(null); }));
|
|
119
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, agentId: 'a1', workspaceId: 'w1', opencode: oc }));
|
|
120
|
+
await opened;
|
|
121
|
+
ws.close();
|
|
122
|
+
const cfg = cfgOf({ env: spawns[0].env });
|
|
123
|
+
expect(cfg.plugin[0]).toMatch(/^file:\/\//);
|
|
124
|
+
expect(cfg.mcp['wenrwa-marketplace']).toBeDefined();
|
|
125
|
+
expect(cfg.theme).toBe('system');
|
|
126
|
+
expect(spawns[0].env.WENRWA_CONNECT_TOKEN).toBe('bridge-cfg-token');
|
|
127
|
+
expect(spawns[0].env.OPENCODE_CONFIG_CONTENT).not.toContain('bridge-cfg-token');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -4,6 +4,7 @@ exports.HIVEOP_CONTEXT_ENV_VALUE = exports.HIVEOP_CONTEXT_ENV_VAR = void 0;
|
|
|
4
4
|
exports.missingCommandMessage = missingCommandMessage;
|
|
5
5
|
exports.resolveAgentLaunch = resolveAgentLaunch;
|
|
6
6
|
const localContextWriter_1 = require("./localContextWriter");
|
|
7
|
+
const opencodePlugin_1 = require("./opencodePlugin");
|
|
7
8
|
const opencodeConfig_1 = require("./opencodeConfig");
|
|
8
9
|
/** lws-08 — env var name a spawned `claude` (or any tooling inspecting its
|
|
9
10
|
* own environment) can read to find the pushed context manifest. */
|
|
@@ -46,21 +47,27 @@ function resolveOpencodeLaunch(opts, oc) {
|
|
|
46
47
|
const args = [];
|
|
47
48
|
if ((0, opencodeConfig_1.isCapturedOpencodeSessionId)(oc.sessionId))
|
|
48
49
|
args.push('-s', oc.sessionId);
|
|
49
|
-
const config =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
const config = {
|
|
51
|
+
...(0, opencodeConfig_1.buildOpencodeConfig)({
|
|
52
|
+
model: oc.model,
|
|
53
|
+
gatewayBaseUrl: oc.gatewayUrl,
|
|
54
|
+
autonomous: !!opts.autonomous,
|
|
55
|
+
withMcp: !!opts.connectToken,
|
|
56
|
+
// oc-23: Claude loads .hiveop/context.md via its SessionStart hook; OpenCode has none, so it reads it as instructions.
|
|
57
|
+
instructions: [exports.HIVEOP_CONTEXT_ENV_VALUE],
|
|
58
|
+
...(opts.mcpServer ? { mcpCommand: [opts.mcpServer.command, ...opts.mcpServer.args] } : {}),
|
|
59
|
+
...(opts.apiUrl ? { mcpEnvPassthrough: ['MARKETPLACE_API_URL'] } : {}),
|
|
60
|
+
}),
|
|
61
|
+
// oc-27: the HiveOp plugin (activity, titles, Needs-you, session-id capture) — as hosted does.
|
|
62
|
+
...(0, opencodePlugin_1.buildOpenCodePluginConfig)(),
|
|
63
|
+
};
|
|
59
64
|
const env = {
|
|
60
65
|
[exports.HIVEOP_CONTEXT_ENV_VAR]: exports.HIVEOP_CONTEXT_ENV_VALUE,
|
|
61
66
|
...opencodeConfig_1.OPENCODE_QUIET_ENV,
|
|
62
67
|
OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
|
|
63
68
|
[opencodeConfig_1.OPENCODE_GATEWAY_TOKEN_ENV]: oc.gatewayToken,
|
|
69
|
+
// oc-27: the plugin POSTs the captured session id to <gateway>/hiveop/session (resume).
|
|
70
|
+
HIVEOP_GATEWAY_URL: oc.gatewayUrl,
|
|
64
71
|
};
|
|
65
72
|
if (opts.connectToken)
|
|
66
73
|
env.WENRWA_CONNECT_TOKEN = opts.connectToken;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const OPENCODE_PACKAGE = "opencode-ai";
|
|
2
|
+
export declare const OPENCODE_INSTALL_TIMEOUT_MS: number;
|
|
3
|
+
export interface OpencodeBinaryDeps {
|
|
4
|
+
exists: (p: string) => boolean;
|
|
5
|
+
writeFile: (p: string, content: string) => void;
|
|
6
|
+
mkdirp: (dir: string) => void;
|
|
7
|
+
rm: (p: string) => void;
|
|
8
|
+
/** Run npm with exactly this env. Resolves { code } (never rejects on non-zero). */
|
|
9
|
+
runNpm: (args: string[], env: NodeJS.ProcessEnv) => Promise<{
|
|
10
|
+
code: number;
|
|
11
|
+
stderr?: string;
|
|
12
|
+
}>;
|
|
13
|
+
/** PATH lookup: absolute path + `--version` output of the `opencode` on PATH, or null. */
|
|
14
|
+
pathOpencode: () => {
|
|
15
|
+
path: string;
|
|
16
|
+
version: string | null;
|
|
17
|
+
} | null;
|
|
18
|
+
/** `<bin> --version` output, or null if it will not run. */
|
|
19
|
+
versionOf: (bin: string) => string | null;
|
|
20
|
+
env: NodeJS.ProcessEnv;
|
|
21
|
+
platform: NodeJS.Platform;
|
|
22
|
+
}
|
|
23
|
+
export interface LaunchShape {
|
|
24
|
+
command: string;
|
|
25
|
+
args: string[];
|
|
26
|
+
env: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
export type PreparedLaunch = {
|
|
29
|
+
ok: true;
|
|
30
|
+
launch: LaunchShape;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
message: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function managedPrefix(baseDir: string, version?: string): string;
|
|
36
|
+
export declare const managedBin: (prefix: string) => string;
|
|
37
|
+
/** Short lines on purpose: a narrow pane wraps long ones badly. */
|
|
38
|
+
export declare function opencodeInstallFailureLines(reason: string): string[];
|
|
39
|
+
/** POSIX sh waiter: progress now, then exec once the bridge marks the install ready (or print the failure). */
|
|
40
|
+
export declare const OPENCODE_WAIT_SCRIPT: string;
|
|
41
|
+
export declare function createOpencodeProvisioner(baseDir: string, deps: OpencodeBinaryDeps): {
|
|
42
|
+
managedBin: string;
|
|
43
|
+
/** Resolve (or start provisioning) and return the launch to spawn. Synchronous. */
|
|
44
|
+
prepare(launch: LaunchShape): PreparedLaunch;
|
|
45
|
+
/** Test seam: resolves when the in-flight install (if any) settles. */
|
|
46
|
+
settled: () => Promise<void>;
|
|
47
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.OPENCODE_WAIT_SCRIPT = exports.managedBin = exports.OPENCODE_INSTALL_TIMEOUT_MS = exports.OPENCODE_PACKAGE = void 0;
|
|
37
|
+
exports.managedPrefix = managedPrefix;
|
|
38
|
+
exports.opencodeInstallFailureLines = opencodeInstallFailureLines;
|
|
39
|
+
exports.createOpencodeProvisioner = createOpencodeProvisioner;
|
|
40
|
+
/**
|
|
41
|
+
* oc-25 — the bridge provides OpenCode itself (ADR-0013 local-lane parity).
|
|
42
|
+
*
|
|
43
|
+
* A user must never install OpenCode by hand. Binary choice, in order:
|
|
44
|
+
* 1. the bridge-managed pinned copy (~/.wen-market/opencode/<pinned>), if there;
|
|
45
|
+
* 2. else a PATH `opencode` at or above the minimum;
|
|
46
|
+
* 3. else install the pinned version into the managed prefix (lazy: only on
|
|
47
|
+
* the first OpenCode launch, so Claude-only users never download it).
|
|
48
|
+
*
|
|
49
|
+
* The launch always carries an ABSOLUTE binary path. While an install runs the
|
|
50
|
+
* PTY runs a tiny `sh` waiter (progress line, then exec) — the pane shows
|
|
51
|
+
* "Setting up OpenCode" instead of a blank wait or a bare shell. Concurrent
|
|
52
|
+
* opens share ONE install (in-flight promise). The install must NOT inherit
|
|
53
|
+
* OPENCODE_QUIET_ENV's npm_config_offline=true, so it runs with a scrubbed env.
|
|
54
|
+
*/
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const opencodeConfig_1 = require("./opencodeConfig");
|
|
57
|
+
exports.OPENCODE_PACKAGE = 'opencode-ai';
|
|
58
|
+
exports.OPENCODE_INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
59
|
+
function managedPrefix(baseDir, version = opencodeConfig_1.OPENCODE_PINNED_VERSION) {
|
|
60
|
+
return path.join(baseDir, version);
|
|
61
|
+
}
|
|
62
|
+
const managedBin = (prefix) => path.join(prefix, 'node_modules', '.bin', 'opencode');
|
|
63
|
+
exports.managedBin = managedBin;
|
|
64
|
+
const readyMarker = (prefix) => path.join(prefix, '.ready');
|
|
65
|
+
const failMarker = (prefix) => path.join(prefix, '.install-failed');
|
|
66
|
+
/** Short lines on purpose: a narrow pane wraps long ones badly. */
|
|
67
|
+
function opencodeInstallFailureLines(reason) {
|
|
68
|
+
const why = reason.replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
69
|
+
return [
|
|
70
|
+
'Could not set up OpenCode.',
|
|
71
|
+
...(why ? [why] : []),
|
|
72
|
+
'Install it by hand:',
|
|
73
|
+
`npm i -g ${exports.OPENCODE_PACKAGE}@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`,
|
|
74
|
+
'Then reopen this pane.',
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
/** POSIX sh waiter: progress now, then exec once the bridge marks the install ready (or print the failure). */
|
|
78
|
+
exports.OPENCODE_WAIT_SCRIPT = [
|
|
79
|
+
'ready=$1; fail=$2; shift 2',
|
|
80
|
+
"printf '\\r\\n\\033[2mSetting up OpenCode (one-time, ~30s)...\\033[0m\\r\\n'",
|
|
81
|
+
'while [ ! -e "$ready" ]; do',
|
|
82
|
+
' if [ -e "$fail" ]; then',
|
|
83
|
+
" printf '\\r\\n'",
|
|
84
|
+
" while IFS= read -r l; do printf '\\033[31m%s\\033[0m\\r\\n' \"$l\"; done < \"$fail\"",
|
|
85
|
+
' exit 1',
|
|
86
|
+
' fi',
|
|
87
|
+
' sleep 1',
|
|
88
|
+
'done',
|
|
89
|
+
'exec "$@"',
|
|
90
|
+
].join('\n');
|
|
91
|
+
function createOpencodeProvisioner(baseDir, deps) {
|
|
92
|
+
const prefix = managedPrefix(baseDir);
|
|
93
|
+
const bin = (0, exports.managedBin)(prefix);
|
|
94
|
+
let inflight = null;
|
|
95
|
+
function install() {
|
|
96
|
+
if (inflight)
|
|
97
|
+
return inflight;
|
|
98
|
+
// Clear the previous verdict synchronously so a waiter spawned right after never reads a stale one.
|
|
99
|
+
try {
|
|
100
|
+
deps.rm(failMarker(prefix));
|
|
101
|
+
}
|
|
102
|
+
catch { /* none */ }
|
|
103
|
+
const env = { ...deps.env };
|
|
104
|
+
for (const k of Object.keys(env))
|
|
105
|
+
if (k.toLowerCase() === 'npm_config_offline')
|
|
106
|
+
delete env[k];
|
|
107
|
+
const fail = (reason) => {
|
|
108
|
+
try {
|
|
109
|
+
deps.writeFile(failMarker(prefix), opencodeInstallFailureLines(reason).join('\n') + '\n');
|
|
110
|
+
}
|
|
111
|
+
catch { /* best effort */ }
|
|
112
|
+
};
|
|
113
|
+
const run = (async () => {
|
|
114
|
+
try {
|
|
115
|
+
deps.mkdirp(prefix);
|
|
116
|
+
const pkg = path.join(prefix, 'package.json');
|
|
117
|
+
if (!deps.exists(pkg))
|
|
118
|
+
deps.writeFile(pkg, JSON.stringify({ name: 'wen-market-opencode', private: true }) + '\n');
|
|
119
|
+
const res = await deps.runNpm(['install', '--prefix', prefix, `${exports.OPENCODE_PACKAGE}@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`, '--no-audit', '--no-fund'], env);
|
|
120
|
+
if (res.code !== 0)
|
|
121
|
+
return fail(`npm exited ${res.code}`);
|
|
122
|
+
if (!deps.exists(bin) || !(0, opencodeConfig_1.opencodeVersionOk)(deps.versionOf(bin)))
|
|
123
|
+
return fail('installed binary will not run');
|
|
124
|
+
deps.writeFile(readyMarker(prefix), opencodeConfig_1.OPENCODE_PINNED_VERSION);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
inflight = run;
|
|
131
|
+
void run.then(() => { if (inflight === run)
|
|
132
|
+
inflight = null; });
|
|
133
|
+
return run;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
managedBin: bin,
|
|
137
|
+
/** Resolve (or start provisioning) and return the launch to spawn. Synchronous. */
|
|
138
|
+
prepare(launch) {
|
|
139
|
+
const withBin = (b) => ({ ...launch, command: b });
|
|
140
|
+
if (deps.exists(bin) && deps.exists(readyMarker(prefix)))
|
|
141
|
+
return { ok: true, launch: withBin(bin) };
|
|
142
|
+
const onPath = deps.pathOpencode();
|
|
143
|
+
if (onPath && (0, opencodeConfig_1.opencodeVersionOk)(onPath.version))
|
|
144
|
+
return { ok: true, launch: withBin(onPath.path) };
|
|
145
|
+
if (deps.platform === 'win32') {
|
|
146
|
+
return { ok: false, message: opencodeInstallFailureLines('automatic setup is not on Windows yet').join('\r\n') };
|
|
147
|
+
}
|
|
148
|
+
void install();
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
launch: {
|
|
152
|
+
...launch,
|
|
153
|
+
command: '/bin/sh',
|
|
154
|
+
args: ['-c', exports.OPENCODE_WAIT_SCRIPT, 'opencode-wait', readyMarker(prefix), failMarker(prefix), bin, ...launch.args],
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
/** Test seam: resolves when the in-flight install (if any) settles. */
|
|
159
|
+
settled: () => inflight ?? Promise.resolve(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
/** Oldest OpenCode the local lane accepts. 1.18.35 is the version oc-04 verified
|
|
12
12
|
* (`permission.asked` / `session.idle` events, `-s <id>` resume, quiet env). */
|
|
13
|
+
export declare const OPENCODE_PINNED_VERSION = "1.18.35";
|
|
14
|
+
/** oc-25: the version the bridge installs for itself IS the minimum it accepts from PATH. */
|
|
13
15
|
export declare const OPENCODE_MIN_VERSION = "1.18.35";
|
|
14
16
|
export declare const OPENCODE_INSTALL_HINT = "Install OpenCode: npm i -g opencode-ai@>=1.18.35";
|
|
15
17
|
/** Every outbound call OpenCode makes at startup besides the gateway (oc-04 s5). */
|
|
16
18
|
export declare const OPENCODE_QUIET_ENV: Readonly<Record<string, string>>;
|
|
19
|
+
export declare const OPENCODE_THEME = "system";
|
|
17
20
|
export declare const OPENCODE_PROVIDER_ID = "hiveop";
|
|
18
21
|
export declare const OPENCODE_GATEWAY_TOKEN_ENV = "HIVEOP_GATEWAY_TOKEN";
|
|
19
22
|
export interface OpencodeConfigInput {
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
* (`{env:HIVEOP_GATEWAY_TOKEN}`) so it never appears inside the JSON.
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.isCapturedOpencodeSessionId = exports.OPENCODE_GATEWAY_TOKEN_ENV = exports.OPENCODE_PROVIDER_ID = exports.OPENCODE_QUIET_ENV = exports.OPENCODE_INSTALL_HINT = exports.OPENCODE_MIN_VERSION = void 0;
|
|
13
|
+
exports.isCapturedOpencodeSessionId = exports.OPENCODE_GATEWAY_TOKEN_ENV = exports.OPENCODE_PROVIDER_ID = exports.OPENCODE_THEME = exports.OPENCODE_QUIET_ENV = exports.OPENCODE_INSTALL_HINT = exports.OPENCODE_MIN_VERSION = exports.OPENCODE_PINNED_VERSION = void 0;
|
|
14
14
|
exports.buildOpencodeConfig = buildOpencodeConfig;
|
|
15
15
|
exports.opencodeVersionOk = opencodeVersionOk;
|
|
16
16
|
/** Oldest OpenCode the local lane accepts. 1.18.35 is the version oc-04 verified
|
|
17
17
|
* (`permission.asked` / `session.idle` events, `-s <id>` resume, quiet env). */
|
|
18
|
-
exports.
|
|
18
|
+
exports.OPENCODE_PINNED_VERSION = '1.18.35';
|
|
19
|
+
/** oc-25: the version the bridge installs for itself IS the minimum it accepts from PATH. */
|
|
20
|
+
exports.OPENCODE_MIN_VERSION = exports.OPENCODE_PINNED_VERSION;
|
|
19
21
|
exports.OPENCODE_INSTALL_HINT = `Install OpenCode: npm i -g opencode-ai@>=${exports.OPENCODE_MIN_VERSION}`;
|
|
20
22
|
/** Every outbound call OpenCode makes at startup besides the gateway (oc-04 s5). */
|
|
21
23
|
exports.OPENCODE_QUIET_ENV = Object.freeze({
|
|
@@ -27,6 +29,7 @@ exports.OPENCODE_QUIET_ENV = Object.freeze({
|
|
|
27
29
|
OPENCODE_DISABLE_TERMINAL_TITLE: '1',
|
|
28
30
|
npm_config_offline: 'true',
|
|
29
31
|
});
|
|
32
|
+
exports.OPENCODE_THEME = 'system';
|
|
30
33
|
exports.OPENCODE_PROVIDER_ID = 'hiveop';
|
|
31
34
|
exports.OPENCODE_GATEWAY_TOKEN_ENV = 'HIVEOP_GATEWAY_TOKEN';
|
|
32
35
|
function buildOpencodeConfig(input) {
|
|
@@ -39,6 +42,9 @@ function buildOpencodeConfig(input) {
|
|
|
39
42
|
model: `${exports.OPENCODE_PROVIDER_ID}/${model}`,
|
|
40
43
|
// OpenCode makes a second, billed "title" call on the small model.
|
|
41
44
|
small_model: `${exports.OPENCODE_PROVIDER_ID}/${model}`,
|
|
45
|
+
// oc-27: OpenCode's built-in "system" theme derives its palette from the terminal and
|
|
46
|
+
// leaves the background transparent, so the pane blends with the canvas like Claude's.
|
|
47
|
+
theme: exports.OPENCODE_THEME,
|
|
42
48
|
provider: {
|
|
43
49
|
[exports.OPENCODE_PROVIDER_ID]: {
|
|
44
50
|
npm: '@ai-sdk/openai-compatible',
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* `replay`, exactly as it would against the local server.
|
|
18
18
|
*/
|
|
19
19
|
import { type OpencodeLaunchInput } from './agentLaunch';
|
|
20
|
+
import type { PreparedLaunch } from './opencodeBinary';
|
|
20
21
|
import { SessionManager } from './sessionManager';
|
|
21
22
|
import { MemInfo } from './containerMemory';
|
|
22
23
|
import { BrowserDriver } from './browserDriver';
|
|
@@ -200,6 +201,12 @@ export interface RelayClientOpts {
|
|
|
200
201
|
* error frame the conductor can report.
|
|
201
202
|
*/
|
|
202
203
|
commandExists?: (command: string) => boolean;
|
|
204
|
+
/** oc-25 — see BridgeServerOpts#prepareOpencodeLaunch. */
|
|
205
|
+
prepareOpencodeLaunch?: (launch: {
|
|
206
|
+
command: string;
|
|
207
|
+
args: string[];
|
|
208
|
+
env: Record<string, string>;
|
|
209
|
+
}) => PreparedLaunch;
|
|
203
210
|
/**
|
|
204
211
|
* fcct-27 — RELAY-OWNED SESSIONS ONLY. The consent boundary, made structural.
|
|
205
212
|
*
|
|
@@ -1060,9 +1060,17 @@ function startRelayClient(opts) {
|
|
|
1060
1060
|
// no override, so realPty spawns $SHELL and autolaunch.sh turns it
|
|
1061
1061
|
// into claude. Present (laptop) → `claude` explicitly, since
|
|
1062
1062
|
// nothing on a user's machine does that for us.
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1063
|
+
let launch = opts.resolveLaunch?.({ workspaceId, sessionId: relaySessionId, autonomous, opencode, connectToken }) ?? null;
|
|
1064
|
+
let refusal = null;
|
|
1065
|
+
if (launch && launch.command === 'opencode' && opts.prepareOpencodeLaunch) {
|
|
1066
|
+
const prepared = opts.prepareOpencodeLaunch({ ...launch, env: launch.env ?? {} });
|
|
1067
|
+
if (prepared.ok)
|
|
1068
|
+
launch = prepared.launch;
|
|
1069
|
+
else
|
|
1070
|
+
refusal = prepared.message;
|
|
1071
|
+
}
|
|
1072
|
+
if (launch && (refusal || (opts.commandExists && !opts.commandExists(launch.command)))) {
|
|
1073
|
+
const message = refusal ?? (0, agentLaunch_1.missingCommandMessage)(launch.command);
|
|
1066
1074
|
console.error(`[bridge] refusing PTY spawn — ${message}`);
|
|
1067
1075
|
send(socket, {
|
|
1068
1076
|
t: 'error',
|
package/dist/bridge/server.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { SessionManager } from './sessionManager';
|
|
|
2
2
|
import { ValidatePathResult } from './localPathValidation';
|
|
3
3
|
import { PickFolderResult } from './folderPicker';
|
|
4
4
|
import { LocalAddCoreResult } from './localAdd';
|
|
5
|
+
import type { PreparedLaunch } from './opencodeBinary';
|
|
5
6
|
import { VerifyOriginResult } from './originVerification';
|
|
6
7
|
import { PublishResult } from './localPublish';
|
|
7
8
|
import { PublishPreflightResult } from './publishPreflight';
|
|
@@ -108,6 +109,14 @@ export interface BridgeServerOpts {
|
|
|
108
109
|
* crashes the open — see the 'open' handler below.
|
|
109
110
|
*/
|
|
110
111
|
commandExists?: (command: string) => boolean;
|
|
112
|
+
/** oc-25 — resolves/provisions OpenCode for an `opencode` launch: returns the
|
|
113
|
+
* launch to spawn (absolute binary, or a progress waiter while the bridge
|
|
114
|
+
* installs the pinned copy) or a refusal message. Absent → bare `opencode`. */
|
|
115
|
+
prepareOpencodeLaunch?: (launch: {
|
|
116
|
+
command: string;
|
|
117
|
+
args: string[];
|
|
118
|
+
env: Record<string, string>;
|
|
119
|
+
}) => PreparedLaunch;
|
|
111
120
|
/**
|
|
112
121
|
* psp-01 (production bug, 2026-09-14; supersedes bres-01's `hasSessionHistory`)
|
|
113
122
|
* — does Claude Code already have a conversation on disk for THIS AGENT at
|
|
@@ -235,6 +244,9 @@ export interface BridgeServerOpts {
|
|
|
235
244
|
args: string[];
|
|
236
245
|
};
|
|
237
246
|
mcpApiUrl?: string;
|
|
247
|
+
/** oc-27 — the bridge's own stored Connect Token: the fallback for an `open` frame that carries none
|
|
248
|
+
* (loopback frames from the browser never do), so local OpenCode agents get the same MCP as Claude. */
|
|
249
|
+
connectToken?: string;
|
|
238
250
|
/**
|
|
239
251
|
* rival-10 — every file touch of a LOCAL WORKSPACE AGENT (an `open` with
|
|
240
252
|
* agentId + workspaceId), handed on for the ranked rival feed. The hook
|
package/dist/bridge/server.js
CHANGED
|
@@ -733,10 +733,10 @@ function startBridgeServer(opts) {
|
|
|
733
733
|
// folder still falls back to $HOME for its PTY cwd, but the
|
|
734
734
|
// workgraph MCP pin should still match whatever workspace the
|
|
735
735
|
// arena client actually asked to open.
|
|
736
|
-
|
|
736
|
+
let launch = (0, agentLaunch_1.resolveAgentLaunch)({
|
|
737
737
|
autonomous: !!frame.autonomous,
|
|
738
738
|
...(frame.opencode ? { opencode: frame.opencode } : {}),
|
|
739
|
-
...(frame.connectToken ? { connectToken: frame.connectToken } : {}),
|
|
739
|
+
...((frame.connectToken || opts.connectToken) ? { connectToken: (frame.connectToken || opts.connectToken) } : {}),
|
|
740
740
|
...(opts.mcpServer ? { mcpServer: opts.mcpServer } : {}),
|
|
741
741
|
...(opts.mcpApiUrl ? { apiUrl: opts.mcpApiUrl } : {}),
|
|
742
742
|
...(cwd ? { sessionPin: { agentId, exists: agentSessionExists } } : {}),
|
|
@@ -745,7 +745,15 @@ function startBridgeServer(opts) {
|
|
|
745
745
|
: {}),
|
|
746
746
|
...(frame.workspaceId ? { workspaceId: frame.workspaceId } : {}),
|
|
747
747
|
});
|
|
748
|
-
|
|
748
|
+
let refusal = null;
|
|
749
|
+
if (launch.command === 'opencode' && opts.prepareOpencodeLaunch) {
|
|
750
|
+
const prepared = opts.prepareOpencodeLaunch(launch);
|
|
751
|
+
if (prepared.ok)
|
|
752
|
+
launch = prepared.launch;
|
|
753
|
+
else
|
|
754
|
+
refusal = prepared.message;
|
|
755
|
+
}
|
|
756
|
+
const claudeAvailable = !refusal && (opts.commandExists ? opts.commandExists(launch.command) : true);
|
|
749
757
|
if (!claudeAvailable) {
|
|
750
758
|
// Never crash on a missing binary: open a plain session (no
|
|
751
759
|
// command override — realPty's $SHELL default) so the pane
|
|
@@ -771,9 +779,11 @@ function startBridgeServer(opts) {
|
|
|
771
779
|
send(ws, {
|
|
772
780
|
t: 'data',
|
|
773
781
|
sessionId,
|
|
774
|
-
data:
|
|
775
|
-
? `\r\n\x1b[31m${
|
|
776
|
-
:
|
|
782
|
+
data: refusal
|
|
783
|
+
? `\r\n\x1b[31m${refusal}\x1b[0m\r\n`
|
|
784
|
+
: launch.command === 'opencode'
|
|
785
|
+
? `\r\n\x1b[31m${(0, agentLaunch_1.missingCommandMessage)('opencode')}, then reopen this pane.\x1b[0m\r\n`
|
|
786
|
+
: '\r\n\x1b[31mclaude not found on PATH — install Claude Code CLI, then reopen this pane.\x1b[0m\r\n',
|
|
777
787
|
});
|
|
778
788
|
}).catch((err) => {
|
|
779
789
|
console.error('[bridge] session attach failed:', err);
|
package/dist/commands/bridge.js
CHANGED
|
@@ -70,6 +70,7 @@ const realPty_1 = require("../bridge/realPty");
|
|
|
70
70
|
const config_1 = require("../bridge/config");
|
|
71
71
|
const mcpRegister_1 = require("../bridge/mcpRegister");
|
|
72
72
|
const mcpInstall_1 = require("../bridge/mcpInstall");
|
|
73
|
+
const opencodeBinary_1 = require("../bridge/opencodeBinary");
|
|
73
74
|
const autostart_1 = require("../bridge/autostart");
|
|
74
75
|
const repoCheckout_1 = require("../bridge/repoCheckout");
|
|
75
76
|
const bundleInstaller_1 = require("../bridge/bundleInstaller");
|
|
@@ -192,6 +193,43 @@ function opencodeUsable() {
|
|
|
192
193
|
* pane). One implementation so the two lanes can never disagree about whether
|
|
193
194
|
* this machine can run Claude Code.
|
|
194
195
|
*/
|
|
196
|
+
/**
|
|
197
|
+
* oc-25 — ONE provisioner for the process, so the loopback lane and the crew
|
|
198
|
+
* relay lane (and every pane) share a single in-flight OpenCode install.
|
|
199
|
+
*/
|
|
200
|
+
const opencodeProvisioner = (0, opencodeBinary_1.createOpencodeProvisioner)(path.join(os.homedir(), '.wen-market', 'opencode'), {
|
|
201
|
+
exists: (p) => fs.existsSync(p),
|
|
202
|
+
writeFile: (p, c) => fs.writeFileSync(p, c),
|
|
203
|
+
mkdirp: (d) => fs.mkdirSync(d, { recursive: true }),
|
|
204
|
+
rm: (p) => fs.rmSync(p, { force: true }),
|
|
205
|
+
runNpm: (args, env) => new Promise((resolve) => {
|
|
206
|
+
(0, child_process_1.execFile)(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { env, timeout: opencodeBinary_1.OPENCODE_INSTALL_TIMEOUT_MS }, (err, _out, stderr) => {
|
|
207
|
+
resolve({ code: err ? (typeof err.code === 'number' ? err.code : 1) : 0, stderr: String(stderr ?? '') });
|
|
208
|
+
});
|
|
209
|
+
}),
|
|
210
|
+
pathOpencode: () => {
|
|
211
|
+
try {
|
|
212
|
+
const abs = (0, child_process_1.execFileSync)(process.platform === 'win32' ? 'where' : 'command', process.platform === 'win32' ? ['opencode'] : ['-v', 'opencode'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).split(/\r?\n/)[0].trim();
|
|
213
|
+
if (!abs)
|
|
214
|
+
return null;
|
|
215
|
+
return { path: abs, version: opencodeVersionOf(abs) };
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
versionOf: (bin) => opencodeVersionOf(bin),
|
|
222
|
+
env: process.env,
|
|
223
|
+
platform: process.platform,
|
|
224
|
+
});
|
|
225
|
+
function opencodeVersionOf(bin) {
|
|
226
|
+
try {
|
|
227
|
+
return (0, child_process_1.execFileSync)(bin, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 15000 });
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
195
233
|
function commandExistsOnPath(command) {
|
|
196
234
|
if (command === 'opencode')
|
|
197
235
|
return opencodeUsable();
|
|
@@ -1089,6 +1127,8 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1089
1127
|
hiveOpSettingsPath,
|
|
1090
1128
|
...(mcpInstall.ok ? { mcpServer: { command: mcpInstall.command, args: mcpInstall.args } } : {}),
|
|
1091
1129
|
mcpApiUrl: bridgeApiUrl(),
|
|
1130
|
+
// oc-27: local OpenCode panes get the HiveOp MCP with the same token Claude's registration uses.
|
|
1131
|
+
connectToken: config.connectToken,
|
|
1092
1132
|
// tchp-18: the loopback changes reporter is opt-in; without this the
|
|
1093
1133
|
// tchp-12 machinery never constructs on a laptop and local panels sit
|
|
1094
1134
|
// on "Waiting for changes…" forever.
|
|
@@ -1210,6 +1250,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1210
1250
|
// agent's PTY, so a missing install surfaces a clear in-pane error
|
|
1211
1251
|
// line (server.ts) instead of a raw spawn ENOENT crash.
|
|
1212
1252
|
commandExists: (command) => commandExistsOnPath(command),
|
|
1253
|
+
prepareOpencodeLaunch: (l) => opencodeProvisioner.prepare(l),
|
|
1213
1254
|
// psp-01 — real on-disk check: does THIS AGENT already have a Claude
|
|
1214
1255
|
// Code conversation for this cwd (a "<agentId>.jsonl" transcript)?
|
|
1215
1256
|
// Drives server.ts's per-agent session-pin decision on the
|
|
@@ -1314,7 +1355,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1314
1355
|
resolveLaunch: ({ sessionId, opencode, connectToken }) => (0, agentLaunch_1.resolveAgentLaunch)({
|
|
1315
1356
|
autonomous: true,
|
|
1316
1357
|
...(opencode ? { opencode } : {}),
|
|
1317
|
-
...(connectToken ? { connectToken } : {}),
|
|
1358
|
+
...((connectToken || config.connectToken) ? { connectToken: (connectToken || config.connectToken) } : {}),
|
|
1318
1359
|
...(mcpInstall.ok ? { mcpServer: { command: mcpInstall.command, args: mcpInstall.args } } : {}),
|
|
1319
1360
|
apiUrl: bridgeApiUrl(),
|
|
1320
1361
|
...(activityFile && hiveOpSettingsPath && sessionId
|
|
@@ -1324,6 +1365,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1324
1365
|
// A missing binary is refused rather than degraded to a shell — a crew
|
|
1325
1366
|
// member has no pane to print a red line into.
|
|
1326
1367
|
commandExists: (command) => commandExistsOnPath(command),
|
|
1368
|
+
prepareOpencodeLaunch: (l) => opencodeProvisioner.prepare(l),
|
|
1327
1369
|
// fcct-27 — this is what keeps "crews may run here" from becoming "the
|
|
1328
1370
|
// platform may drive any terminal on this machine". Both lanes share one
|
|
1329
1371
|
// SessionManager by necessity, so without it the relay could attach to,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenrwa/marketplace-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "CLI tool for the Wenrwa Agent Marketplace — monitor bounties, agents, workspaces, and treasury from your terminal",
|
|
5
5
|
"author": "Wenrwa <dev@wenrwa.com>",
|
|
6
6
|
"license": "MIT",
|