@wenrwa/marketplace-cli 0.7.8 → 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 (59) 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-bundle-installer-opencode.test.d.ts +1 -0
  5. package/dist/__tests__/bridge-bundle-installer-opencode.test.js +69 -0
  6. package/dist/__tests__/bridge-context-engine.test.d.ts +1 -0
  7. package/dist/__tests__/bridge-context-engine.test.js +112 -0
  8. package/dist/__tests__/bridge-context-manifest-frame.test.js +19 -0
  9. package/dist/__tests__/bridge-crew-translator.test.d.ts +1 -0
  10. package/dist/__tests__/bridge-crew-translator.test.js +103 -0
  11. package/dist/__tests__/bridge-install-bundle-frame.test.js +24 -0
  12. package/dist/__tests__/bridge-loopback-opencode-open.test.d.ts +1 -0
  13. package/dist/__tests__/bridge-loopback-opencode-open.test.js +51 -0
  14. package/dist/__tests__/bridge-opencode-launch.test.d.ts +1 -0
  15. package/dist/__tests__/bridge-opencode-launch.test.js +179 -0
  16. package/dist/__tests__/bridge-relay-pucc14.test.js +28 -0
  17. package/dist/__tests__/bridge-server.test.js +32 -0
  18. package/dist/__tests__/opencode-config-parity.test.d.ts +1 -0
  19. package/dist/__tests__/opencode-config-parity.test.js +85 -0
  20. package/dist/__tests__/opencode-plugin.test.d.ts +1 -0
  21. package/dist/__tests__/opencode-plugin.test.js +197 -0
  22. package/dist/__tests__/opencode-title.test.d.ts +1 -0
  23. package/dist/__tests__/opencode-title.test.js +99 -0
  24. package/dist/__tests__/opencode-transcript-reader.test.d.ts +1 -0
  25. package/dist/__tests__/opencode-transcript-reader.test.js +156 -0
  26. package/dist/bridge/activityTail.d.ts +7 -0
  27. package/dist/bridge/activityTail.js +1 -0
  28. package/dist/bridge/agentLaunch.d.ts +28 -0
  29. package/dist/bridge/agentLaunch.js +49 -0
  30. package/dist/bridge/attachmentWriter.d.ts +16 -0
  31. package/dist/bridge/attachmentWriter.js +24 -1
  32. package/dist/bridge/bundleInstaller.d.ts +3 -0
  33. package/dist/bridge/bundleInstaller.js +22 -8
  34. package/dist/bridge/claudeTranscriptReader.d.ts +7 -1
  35. package/dist/bridge/claudeTranscriptReader.js +12 -2
  36. package/dist/bridge/contextFile.d.ts +20 -0
  37. package/dist/bridge/contextFile.js +30 -0
  38. package/dist/bridge/contextWriter.d.ts +10 -1
  39. package/dist/bridge/contextWriter.js +29 -9
  40. package/dist/bridge/crewTranslator.d.ts +41 -0
  41. package/dist/bridge/crewTranslator.js +129 -0
  42. package/dist/bridge/launchSupervisor.d.ts +4 -0
  43. package/dist/bridge/opencodeConfig.d.ts +41 -0
  44. package/dist/bridge/opencodeConfig.js +107 -0
  45. package/dist/bridge/opencodePlugin.d.ts +13 -0
  46. package/dist/bridge/opencodePlugin.js +54 -0
  47. package/dist/bridge/opencodeTranscriptReader.d.ts +24 -0
  48. package/dist/bridge/opencodeTranscriptReader.js +178 -0
  49. package/dist/bridge/relayClient.d.ts +27 -1
  50. package/dist/bridge/relayClient.js +24 -7
  51. package/dist/bridge/screenshotAttach.d.ts +2 -0
  52. package/dist/bridge/screenshotAttach.js +3 -1
  53. package/dist/bridge/server.d.ts +6 -0
  54. package/dist/bridge/server.js +9 -1
  55. package/dist/bridge/sessionManager.d.ts +5 -0
  56. package/dist/bridge/sessionManager.js +21 -1
  57. package/dist/commands/bridge.js +36 -9
  58. package/hooks/opencode-plugin.mjs +186 -0
  59. package/package.json +1 -1
@@ -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 {};
@@ -0,0 +1,197 @@
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-06 — the OpenCode activity plugin must write the SAME lines the Claude
38
+ * Code hooks write. Expected lines for tool/stop are produced by running the
39
+ * REAL hooks (so the contract cannot drift); the plugin (ESM) is driven in a
40
+ * child node process.
41
+ */
42
+ const fs = __importStar(require("fs"));
43
+ const os = __importStar(require("os"));
44
+ const path = __importStar(require("path"));
45
+ const child_process_1 = require("child_process");
46
+ const opencodePlugin_1 = require("../bridge/opencodePlugin");
47
+ const ROOT = path.join(__dirname, '..', '..');
48
+ const LOCAL_PLUGIN = path.join(ROOT, 'hooks', 'opencode-plugin.mjs');
49
+ const HOSTED_PLUGIN = path.join(ROOT, '..', 'infra', 'terminal-image', 'opencode', 'hiveop-plugin.mjs');
50
+ const HOOKS = path.join(ROOT, 'hooks');
51
+ function tmp() { return fs.mkdtempSync(path.join(os.tmpdir(), 'oc06-')); }
52
+ function runPlugin(calls, env, plugin = LOCAL_PLUGIN) {
53
+ const script = `
54
+ const { HiveOp } = await import(process.argv[1]);
55
+ const h = await HiveOp({});
56
+ for (const c of JSON.parse(process.argv[2])) {
57
+ if ('event' in c) await h.event({ event: c.event });
58
+ else await h[c.hook](c.input, c.output);
59
+ }`;
60
+ const base = { ...process.env };
61
+ delete base.HIVEOP_ACTIVITY_FILE;
62
+ delete base.WENRWA_SESSION_ID;
63
+ (0, child_process_1.execFileSync)(process.execPath, ['--input-type=module', '-e', script, plugin, JSON.stringify(calls)], {
64
+ env: { ...base, ...env },
65
+ });
66
+ }
67
+ function runClaudeHook(file, event, af, cwd) {
68
+ (0, child_process_1.execFileSync)(process.execPath, [path.join(HOOKS, file)], {
69
+ input: JSON.stringify(event), cwd,
70
+ env: { ...process.env, WENRWA_SESSION_ID: 's1', WENRWA_WORKSPACE_ID: 'w1', HIVEOP_ACTIVITY_FILE: af },
71
+ });
72
+ }
73
+ const lines = (f) => fs.existsSync(f) ? fs.readFileSync(f, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l)) : [];
74
+ const ENV = (af) => ({ WENRWA_SESSION_ID: 's1', WENRWA_WORKSPACE_ID: 'w1', HIVEOP_ACTIVITY_FILE: af });
75
+ describe('opencode plugin activity lines (oc-06)', () => {
76
+ let dir;
77
+ let af;
78
+ let ref;
79
+ beforeEach(() => { dir = tmp(); af = path.join(dir, 'a.jsonl'); ref = path.join(dir, 'ref.jsonl'); });
80
+ it('tool.execute.before for edit matches the Claude PostToolUse Edit line', () => {
81
+ runClaudeHook('post-tool-use.js', { tool_name: 'Edit', tool_input: { file_path: '/repo/src/foo.ts' } }, ref);
82
+ runPlugin([{ hook: 'tool.execute.before', input: { tool: 'edit', sessionID: 'x' }, output: { args: { filePath: '/repo/src/foo.ts' } } }], ENV(af));
83
+ expect(lines(af)).toEqual(lines(ref));
84
+ expect(lines(af)[0]).toMatchObject({ kind: 'tool', tool: 'Edit', thinking: 'Editing foo.ts' });
85
+ });
86
+ it('tool.execute.after emits the same working line (args from input)', () => {
87
+ runClaudeHook('post-tool-use.js', { tool_name: 'Read', tool_input: { file_path: '/repo/a.ts' } }, ref);
88
+ runPlugin([{ hook: 'tool.execute.after', input: { tool: 'read', args: { filePath: '/repo/a.ts' } }, output: {} }], ENV(af));
89
+ expect(lines(af)).toEqual(lines(ref));
90
+ });
91
+ it('bash with an existing path matches the Claude hook (bash path extraction parity)', () => {
92
+ const f = path.join(dir, 'real.txt');
93
+ fs.writeFileSync(f, 'x');
94
+ runClaudeHook('post-tool-use.js', { tool_name: 'Bash', tool_input: { command: `cat ${f}` } }, ref, dir);
95
+ runPlugin([{ hook: 'tool.execute.before', input: { tool: 'bash' }, output: { args: { command: `cat ${f}` } } }], ENV(af));
96
+ const got = lines(af);
97
+ expect(got).toEqual(lines(ref));
98
+ expect(got[0].filePaths).toEqual([f]);
99
+ });
100
+ it('unknown tool and missing args fall back to Working', () => {
101
+ runClaudeHook('post-tool-use.js', { tool_name: 'mcp_thing', tool_input: {} }, ref);
102
+ runPlugin([{ hook: 'tool.execute.before', input: { tool: 'mcp_thing' }, output: {} }], ENV(af));
103
+ expect(lines(af)).toEqual(lines(ref));
104
+ });
105
+ it('session.idle matches the Claude Stop line', () => {
106
+ runClaudeHook('stop.js', {}, ref);
107
+ runPlugin([{ event: { type: 'session.idle', properties: { sessionID: 'o' } } }], ENV(af));
108
+ expect(lines(af)).toEqual(lines(ref));
109
+ });
110
+ it('session.status idle is a stop; idle then session.idle writes ONE stop; busy writes nothing', () => {
111
+ runPlugin([
112
+ { event: { type: 'session.status', properties: { status: { type: 'busy' } } } },
113
+ { event: { type: 'session.status', properties: { status: { type: 'idle' } } } },
114
+ { event: { type: 'session.idle', properties: {} } },
115
+ ], ENV(af));
116
+ expect(lines(af).map((l) => l.kind)).toEqual(['stop']);
117
+ });
118
+ it('a stop after new work is written again', () => {
119
+ runPlugin([
120
+ { event: { type: 'session.idle', properties: {} } },
121
+ { hook: 'tool.execute.before', input: { tool: 'bash' }, output: { args: {} } },
122
+ { event: { type: 'session.idle', properties: {} } },
123
+ ], ENV(af));
124
+ expect(lines(af).map((l) => l.kind)).toEqual(['stop', 'tool', 'stop']);
125
+ });
126
+ it('permission.asked and question.asked are needs_you; permission.replied is working', () => {
127
+ runPlugin([
128
+ { event: { type: 'permission.asked', properties: {} } },
129
+ { event: { type: 'permission.replied', properties: {} } },
130
+ { event: { type: 'question.asked', properties: {} } },
131
+ ], ENV(af));
132
+ expect(lines(af)).toEqual([
133
+ { sessionId: 's1', workspaceId: 'w1', kind: 'needs_you', thinking: 'Waiting for you' },
134
+ { sessionId: 's1', workspaceId: 'w1', kind: 'tool', tool: '', filePaths: [], thinking: 'Working' },
135
+ { sessionId: 's1', workspaceId: 'w1', kind: 'needs_you', thinking: 'Waiting for you' },
136
+ ]);
137
+ });
138
+ it('is a no-op without HIVEOP_ACTIVITY_FILE, and without a session id (like the hooks)', () => {
139
+ runPlugin([{ event: { type: 'session.idle', properties: {} } }], { WENRWA_SESSION_ID: 's1' });
140
+ runPlugin([{ event: { type: 'session.idle', properties: {} } }], { HIVEOP_ACTIVITY_FILE: af });
141
+ expect(fs.existsSync(af)).toBe(false);
142
+ });
143
+ it('swallows write errors and malformed input', () => {
144
+ const blocker = path.join(dir, 'file');
145
+ fs.writeFileSync(blocker, '');
146
+ expect(() => runPlugin([
147
+ { event: { type: 'session.idle', properties: {} } },
148
+ { event: null },
149
+ { hook: 'tool.execute.before', input: null, output: null },
150
+ ], ENV(path.join(blocker, 'sub', 'a.jsonl')))).not.toThrow();
151
+ });
152
+ it('session.created capture is kept behind captureSessionId (primary only)', () => {
153
+ const sess = path.join(dir, 'sessions');
154
+ const env = { ...ENV(af), WENRWA_AGENT_ID: 'ag1', HIVEOP_OPENCODE_SESSIONS_DIR: sess };
155
+ runPlugin([
156
+ { event: { type: 'session.created', properties: { sessionID: 'ses_main', info: {} } } },
157
+ { event: { type: 'session.created', properties: { sessionID: 'ses_sub', info: { parentID: 'ses_main' } } } },
158
+ ], env);
159
+ expect(fs.readFileSync(path.join(sess, 'ag1'), 'utf8')).toBe('ses_main');
160
+ expect(fs.existsSync(af)).toBe(false);
161
+ });
162
+ it('the hosted and local copies are byte-identical', () => {
163
+ expect(fs.readFileSync(LOCAL_PLUGIN, 'utf8')).toBe(fs.readFileSync(HOSTED_PLUGIN, 'utf8'));
164
+ });
165
+ });
166
+ describe('opencode plugin local helper (oc-06, for oc-08)', () => {
167
+ it('resolves the bundled plugin that ships in the package', () => {
168
+ const p = (0, opencodePlugin_1.resolveOpenCodePluginPath)();
169
+ expect(p).toBe(LOCAL_PLUGIN);
170
+ expect(fs.existsSync(p)).toBe(true);
171
+ });
172
+ it('builds a plugin config fragment with an encoded file URL', () => {
173
+ expect((0, opencodePlugin_1.buildOpenCodePluginConfig)('/a b/opencode-plugin.mjs')).toEqual({ plugin: ['file:///a%20b/opencode-plugin.mjs'] });
174
+ });
175
+ });
176
+ // OpenCode loads EVERY export of a plugin module as a plugin and reads hooks off
177
+ // whatever it returns. A helper export (e.g. a string-returning function) yields
178
+ // no hooks object, and OpenCode's Plugin.trigger then throws on every prompt —
179
+ // found live in image v62 against a fake gateway ("evaluating 'N.config'").
180
+ describe('module shape OpenCode loads', () => {
181
+ for (const plugin of [LOCAL_PLUGIN, HOSTED_PLUGIN]) {
182
+ it(`${path.basename(plugin)}: every export is a plugin that resolves to a hooks object`, () => {
183
+ const script = `
184
+ const mod = await import(process.argv[1]);
185
+ const bad = [];
186
+ for (const [name, fn] of Object.entries(mod)) {
187
+ if (typeof fn !== 'function') { bad.push(name + ':not-a-function'); continue; }
188
+ const h = await fn({});
189
+ if (!h || typeof h !== 'object') bad.push(name + ':no-hooks');
190
+ }
191
+ process.stdout.write(JSON.stringify({ names: Object.keys(mod), bad }));`;
192
+ const out = JSON.parse((0, child_process_1.execFileSync)(process.execPath, ['--input-type=module', '-e', script, plugin]).toString());
193
+ expect(out.bad).toEqual([]);
194
+ expect(out.names).toContain('HiveOp');
195
+ });
196
+ }
197
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,99 @@
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-17 -- OpenCode sets its own OSC title ("OpenCode" at home / for an untitled
38
+ * session, "OC | <title>" otherwise; observed on 1.18.35). The app ingests the OSC
39
+ * title verbatim, so the launch turns OpenCode's title off
40
+ * (OPENCODE_DISABLE_TERMINAL_TITLE=1) and the HiveOp plugin writes the bare session
41
+ * title itself. The plugin is driven in a child node process; HIVEOP_TITLE_TTY stands
42
+ * in for /dev/tty.
43
+ */
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
47
+ const child_process_1 = require("child_process");
48
+ const opencodeConfig_1 = require("../bridge/opencodeConfig");
49
+ const ROOT = path.join(__dirname, '..', '..');
50
+ const PLUGINS = [
51
+ path.join(ROOT, 'hooks', 'opencode-plugin.mjs'),
52
+ path.join(ROOT, '..', 'infra', 'terminal-image', 'opencode', 'hiveop-plugin.mjs'),
53
+ ];
54
+ function titlesFor(plugin, infos) {
55
+ const tty = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'oc17-')), 'tty');
56
+ fs.writeFileSync(tty, '');
57
+ const script = `
58
+ const { HiveOp } = await import(process.argv[1]);
59
+ const h = await HiveOp({});
60
+ for (const info of JSON.parse(process.argv[2])) await h.event({ event: { type: 'session.updated', properties: { info } } });`;
61
+ (0, child_process_1.execFileSync)('node', ['--input-type=module', '-e', script, plugin, JSON.stringify(infos)], {
62
+ env: { PATH: process.env.PATH, HIVEOP_TITLE_TTY: tty },
63
+ });
64
+ return fs.readFileSync(tty, 'utf-8');
65
+ }
66
+ const osc = (t) => `\x1b]0;${t}\x07`;
67
+ describe.each(PLUGINS.map((p) => [path.basename(p), p]))('oc-17 pane title (%s)', (_n, plugin) => {
68
+ it('writes the bare session title as OSC 0, no "OC | " prefix', () => {
69
+ expect(titlesFor(plugin, [{ id: 'ses_1', title: 'Fix the login redirect' }])).toBe(osc('Fix the login redirect'));
70
+ });
71
+ it('ignores the placeholder titles OpenCode starts every session with', () => {
72
+ expect(titlesFor(plugin, [
73
+ { id: 'ses_1', title: 'New session - 2026-10-11T03:29:07.080Z' },
74
+ { id: 'ses_2', title: 'Child session - 2026-10-11T03:29:07.080Z' },
75
+ ])).toBe('');
76
+ });
77
+ it('ignores sub-agent sessions and events without a title', () => {
78
+ expect(titlesFor(plugin, [{ id: 'ses_2', parentID: 'ses_1', title: 'Explore repo' }, { id: 'ses_1' }, null])).toBe('');
79
+ });
80
+ it('writes a changed title once, not once per event', () => {
81
+ const out = titlesFor(plugin, [
82
+ { id: 'ses_1', title: 'First' }, { id: 'ses_1', title: 'First' }, { id: 'ses_1', title: 'Second' },
83
+ ]);
84
+ expect(out).toBe(osc('First') + osc('Second'));
85
+ });
86
+ it('strips control characters (no escape injection from a model-written title) and caps length', () => {
87
+ const out = titlesFor(plugin, [{ id: 'ses_1', title: 'ok\x1b]0;evil\x07 title' }, { id: 'ses_1', title: 'x'.repeat(200) }]);
88
+ expect(out).toBe(osc('ok ]0;evil title') + osc('x'.repeat(79) + '…'));
89
+ });
90
+ });
91
+ describe('oc-17 launch env', () => {
92
+ it("switches OpenCode's own terminal title off", () => {
93
+ expect(opencodeConfig_1.OPENCODE_QUIET_ENV.OPENCODE_DISABLE_TERMINAL_TITLE).toBe('1');
94
+ });
95
+ it('hosted autolaunch exports the same switch', () => {
96
+ const sh = fs.readFileSync(path.join(ROOT, '..', 'infra', 'terminal-image', 'autolaunch.sh'), 'utf-8');
97
+ expect(sh).toMatch(/export [^\n]*OPENCODE_DISABLE_TERMINAL_TITLE=1/);
98
+ });
99
+ });