@wenrwa/marketplace-cli 0.7.8 → 0.8.2
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-activity-tail.test.js +16 -0
- package/dist/__tests__/bridge-attach-paste-engine.test.d.ts +1 -0
- package/dist/__tests__/bridge-attach-paste-engine.test.js +108 -0
- package/dist/__tests__/bridge-bundle-installer-opencode.test.d.ts +1 -0
- package/dist/__tests__/bridge-bundle-installer-opencode.test.js +69 -0
- package/dist/__tests__/bridge-context-engine.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-engine.test.js +112 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.js +19 -0
- package/dist/__tests__/bridge-crew-translator.test.d.ts +1 -0
- package/dist/__tests__/bridge-crew-translator.test.js +103 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.js +24 -0
- package/dist/__tests__/bridge-loopback-opencode-open.test.d.ts +1 -0
- package/dist/__tests__/bridge-loopback-opencode-open.test.js +51 -0
- 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.d.ts +1 -0
- package/dist/__tests__/bridge-opencode-launch.test.js +216 -0
- package/dist/__tests__/bridge-relay-pucc14.test.js +28 -0
- package/dist/__tests__/bridge-server.test.js +32 -0
- package/dist/__tests__/opencode-config-parity.test.d.ts +1 -0
- package/dist/__tests__/opencode-config-parity.test.js +85 -0
- package/dist/__tests__/opencode-plugin.test.d.ts +1 -0
- package/dist/__tests__/opencode-plugin.test.js +197 -0
- package/dist/__tests__/opencode-title.test.d.ts +1 -0
- package/dist/__tests__/opencode-title.test.js +99 -0
- package/dist/__tests__/opencode-transcript-reader.test.d.ts +1 -0
- package/dist/__tests__/opencode-transcript-reader.test.js +156 -0
- package/dist/bridge/activityTail.d.ts +7 -0
- package/dist/bridge/activityTail.js +1 -0
- package/dist/bridge/agentLaunch.d.ts +28 -0
- package/dist/bridge/agentLaunch.js +49 -0
- package/dist/bridge/attachmentWriter.d.ts +16 -0
- package/dist/bridge/attachmentWriter.js +24 -1
- package/dist/bridge/bundleInstaller.d.ts +3 -0
- package/dist/bridge/bundleInstaller.js +22 -8
- package/dist/bridge/claudeTranscriptReader.d.ts +7 -1
- package/dist/bridge/claudeTranscriptReader.js +12 -2
- package/dist/bridge/contextFile.d.ts +20 -0
- package/dist/bridge/contextFile.js +30 -0
- package/dist/bridge/contextWriter.d.ts +10 -1
- package/dist/bridge/contextWriter.js +29 -9
- package/dist/bridge/crewTranslator.d.ts +41 -0
- package/dist/bridge/crewTranslator.js +129 -0
- package/dist/bridge/launchSupervisor.d.ts +4 -0
- package/dist/bridge/opencodeBinary.d.ts +47 -0
- package/dist/bridge/opencodeBinary.js +161 -0
- package/dist/bridge/opencodeConfig.d.ts +43 -0
- package/dist/bridge/opencodeConfig.js +109 -0
- package/dist/bridge/opencodePlugin.d.ts +13 -0
- package/dist/bridge/opencodePlugin.js +54 -0
- package/dist/bridge/opencodeTranscriptReader.d.ts +24 -0
- package/dist/bridge/opencodeTranscriptReader.js +178 -0
- package/dist/bridge/relayClient.d.ts +34 -1
- package/dist/bridge/relayClient.js +33 -8
- package/dist/bridge/screenshotAttach.d.ts +2 -0
- package/dist/bridge/screenshotAttach.js +3 -1
- package/dist/bridge/server.d.ts +15 -0
- package/dist/bridge/server.js +21 -3
- package/dist/bridge/sessionManager.d.ts +5 -0
- package/dist/bridge/sessionManager.js +21 -1
- package/dist/commands/bridge.js +76 -9
- package/hooks/opencode-plugin.mjs +186 -0
- package/package.json +1 -1
|
@@ -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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* oc-08 — the local bridge launches OpenCode agents.
|
|
5
|
+
* Resolver per engine (exact command/args/env), the install hint, the minimum
|
|
6
|
+
* version, `-s` only for a captured id, and the relay lane carrying the
|
|
7
|
+
* server-delivered launch block through to the spawn.
|
|
8
|
+
*/
|
|
9
|
+
const ws_1 = require("ws");
|
|
10
|
+
const agentLaunch_1 = require("../bridge/agentLaunch");
|
|
11
|
+
const opencodeConfig_1 = require("../bridge/opencodeConfig");
|
|
12
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
13
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
14
|
+
const GW = 'https://api.example.test/api/v1/opencode/v1';
|
|
15
|
+
const oc = (extra = {}) => ({
|
|
16
|
+
engine: 'opencode', model: 'moonshotai/kimi-k2', gatewayUrl: GW, gatewayToken: 'hgw_abc', ...extra,
|
|
17
|
+
});
|
|
18
|
+
describe('oc-07 — local MCP entry', () => {
|
|
19
|
+
const mcpOf = (l) => JSON.parse(l.env.OPENCODE_CONFIG_CONTENT).mcp['wenrwa-marketplace'];
|
|
20
|
+
it('uses the resolved installed server (never npx) and passes MARKETPLACE_API_URL by reference', () => {
|
|
21
|
+
const l = (0, agentLaunch_1.resolveAgentLaunch)({
|
|
22
|
+
opencode: oc(), connectToken: 'tok',
|
|
23
|
+
mcpServer: { command: '/usr/bin/node', args: ['/h/.wen-market/mcp/dist/index.js'] },
|
|
24
|
+
apiUrl: 'https://api.example.test/api/v1',
|
|
25
|
+
});
|
|
26
|
+
const m = mcpOf(l);
|
|
27
|
+
expect(m.command).toEqual(['/usr/bin/node', '/h/.wen-market/mcp/dist/index.js']);
|
|
28
|
+
expect(m.environment).toEqual({
|
|
29
|
+
WENRWA_CONNECT_TOKEN: '{env:WENRWA_CONNECT_TOKEN}',
|
|
30
|
+
MARKETPLACE_API_URL: '{env:MARKETPLACE_API_URL}',
|
|
31
|
+
});
|
|
32
|
+
expect(l.env.MARKETPLACE_API_URL).toBe('https://api.example.test/api/v1');
|
|
33
|
+
expect(l.env.OPENCODE_CONFIG_CONTENT).not.toContain('tok"');
|
|
34
|
+
});
|
|
35
|
+
it('npx fallback must not inherit npm_config_offline=true', () => {
|
|
36
|
+
const l = (0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc(), connectToken: 'tok' });
|
|
37
|
+
expect(l.env.npm_config_offline).toBe('true'); // the pane itself stays quiet
|
|
38
|
+
const m = mcpOf(l);
|
|
39
|
+
expect(m.command).toEqual(['npx', '@wenrwa/marketplace-mcp']);
|
|
40
|
+
expect(m.environment.npm_config_offline).toBe('false');
|
|
41
|
+
});
|
|
42
|
+
it('no connect token -> no mcp entry', () => {
|
|
43
|
+
expect(JSON.parse((0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc(), mcpServer: { command: 'n', args: [] } }).env.OPENCODE_CONFIG_CONTENT).mcp).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('resolveAgentLaunch per engine', () => {
|
|
47
|
+
it('Claude path is unchanged when no opencode block, or engine is claude_code', () => {
|
|
48
|
+
const base = (0, agentLaunch_1.resolveAgentLaunch)({ autonomous: true, workspaceId: 'w1' });
|
|
49
|
+
expect(base).toEqual({
|
|
50
|
+
command: 'claude',
|
|
51
|
+
args: ['--dangerously-skip-permissions'],
|
|
52
|
+
env: { HIVEOP_CONTEXT_FILE: '.hiveop/context.md', WENRWA_WORKSPACE_ID: 'w1' },
|
|
53
|
+
});
|
|
54
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({ autonomous: true, workspaceId: 'w1', opencode: { engine: 'claude_code' } })).toEqual(base);
|
|
55
|
+
// an opencode engine without a token/model/url must NOT half-launch
|
|
56
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({ opencode: { engine: 'opencode', model: 'm' } }).command).toBe('claude');
|
|
57
|
+
});
|
|
58
|
+
it('opencode: command, args, env, and no gateway token on the Claude path', () => {
|
|
59
|
+
const l = (0, agentLaunch_1.resolveAgentLaunch)({ autonomous: false, opencode: oc(), workspaceId: 'w1', sessionPin: { agentId: 'a1', exists: true } });
|
|
60
|
+
expect(l.command).toBe('opencode');
|
|
61
|
+
expect(l.args).toEqual([]);
|
|
62
|
+
expect(l.env).toMatchObject({
|
|
63
|
+
...opencodeConfig_1.OPENCODE_QUIET_ENV,
|
|
64
|
+
HIVEOP_GATEWAY_TOKEN: 'hgw_abc',
|
|
65
|
+
WENRWA_WORKSPACE_ID: 'w1',
|
|
66
|
+
WENRWA_AGENT_ID: 'a1',
|
|
67
|
+
HIVEOP_CONTEXT_FILE: '.hiveop/context.md',
|
|
68
|
+
});
|
|
69
|
+
const claude = (0, agentLaunch_1.resolveAgentLaunch)({ workspaceId: 'w1' });
|
|
70
|
+
expect(claude.env.HIVEOP_GATEWAY_TOKEN).toBeUndefined();
|
|
71
|
+
expect(claude.env.OPENCODE_CONFIG_CONTENT).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
it('config JSON: gateway provider, single model, small_model, no secrets inline', () => {
|
|
74
|
+
const l = (0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc() });
|
|
75
|
+
const cfg = JSON.parse(l.env.OPENCODE_CONFIG_CONTENT);
|
|
76
|
+
expect(cfg).toMatchObject({
|
|
77
|
+
autoupdate: false, share: 'disabled', enabled_providers: ['hiveop'],
|
|
78
|
+
model: 'hiveop/moonshotai/kimi-k2', small_model: 'hiveop/moonshotai/kimi-k2',
|
|
79
|
+
});
|
|
80
|
+
expect(cfg.provider.hiveop.options).toEqual({ baseURL: GW, apiKey: '{env:HIVEOP_GATEWAY_TOKEN}' });
|
|
81
|
+
expect(cfg.provider.hiveop.whitelist).toEqual(['moonshotai/kimi-k2']);
|
|
82
|
+
expect(cfg.permission).toBeUndefined();
|
|
83
|
+
expect(cfg.mcp).toBeUndefined();
|
|
84
|
+
expect(l.env.OPENCODE_CONFIG_CONTENT).not.toContain('hgw_abc');
|
|
85
|
+
expect(JSON.stringify(l)).not.toMatch(/OPENROUTER|sk-or-/i);
|
|
86
|
+
});
|
|
87
|
+
it('autonomous -> permission allow; connect token -> MCP server by env reference', () => {
|
|
88
|
+
const l = (0, agentLaunch_1.resolveAgentLaunch)({ autonomous: true, connectToken: 'ct_1', opencode: oc() });
|
|
89
|
+
const cfg = JSON.parse(l.env.OPENCODE_CONFIG_CONTENT);
|
|
90
|
+
expect(cfg.permission).toBe('allow');
|
|
91
|
+
expect(cfg.mcp['wenrwa-marketplace']).toMatchObject({
|
|
92
|
+
type: 'local', enabled: true, environment: { WENRWA_CONNECT_TOKEN: '{env:WENRWA_CONNECT_TOKEN}' },
|
|
93
|
+
});
|
|
94
|
+
expect(l.env.WENRWA_CONNECT_TOKEN).toBe('ct_1');
|
|
95
|
+
expect(l.env.OPENCODE_CONFIG_CONTENT).not.toContain('ct_1');
|
|
96
|
+
});
|
|
97
|
+
it('-s only when a CAPTURED (ses_) id exists; a pre-minted uuid never resumes', () => {
|
|
98
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc() }).args).toEqual([]);
|
|
99
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc({ sessionId: 'ses_29fabc123' }) }).args).toEqual(['-s', 'ses_29fabc123']);
|
|
100
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc({ sessionId: '2b7c1e0a-1111-4222-8333-444455556666' }) }).args).toEqual([]);
|
|
101
|
+
expect((0, opencodeConfig_1.isCapturedOpencodeSessionId)('ses_x; rm -rf')).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
it('buildOpencodeConfig is pure and deterministic', () => {
|
|
104
|
+
const i = { model: 'm', gatewayBaseUrl: GW, autonomous: true, withMcp: true };
|
|
105
|
+
expect((0, opencodeConfig_1.buildOpencodeConfig)(i)).toEqual((0, opencodeConfig_1.buildOpencodeConfig)(i));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('install hint and minimum version', () => {
|
|
109
|
+
it('hint mirrors the Claude message', () => {
|
|
110
|
+
expect(opencodeConfig_1.OPENCODE_INSTALL_HINT).toBe(`Install OpenCode: npm i -g opencode-ai@>=${opencodeConfig_1.OPENCODE_MIN_VERSION}`);
|
|
111
|
+
expect((0, agentLaunch_1.missingCommandMessage)('opencode')).toContain(opencodeConfig_1.OPENCODE_INSTALL_HINT);
|
|
112
|
+
expect((0, agentLaunch_1.missingCommandMessage)('claude')).toBe('claude not found on PATH — install Claude Code CLI on this machine');
|
|
113
|
+
});
|
|
114
|
+
it('version gate', () => {
|
|
115
|
+
expect((0, opencodeConfig_1.opencodeVersionOk)('1.18.35')).toBe(true);
|
|
116
|
+
expect((0, opencodeConfig_1.opencodeVersionOk)('opencode 1.18.36\n')).toBe(true);
|
|
117
|
+
expect((0, opencodeConfig_1.opencodeVersionOk)('1.19.0')).toBe(true);
|
|
118
|
+
expect((0, opencodeConfig_1.opencodeVersionOk)('2.0.0')).toBe(true);
|
|
119
|
+
expect((0, opencodeConfig_1.opencodeVersionOk)('1.18.34')).toBe(false);
|
|
120
|
+
expect((0, opencodeConfig_1.opencodeVersionOk)('0.99.99')).toBe(false);
|
|
121
|
+
expect((0, opencodeConfig_1.opencodeVersionOk)('garbage')).toBe(false);
|
|
122
|
+
expect((0, opencodeConfig_1.opencodeVersionOk)(null)).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
class FakePty {
|
|
126
|
+
opts;
|
|
127
|
+
constructor(opts) {
|
|
128
|
+
this.opts = opts;
|
|
129
|
+
}
|
|
130
|
+
write() { }
|
|
131
|
+
resize() { }
|
|
132
|
+
kill() { }
|
|
133
|
+
onData() { }
|
|
134
|
+
onExit() { }
|
|
135
|
+
}
|
|
136
|
+
function setup() {
|
|
137
|
+
const ptys = [];
|
|
138
|
+
const factory = { spawn: (o) => { const p = new FakePty(o); ptys.push(p); return p; } };
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
|
|
141
|
+
wss.on('listening', () => resolve({ wss, port: wss.address().port, ptys, factory }));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
describe('relay lane carries the server-delivered opencode block', () => {
|
|
145
|
+
it('spawns opencode with the gateway env; refuses with the install hint when missing', async () => {
|
|
146
|
+
const { wss, port, ptys, factory } = await setup();
|
|
147
|
+
const frames = [];
|
|
148
|
+
let conn;
|
|
149
|
+
const got = new Promise((r) => wss.on('connection', (ws) => { conn = ws; ws.on('message', (m) => frames.push(JSON.parse(String(m)))); r(); }));
|
|
150
|
+
let exists = true;
|
|
151
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
152
|
+
relayUrl: `ws://127.0.0.1:${port}`, relayToken: 'tok',
|
|
153
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
154
|
+
resolveLaunch: (o) => (0, agentLaunch_1.resolveAgentLaunch)({ autonomous: true, opencode: o.opencode, connectToken: o.connectToken }),
|
|
155
|
+
commandExists: () => exists,
|
|
156
|
+
});
|
|
157
|
+
await got;
|
|
158
|
+
const wait = async (pred) => { for (let i = 0; i < 100; i++) {
|
|
159
|
+
const f = frames.find(pred);
|
|
160
|
+
if (f)
|
|
161
|
+
return f;
|
|
162
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
163
|
+
} throw new Error('timeout'); };
|
|
164
|
+
conn.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, sessionId: 'a-1', workspaceId: 'w', connectToken: 'ct_9', opencode: oc({ sessionId: 'ses_abc' }) }));
|
|
165
|
+
await wait((f) => f.t === 'opened');
|
|
166
|
+
expect(ptys[0].opts.command).toBe('opencode');
|
|
167
|
+
expect(ptys[0].opts.args).toEqual(['-s', 'ses_abc']);
|
|
168
|
+
expect(ptys[0].opts.env).toMatchObject({ HIVEOP_GATEWAY_TOKEN: 'hgw_abc', OPENCODE_DISABLE_MODELS_FETCH: '1', WENRWA_CONNECT_TOKEN: 'ct_9' });
|
|
169
|
+
exists = false;
|
|
170
|
+
conn.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, sessionId: 'a-2', workspaceId: 'w', opencode: oc() }));
|
|
171
|
+
const err = await wait((f) => f.t === 'error' && f.sessionId === 'a-2');
|
|
172
|
+
expect(err.message).toContain(opencodeConfig_1.OPENCODE_INSTALL_HINT);
|
|
173
|
+
expect(ptys).toHaveLength(1);
|
|
174
|
+
handle.close();
|
|
175
|
+
for (const c of wss.clients)
|
|
176
|
+
c.terminate();
|
|
177
|
+
await new Promise((r) => wss.close(r));
|
|
178
|
+
});
|
|
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
|
+
});
|
|
@@ -331,6 +331,34 @@ describe('relay client — hooks-based activity tail (fcct-25)', () => {
|
|
|
331
331
|
handle.close();
|
|
332
332
|
await closeRelay(relay.wss);
|
|
333
333
|
});
|
|
334
|
+
it('oc-22: forwards an OpenCode needs_you record as an activity frame carrying kind', async () => {
|
|
335
|
+
fs.writeFileSync(activityFile, '');
|
|
336
|
+
process.env.HIVEOP_ACTIVITY_FILE = activityFile;
|
|
337
|
+
const relay = await startFakeRelay();
|
|
338
|
+
const { factory } = makeFactory();
|
|
339
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
340
|
+
let tick = null;
|
|
341
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
342
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
343
|
+
relayToken: 'tok',
|
|
344
|
+
sessions,
|
|
345
|
+
setIntervalFn: (cb) => { tick = cb; return 1; },
|
|
346
|
+
clearIntervalFn: () => { },
|
|
347
|
+
});
|
|
348
|
+
const ws = await relay.nextConnection();
|
|
349
|
+
const q = frameQueue(ws);
|
|
350
|
+
await q.next(); // auth
|
|
351
|
+
fs.appendFileSync(activityFile, JSON.stringify({ sessionId: 'agent-A', workspaceId: 'ws-7', kind: 'needs_you', thinking: 'Waiting for you' }) + '\n');
|
|
352
|
+
tick();
|
|
353
|
+
expect(await q.next()).toEqual({
|
|
354
|
+
t: 'activity',
|
|
355
|
+
sessionId: 'agent-A',
|
|
356
|
+
thinking: 'Waiting for you',
|
|
357
|
+
kind: 'needs_you',
|
|
358
|
+
});
|
|
359
|
+
handle.close();
|
|
360
|
+
await closeRelay(relay.wss);
|
|
361
|
+
});
|
|
334
362
|
it('never schedules the tail when HIVEOP_ACTIVITY_FILE is unset (no stray fs polling)', async () => {
|
|
335
363
|
delete process.env.HIVEOP_ACTIVITY_FILE;
|
|
336
364
|
const relay = await startFakeRelay();
|
|
@@ -814,6 +814,38 @@ describe('bridge WebSocket server — fcct-24 local-workspace-agent activity', (
|
|
|
814
814
|
await handle.close();
|
|
815
815
|
}
|
|
816
816
|
});
|
|
817
|
+
it('oc-22: local lane — an OpenCode needs_you record reaches the app as an activity frame carrying kind', async () => {
|
|
818
|
+
const file = fs.mkdtempSync(path.join(os.tmpdir(), 'oc22-server-activity-')) + '/activity.jsonl';
|
|
819
|
+
fs.writeFileSync(file, '');
|
|
820
|
+
const factory = { spawn: () => new FakePty() };
|
|
821
|
+
const tickBox = { fn: null };
|
|
822
|
+
const handle = await (0, server_1.startBridgeServer)({
|
|
823
|
+
sessionToken: TOKEN,
|
|
824
|
+
allowedOrigin: ORIGIN,
|
|
825
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
826
|
+
resolveLocalAgentCwd: () => '/Users/dev/my-project',
|
|
827
|
+
activityFile: file,
|
|
828
|
+
hiveOpSettingsPath: '/Users/t/.wen-market/claude-hooks-settings.json',
|
|
829
|
+
activityScheduleInterval: (cb) => { tickBox.fn = cb; return 'h'; },
|
|
830
|
+
activityCancelInterval: () => { },
|
|
831
|
+
});
|
|
832
|
+
try {
|
|
833
|
+
const { ws, q } = await openAndAuth(handle.port);
|
|
834
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, agentId: 'agent-1', workspaceId: 'ws-1' }));
|
|
835
|
+
await q.next();
|
|
836
|
+
fs.appendFileSync(file, JSON.stringify({ sessionId: 'agent-1', kind: 'needs_you', thinking: 'Waiting for you' }) + '\n');
|
|
837
|
+
tickBox.fn?.();
|
|
838
|
+
expect(await q.next()).toEqual({ t: 'activity', sessionId: 'agent-1', thinking: 'Waiting for you', kind: 'needs_you' });
|
|
839
|
+
// Claude's records carry kind 'tool'/'stop' — the outward frame must stay exactly as it was.
|
|
840
|
+
fs.appendFileSync(file, JSON.stringify({ sessionId: 'agent-1', kind: 'stop', thinking: '' }) + '\n');
|
|
841
|
+
tickBox.fn?.();
|
|
842
|
+
expect(await q.next()).toEqual({ t: 'activity', sessionId: 'agent-1', thinking: '' });
|
|
843
|
+
ws.close();
|
|
844
|
+
}
|
|
845
|
+
finally {
|
|
846
|
+
await handle.close();
|
|
847
|
+
}
|
|
848
|
+
});
|
|
817
849
|
});
|
|
818
850
|
// lws — GET /local-file/:workspaceId?path=…&token=… . Same origin+token gate
|
|
819
851
|
// as /local-tree; the interesting part is that refusal REASONS survive as
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
/**
|
|
37
|
+
* oc-05/oc-08 — ONE config contract. The hosted image builds OPENCODE_CONFIG_CONTENT with
|
|
38
|
+
* infra/terminal-image/opencode/build-config.js (the image may carry an older CLI, so it does
|
|
39
|
+
* not import the compiled builder); this pins it deep-equal to the canonical
|
|
40
|
+
* buildOpencodeConfig for equivalent inputs, and the quiet env identical.
|
|
41
|
+
*/
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const opencodeConfig_1 = require("../bridge/opencodeConfig");
|
|
45
|
+
const INFRA = path.join(__dirname, '..', '..', '..', 'infra', 'terminal-image');
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
47
|
+
const hosted = require(path.join(INFRA, 'opencode', 'build-config.js'));
|
|
48
|
+
const BASE = {
|
|
49
|
+
WENRWA_AGENT_MODEL: 'anthropic/claude-sonnet-4.5',
|
|
50
|
+
HIVEOP_GATEWAY_URL: 'https://api.wenrwa.com/api/v1/opencode/v1',
|
|
51
|
+
HIVEOP_GATEWAY_TOKEN: 'hgw_x',
|
|
52
|
+
};
|
|
53
|
+
const table = [
|
|
54
|
+
{ name: 'supervised, no mcp', env: BASE, autonomous: false, withMcp: false },
|
|
55
|
+
{ name: 'autonomous', env: { ...BASE, HT_AUTONOMOUS: '1' }, autonomous: true, withMcp: false },
|
|
56
|
+
{ name: 'mcp', env: { ...BASE, WENRWA_CONNECT_TOKEN: 'tok' }, autonomous: false, withMcp: true },
|
|
57
|
+
{ name: 'autonomous + mcp, other model', env: { ...BASE, WENRWA_AGENT_MODEL: 'moonshotai/kimi-k2', HT_AUTONOMOUS: '1', WENRWA_CONNECT_TOKEN: 'tok' }, autonomous: true, withMcp: true },
|
|
58
|
+
];
|
|
59
|
+
describe('hosted build-config.js == CLI buildOpencodeConfig', () => {
|
|
60
|
+
it.each(table)('$name', ({ env, autonomous, withMcp }) => {
|
|
61
|
+
const canonical = {
|
|
62
|
+
...(0, opencodeConfig_1.buildOpencodeConfig)({
|
|
63
|
+
model: env.WENRWA_AGENT_MODEL,
|
|
64
|
+
gatewayBaseUrl: env.HIVEOP_GATEWAY_URL,
|
|
65
|
+
autonomous,
|
|
66
|
+
withMcp,
|
|
67
|
+
mcpCommand: ['marketplace-mcp'],
|
|
68
|
+
mcpEnvPassthrough: ['MARKETPLACE_API_URL'],
|
|
69
|
+
}),
|
|
70
|
+
plugin: [hosted.PLUGIN_URL],
|
|
71
|
+
};
|
|
72
|
+
expect(hosted.buildConfig(env)).toEqual(canonical);
|
|
73
|
+
});
|
|
74
|
+
it('quiet env is identical, and autolaunch.sh exports exactly it', () => {
|
|
75
|
+
expect(hosted.QUIET_ENV).toEqual({ ...opencodeConfig_1.OPENCODE_QUIET_ENV });
|
|
76
|
+
const sh = fs.readFileSync(path.join(INFRA, 'autolaunch.sh'), 'utf8');
|
|
77
|
+
for (const [k, v] of Object.entries(opencodeConfig_1.OPENCODE_QUIET_ENV)) {
|
|
78
|
+
expect(sh).toMatch(new RegExp(`(^|\\s)${k}=${v}(\\s|$)`, 'm'));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
it('the hosted plugin URL is the file the Dockerfile installs', () => {
|
|
82
|
+
expect(fs.readFileSync(path.join(INFRA, 'Dockerfile'), 'utf8')).toContain('COPY opencode/hiveop-plugin.mjs /opt/hiveop/opencode/hiveop-plugin.mjs');
|
|
83
|
+
expect(hosted.PLUGIN_URL).toBe('file:///opt/hiveop/opencode/hiveop-plugin.mjs');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|