@wenrwa/marketplace-cli 0.7.7 → 0.8.1

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.
Files changed (61) hide show
  1. package/dist/__tests__/bridge-activity-tail.test.js +16 -0
  2. package/dist/__tests__/bridge-attach-paste-engine.test.d.ts +1 -0
  3. package/dist/__tests__/bridge-attach-paste-engine.test.js +108 -0
  4. package/dist/__tests__/bridge-attach-screenshot.test.d.ts +1 -0
  5. package/dist/__tests__/bridge-attach-screenshot.test.js +250 -0
  6. package/dist/__tests__/bridge-bundle-installer-opencode.test.d.ts +1 -0
  7. package/dist/__tests__/bridge-bundle-installer-opencode.test.js +69 -0
  8. package/dist/__tests__/bridge-context-engine.test.d.ts +1 -0
  9. package/dist/__tests__/bridge-context-engine.test.js +112 -0
  10. package/dist/__tests__/bridge-context-manifest-frame.test.js +19 -0
  11. package/dist/__tests__/bridge-crew-translator.test.d.ts +1 -0
  12. package/dist/__tests__/bridge-crew-translator.test.js +103 -0
  13. package/dist/__tests__/bridge-install-bundle-frame.test.js +24 -0
  14. package/dist/__tests__/bridge-loopback-opencode-open.test.d.ts +1 -0
  15. package/dist/__tests__/bridge-loopback-opencode-open.test.js +51 -0
  16. package/dist/__tests__/bridge-opencode-launch.test.d.ts +1 -0
  17. package/dist/__tests__/bridge-opencode-launch.test.js +179 -0
  18. package/dist/__tests__/bridge-relay-pucc14.test.js +28 -0
  19. package/dist/__tests__/bridge-server.test.js +32 -0
  20. package/dist/__tests__/opencode-config-parity.test.d.ts +1 -0
  21. package/dist/__tests__/opencode-config-parity.test.js +85 -0
  22. package/dist/__tests__/opencode-plugin.test.d.ts +1 -0
  23. package/dist/__tests__/opencode-plugin.test.js +197 -0
  24. package/dist/__tests__/opencode-title.test.d.ts +1 -0
  25. package/dist/__tests__/opencode-title.test.js +99 -0
  26. package/dist/__tests__/opencode-transcript-reader.test.d.ts +1 -0
  27. package/dist/__tests__/opencode-transcript-reader.test.js +156 -0
  28. package/dist/bridge/activityTail.d.ts +7 -0
  29. package/dist/bridge/activityTail.js +1 -0
  30. package/dist/bridge/agentLaunch.d.ts +28 -0
  31. package/dist/bridge/agentLaunch.js +49 -0
  32. package/dist/bridge/attachmentWriter.d.ts +16 -0
  33. package/dist/bridge/attachmentWriter.js +24 -1
  34. package/dist/bridge/bundleInstaller.d.ts +3 -0
  35. package/dist/bridge/bundleInstaller.js +22 -8
  36. package/dist/bridge/claudeTranscriptReader.d.ts +7 -1
  37. package/dist/bridge/claudeTranscriptReader.js +12 -2
  38. package/dist/bridge/contextFile.d.ts +20 -0
  39. package/dist/bridge/contextFile.js +30 -0
  40. package/dist/bridge/contextWriter.d.ts +10 -1
  41. package/dist/bridge/contextWriter.js +29 -9
  42. package/dist/bridge/crewTranslator.d.ts +41 -0
  43. package/dist/bridge/crewTranslator.js +129 -0
  44. package/dist/bridge/launchSupervisor.d.ts +4 -0
  45. package/dist/bridge/opencodeConfig.d.ts +41 -0
  46. package/dist/bridge/opencodeConfig.js +107 -0
  47. package/dist/bridge/opencodePlugin.d.ts +13 -0
  48. package/dist/bridge/opencodePlugin.js +54 -0
  49. package/dist/bridge/opencodeTranscriptReader.d.ts +24 -0
  50. package/dist/bridge/opencodeTranscriptReader.js +178 -0
  51. package/dist/bridge/relayClient.d.ts +27 -1
  52. package/dist/bridge/relayClient.js +24 -7
  53. package/dist/bridge/screenshotAttach.d.ts +35 -0
  54. package/dist/bridge/screenshotAttach.js +173 -0
  55. package/dist/bridge/server.d.ts +9 -0
  56. package/dist/bridge/server.js +19 -1
  57. package/dist/bridge/sessionManager.d.ts +5 -0
  58. package/dist/bridge/sessionManager.js +21 -1
  59. package/dist/commands/bridge.js +36 -9
  60. package/hooks/opencode-plugin.mjs +186 -0
  61. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * oc-18 — Claude Code crew bundle -> OpenCode files.
5
+ *
6
+ * Formats verified against opencode-ai@1.18.35 (`opencode agent list`,
7
+ * `opencode debug agent <n>`, `opencode debug skill`): agents load from
8
+ * `.opencode/agent/<name>.md` (NOT from `.claude/agents`), `permission` is the
9
+ * tool gate, `.claude/skills/**` loads natively, `AGENTS.md` is the rules file.
10
+ */
11
+ const crewTranslator_1 = require("../bridge/crewTranslator");
12
+ const MARK = '## Prompt Defense Baseline';
13
+ const FENCE = '<!-- HIVEOP:CONTEXT:START -->\nfiles\n<!-- HIVEOP:CONTEXT:END -->';
14
+ const FIXTURE = {
15
+ 'CLAUDE.md': `# Crew\n\n${FENCE}\n`,
16
+ '.claude/agents/reviewer.md': `---\nname: reviewer\ndescription: "Reviews diffs: strict"\ntools: Read, Grep, Glob\nmodel: sonnet\ncolor: blue\n---\n\nYou review.\n\n${MARK}\nBe safe.\n`,
17
+ '.claude/agents/builder.md': `---\nname: builder\ndescription: Builds things\n---\nYou build.\n${MARK}\n`,
18
+ '.claude/skills/run-crew/SKILL.md': '---\nname: run-crew\ndescription: x\n---\nrun\n',
19
+ '.claude/handoff/PROTOCOL.md': 'blackboard',
20
+ };
21
+ describe('mapClaudeToolsToPermission', () => {
22
+ it('absent or empty tools inherit everything (no permission block)', () => {
23
+ expect((0, crewTranslator_1.mapClaudeToolsToPermission)(undefined)).toBeUndefined();
24
+ expect((0, crewTranslator_1.mapClaudeToolsToPermission)('')).toBeUndefined();
25
+ });
26
+ it('is an allowlist: unlisted known tools are denied', () => {
27
+ expect((0, crewTranslator_1.mapClaudeToolsToPermission)('Read, Grep, Glob')).toEqual({
28
+ edit: 'deny', bash: 'deny', webfetch: 'deny', websearch: 'deny', task: 'deny', todowrite: 'deny',
29
+ });
30
+ });
31
+ it('folds Write/Edit/MultiEdit/NotebookEdit into edit and keeps bash when listed', () => {
32
+ expect((0, crewTranslator_1.mapClaudeToolsToPermission)('Write, Bash, WebFetch')).toEqual({
33
+ websearch: 'deny', task: 'deny', todowrite: 'deny',
34
+ });
35
+ expect((0, crewTranslator_1.mapClaudeToolsToPermission)('MultiEdit')).toMatchObject({ bash: 'deny' });
36
+ expect((0, crewTranslator_1.mapClaudeToolsToPermission)('MultiEdit')).not.toHaveProperty('edit');
37
+ });
38
+ it('accepts an array and ignores mcp__ entries and unknown names', () => {
39
+ expect((0, crewTranslator_1.mapClaudeToolsToPermission)(['Read', 'mcp__x__y', 'Bogus'])).toMatchObject({ edit: 'deny', bash: 'deny' });
40
+ });
41
+ });
42
+ describe('translateClaudeAgentToOpencode', () => {
43
+ it('emits mode subagent, quoted description, permission, body; drops model/name/color/tools', () => {
44
+ const out = (0, crewTranslator_1.translateClaudeAgentToOpencode)(FIXTURE['.claude/agents/reviewer.md']);
45
+ const { data, body } = (0, crewTranslator_1.parseFrontmatter)(out);
46
+ expect(data.mode).toBe('subagent');
47
+ expect(data.description).toBe('Reviews diffs: strict');
48
+ expect(data).not.toHaveProperty('model');
49
+ expect(data).not.toHaveProperty('tools');
50
+ expect(data).not.toHaveProperty('color');
51
+ expect(data).not.toHaveProperty('name');
52
+ expect(out).toContain('permission:\n edit: deny');
53
+ expect(body).toContain(MARK);
54
+ });
55
+ it('preserves CRLF', () => {
56
+ const crlf = '---\r\ndescription: d\r\ntools: Read\r\n---\r\nbody\r\n';
57
+ const out = (0, crewTranslator_1.translateClaudeAgentToOpencode)(crlf);
58
+ expect(out).toContain('\r\n');
59
+ expect(out.replace(/\r\n/g, '')).not.toContain('\n');
60
+ });
61
+ it('a file with no frontmatter still becomes a valid subagent', () => {
62
+ const out = (0, crewTranslator_1.translateClaudeAgentToOpencode)('just a prompt');
63
+ expect((0, crewTranslator_1.parseFrontmatter)(out).data.mode).toBe('subagent');
64
+ expect(out).toContain('just a prompt');
65
+ });
66
+ });
67
+ describe('translateCrewBundleForOpencode (golden)', () => {
68
+ const out = (0, crewTranslator_1.translateCrewBundleForOpencode)(FIXTURE);
69
+ it('maps paths', () => {
70
+ expect(Object.keys(out).sort()).toEqual([
71
+ '.claude/handoff/PROTOCOL.md',
72
+ '.claude/skills/run-crew/SKILL.md',
73
+ '.opencode/agent/builder.md',
74
+ '.opencode/agent/reviewer.md',
75
+ 'AGENTS.md',
76
+ ]);
77
+ });
78
+ it('reviewer golden', () => {
79
+ expect(out['.opencode/agent/reviewer.md']).toBe(`---\ndescription: "Reviews diffs: strict"\nmode: subagent\npermission:\n edit: deny\n bash: deny\n webfetch: deny\n websearch: deny\n task: deny\n todowrite: deny\n---\n\nYou review.\n\n${MARK}\nBe safe.\n`);
80
+ });
81
+ it('builder (no tools) has no permission block', () => {
82
+ expect(out['.opencode/agent/builder.md']).toBe(`---\ndescription: "Builds things"\nmode: subagent\n---\nYou build.\n${MARK}\n`);
83
+ });
84
+ it('skills and handoff pass through byte-identical', () => {
85
+ expect(out['.claude/skills/run-crew/SKILL.md']).toBe(FIXTURE['.claude/skills/run-crew/SKILL.md']);
86
+ expect(out['.claude/handoff/PROTOCOL.md']).toBe('blackboard');
87
+ });
88
+ it('CLAUDE.md fenced region round-trips into AGENTS.md with the same markers, byte for byte', () => {
89
+ expect(out['AGENTS.md']).toBe(FIXTURE['CLAUDE.md']);
90
+ expect(out['AGENTS.md']).toContain('<!-- HIVEOP:CONTEXT:START -->');
91
+ expect(out['AGENTS.md']).toContain('<!-- HIVEOP:CONTEXT:END -->');
92
+ });
93
+ it('is pure: input untouched, same output twice', () => {
94
+ const copy = JSON.stringify(FIXTURE);
95
+ expect((0, crewTranslator_1.translateCrewBundleForOpencode)(FIXTURE)).toEqual(out);
96
+ expect(JSON.stringify(FIXTURE)).toBe(copy);
97
+ });
98
+ it('never emits a model field (billing is locked to the agent model via the gateway whitelist)', () => {
99
+ for (const [p, c] of Object.entries(out))
100
+ if (p.startsWith('.opencode/'))
101
+ expect(c).not.toMatch(/^model:/m);
102
+ });
103
+ });
@@ -92,6 +92,30 @@ describe('relay client — install-bundle frame (pucc-int-04)', () => {
92
92
  handle.close();
93
93
  await closeRelay(relay.wss);
94
94
  });
95
+ it('oc-18: passes the frame engine through to the installer (and omits it when absent)', async () => {
96
+ const relay = await startFakeRelay();
97
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
98
+ const calls = [];
99
+ const handle = (0, relayClient_1.startRelayClient)({
100
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
101
+ relayToken: 'tok',
102
+ sessions,
103
+ installBundle: (bundle) => { calls.push(bundle); return { written: [], skipped: [] }; },
104
+ });
105
+ const ws = await relay.nextConnection();
106
+ const q = frameQueue(ws);
107
+ await q.next(); // auth
108
+ ws.send(JSON.stringify({ t: 'install-bundle', workspaceId: 'ws-7', files: {}, engine: 'opencode' }));
109
+ await q.next();
110
+ ws.send(JSON.stringify({ t: 'install-bundle', workspaceId: 'ws-7', files: {} }));
111
+ await q.next();
112
+ expect(calls).toEqual([
113
+ { workspaceId: 'ws-7', files: {}, engine: 'opencode' },
114
+ { workspaceId: 'ws-7', files: {} },
115
+ ]);
116
+ handle.close();
117
+ await closeRelay(relay.wss);
118
+ });
95
119
  it('errors (never crashes) when no installer is wired', async () => {
96
120
  const relay = await startFakeRelay();
97
121
  const sessions = new sessionManager_1.SessionManager(makeFactory());
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /**
7
+ * oc-20 — a local pane opened from the browser over the loopback bridge launches
8
+ * OpenCode when the `open` frame carries the server-delivered block, and stays
9
+ * Claude Code when it does not.
10
+ */
11
+ const ws_1 = __importDefault(require("ws"));
12
+ const server_1 = require("../bridge/server");
13
+ const sessionManager_1 = require("../bridge/sessionManager");
14
+ const ORIGIN = 'https://app.hiveop.io';
15
+ const TOKEN = 'tok';
16
+ const block = { engine: 'opencode', model: 'moonshotai/kimi-k2', gatewayUrl: 'https://api.example.test/api/v1/opencode/v1', gatewayToken: 'hgw_abc' };
17
+ describe('loopback open (browser -> bridge)', () => {
18
+ let handle;
19
+ let spawns;
20
+ beforeEach(async () => {
21
+ spawns = [];
22
+ const pty = { write() { }, resize() { }, kill() { }, onData() { }, onExit() { } };
23
+ handle = await (0, server_1.startBridgeServer)({
24
+ sessionToken: TOKEN, allowedOrigin: ORIGIN,
25
+ sessions: new sessionManager_1.SessionManager({ spawn: (o) => { spawns.push(o); return pty; } }),
26
+ resolveLocalAgentCwd: () => '/tmp/proj',
27
+ commandExists: () => true,
28
+ });
29
+ });
30
+ afterEach(async () => { await handle.close(); });
31
+ const open = async (extra) => {
32
+ const ws = new ws_1.default(`ws://127.0.0.1:${handle.port}`, { headers: { origin: ORIGIN } });
33
+ await new Promise((r) => ws.on('open', r));
34
+ ws.send(JSON.stringify({ v: 1, token: TOKEN }));
35
+ const opened = new Promise((r) => ws.on('message', (m) => { if (JSON.parse(String(m)).t === 'opened')
36
+ r(null); }));
37
+ ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, agentId: 'a1', workspaceId: 'w1', ...extra }));
38
+ await opened;
39
+ ws.close();
40
+ };
41
+ it('spawns opencode with the gateway env when the frame carries the block', async () => {
42
+ await open({ opencode: block });
43
+ expect(spawns[0].command).toBe('opencode');
44
+ expect(spawns[0].env).toMatchObject({ HIVEOP_GATEWAY_TOKEN: 'hgw_abc' });
45
+ });
46
+ it('spawns claude when there is no block (Claude Code panes unchanged)', async () => {
47
+ await open({});
48
+ expect(spawns[0].command).toBe('claude');
49
+ expect(spawns[0].env?.HIVEOP_GATEWAY_TOKEN).toBeUndefined();
50
+ });
51
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,179 @@
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
+ });
@@ -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 {};