@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.
Files changed (63) 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-binary.test.d.ts +1 -0
  15. package/dist/__tests__/bridge-opencode-binary.test.js +128 -0
  16. package/dist/__tests__/bridge-opencode-launch.test.d.ts +1 -0
  17. package/dist/__tests__/bridge-opencode-launch.test.js +216 -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/opencodeBinary.d.ts +47 -0
  46. package/dist/bridge/opencodeBinary.js +161 -0
  47. package/dist/bridge/opencodeConfig.d.ts +43 -0
  48. package/dist/bridge/opencodeConfig.js +109 -0
  49. package/dist/bridge/opencodePlugin.d.ts +13 -0
  50. package/dist/bridge/opencodePlugin.js +54 -0
  51. package/dist/bridge/opencodeTranscriptReader.d.ts +24 -0
  52. package/dist/bridge/opencodeTranscriptReader.js +178 -0
  53. package/dist/bridge/relayClient.d.ts +34 -1
  54. package/dist/bridge/relayClient.js +33 -8
  55. package/dist/bridge/screenshotAttach.d.ts +2 -0
  56. package/dist/bridge/screenshotAttach.js +3 -1
  57. package/dist/bridge/server.d.ts +15 -0
  58. package/dist/bridge/server.js +21 -3
  59. package/dist/bridge/sessionManager.d.ts +5 -0
  60. package/dist/bridge/sessionManager.js +21 -1
  61. package/dist/commands/bridge.js +76 -9
  62. package/hooks/opencode-plugin.mjs +186 -0
  63. package/package.json +1 -1
@@ -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
+ });
@@ -0,0 +1,156 @@
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 transcript reads. The fixture DB uses the table shapes captured from
38
+ * opencode-ai@1.18.35 (session / message / part, JSON in `data`) and is built with the
39
+ * sqlite3 CLI; the suite skips (loudly) where that CLI is missing.
40
+ */
41
+ const fs = __importStar(require("fs"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
44
+ const child_process_1 = require("child_process");
45
+ const claudeTranscriptReader_1 = require("../bridge/claudeTranscriptReader");
46
+ const opencodeTranscriptReader_1 = require("../bridge/opencodeTranscriptReader");
47
+ const HAVE_SQLITE = (0, child_process_1.spawnSync)('sqlite3', ['-version']).status === 0;
48
+ const AGENT = '22222222-2222-2222-2222-222222222222';
49
+ const OLD = 'ses_old111';
50
+ const NEW = 'ses_new222';
51
+ const SUB = 'ses_sub333';
52
+ const j = (o) => JSON.stringify(o).replace(/'/g, "''");
53
+ function makeRoot() {
54
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'oc17-db-'));
55
+ const db = (0, opencodeTranscriptReader_1.opencodeDbPath)(AGENT, { dataRoot: root });
56
+ fs.mkdirSync(path.dirname(db), { recursive: true });
57
+ const sql = `
58
+ CREATE TABLE session (id text PRIMARY KEY, parent_id text, title text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL);
59
+ CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL);
60
+ CREATE TABLE part (id text PRIMARY KEY, message_id text NOT NULL, session_id text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL);
61
+ INSERT INTO session VALUES ('${OLD}', NULL, 'old', 1, 10), ('${NEW}', NULL, 'new', 2, 50), ('${SUB}', '${NEW}', 'sub', 3, 90);
62
+ INSERT INTO message VALUES ('msg_a', '${OLD}', 5, 5, '${j({ role: 'user' })}');
63
+ INSERT INTO part VALUES ('prt_a', 'msg_a', '${OLD}', 5, 5, '${j({ type: 'text', text: 'old question' })}');
64
+ INSERT INTO message VALUES ('msg_1', '${NEW}', 1791671762759, 1, '${j({ role: 'user' })}');
65
+ INSERT INTO part VALUES ('prt_1', 'msg_1', '${NEW}', 1, 1, '${j({ type: 'text', text: "it's RUNTOOL please" })}');
66
+ INSERT INTO message VALUES ('msg_2', '${NEW}', 1791671762800, 1, '${j({ role: 'assistant' })}');
67
+ INSERT INTO part VALUES ('prt_2a', 'msg_2', '${NEW}', 1, 1, '${j({ type: 'step-start' })}');
68
+ INSERT INTO part VALUES ('prt_2b', 'msg_2', '${NEW}', 1, 1, '${j({ type: 'reasoning', text: 'hidden thoughts' })}');
69
+ INSERT INTO part VALUES ('prt_2c', 'msg_2', '${NEW}', 1, 1, '${j({ type: 'tool', tool: 'bash', callID: 'call_1', state: { status: 'completed', input: { command: 'echo hi' }, output: 'hi\n' } })}');
70
+ INSERT INTO part VALUES ('prt_2d', 'msg_2', '${NEW}', 1, 1, '${j({ type: 'tool', tool: 'read', callID: 'call_2', state: { status: 'error', input: { filePath: '/x' }, error: 'ENOENT' } })}');
71
+ INSERT INTO message VALUES ('msg_3', '${NEW}', 1791671762900, 1, '${j({ role: 'assistant' })}');
72
+ INSERT INTO part VALUES ('prt_3', 'msg_3', '${NEW}', 1, 1, '${j({ type: 'text', text: 'Done.' })}');
73
+ INSERT INTO message VALUES ('msg_s', '${SUB}', 9, 9, '${j({ role: 'user' })}');
74
+ INSERT INTO part VALUES ('prt_s', 'msg_s', '${SUB}', 9, 9, '${j({ type: 'text', text: 'subagent chatter' })}');`;
75
+ (0, child_process_1.execFileSync)('sqlite3', [db, sql]);
76
+ return root;
77
+ }
78
+ const lines = (s) => s.trim().split('\n').map((l) => JSON.parse(l));
79
+ const d = HAVE_SQLITE ? describe : describe.skip;
80
+ d('readOpencodeTranscript (oc-17)', () => {
81
+ let root;
82
+ beforeAll(() => { root = makeRoot(); });
83
+ it('reads the newest root session as Claude-shaped jsonl (text, tool_use, tool_result; no reasoning, no sub-agents)', () => {
84
+ const r = (0, opencodeTranscriptReader_1.readOpencodeTranscript)(AGENT, undefined, { dataRoot: root });
85
+ expect(r.ok).toBe(true);
86
+ const out = lines(r.content);
87
+ expect(out.map((l) => l.type)).toEqual(['user', 'assistant', 'user', 'assistant']);
88
+ expect(out[0].message).toEqual({ role: 'user', content: [{ type: 'text', text: "it's RUNTOOL please" }] });
89
+ expect(out[0].sessionId).toBe(NEW);
90
+ expect(out[0].timestamp).toBe(new Date(1791671762759).toISOString());
91
+ expect(out[1].message.content).toEqual([
92
+ { type: 'tool_use', id: 'call_1', name: 'bash', input: { command: 'echo hi' } },
93
+ { type: 'tool_use', id: 'call_2', name: 'read', input: { filePath: '/x' } },
94
+ ]);
95
+ expect(out[2].message.content).toEqual([
96
+ { type: 'tool_result', tool_use_id: 'call_1', content: 'hi\n' },
97
+ { type: 'tool_result', tool_use_id: 'call_2', is_error: true, content: 'ENOENT' },
98
+ ]);
99
+ expect(out[3].message.content).toEqual([{ type: 'text', text: 'Done.' }]);
100
+ expect(r.content).not.toMatch(/hidden thoughts|subagent chatter|old question/);
101
+ });
102
+ it('reads a pinned session id', () => {
103
+ const r = (0, opencodeTranscriptReader_1.readOpencodeTranscript)(AGENT, OLD, { dataRoot: root });
104
+ expect(lines(r.content)[0].message.content[0].text).toBe('old question');
105
+ });
106
+ it('refuses a session id that is not ses_<alnum> (no SQL injection) and an unknown one', () => {
107
+ expect((0, opencodeTranscriptReader_1.readOpencodeTranscript)(AGENT, "x' OR '1'='1", { dataRoot: root })).toEqual({ ok: false, reason: 'not-found' });
108
+ expect((0, opencodeTranscriptReader_1.readOpencodeTranscript)(AGENT, 'ses_nope', { dataRoot: root })).toEqual({ ok: false, reason: 'not-found' });
109
+ });
110
+ it('is not-found when the agent has no database', () => {
111
+ expect((0, opencodeTranscriptReader_1.readOpencodeTranscript)('33333333-3333-3333-3333-333333333333', undefined, { dataRoot: root }))
112
+ .toEqual({ ok: false, reason: 'not-found' });
113
+ });
114
+ it('never writes: the database file is byte-identical after a read', () => {
115
+ const db = (0, opencodeTranscriptReader_1.opencodeDbPath)(AGENT, { dataRoot: root });
116
+ const before = fs.readFileSync(db);
117
+ (0, opencodeTranscriptReader_1.readOpencodeTranscript)(AGENT, undefined, { dataRoot: root });
118
+ expect(fs.readFileSync(db).equals(before)).toBe(true);
119
+ });
120
+ it('the default driver answers a plain SELECT', () => {
121
+ const db = (0, opencodeTranscriptReader_1.opencodeDbPath)(AGENT, { dataRoot: root });
122
+ expect((0, opencodeTranscriptReader_1.defaultSqliteQuery)(db, 'SELECT count(*) AS n FROM session')).toEqual([{ n: 3 }]);
123
+ });
124
+ });
125
+ describe('readAgentTranscript engine dispatch (oc-17)', () => {
126
+ const noClaude = { existsSync: (p) => p.endsWith('opencode.db'), env: { CLAUDE_CONFIG_DIR: undefined }, homedir: () => '/root' };
127
+ it('falls through to the OpenCode reader when there is no Claude jsonl but an OpenCode db', () => {
128
+ const query = jest.fn()
129
+ .mockReturnValueOnce([{ id: 'ses_abc' }])
130
+ .mockReturnValueOnce([{ id: 'm1', time_created: 1, data: '{"role":"user"}' }])
131
+ .mockReturnValueOnce([{ message_id: 'm1', data: '{"type":"text","text":"hi"}' }]);
132
+ const r = (0, claudeTranscriptReader_1.readAgentTranscript)('/data/repos', 'ws-1', AGENT, { ...noClaude, opencode: { dataRoot: '/d', query } });
133
+ expect(r.ok && JSON.parse(r.content.trim()).message.content[0].text).toBe('hi');
134
+ });
135
+ it('engine: "opencode" skips the Claude file even when one exists', () => {
136
+ const query = jest.fn().mockReturnValue([]);
137
+ const r = (0, claudeTranscriptReader_1.readAgentTranscript)('/data/repos', 'ws-1', AGENT, {
138
+ existsSync: () => true, engine: 'opencode', opencode: { dataRoot: '/d', query },
139
+ });
140
+ expect(r).toEqual({ ok: false, reason: 'not-found' });
141
+ expect(query).toHaveBeenCalled();
142
+ });
143
+ it('engine: "claude_code" never probes the OpenCode db', () => {
144
+ const query = jest.fn();
145
+ const r = (0, claudeTranscriptReader_1.readAgentTranscript)('/data/repos', 'ws-1', AGENT, {
146
+ ...noClaude, engine: 'claude_code', opencode: { dataRoot: '/d', query },
147
+ });
148
+ expect(r).toEqual({ ok: false, reason: 'not-found' });
149
+ expect(query).not.toHaveBeenCalled();
150
+ });
151
+ it('answers transcript-unavailable when no sqlite driver exists', () => {
152
+ const query = () => { throw new Error('sqlite-unavailable'); };
153
+ const r = (0, claudeTranscriptReader_1.readAgentTranscript)('/data/repos', 'ws-1', AGENT, { ...noClaude, opencode: { dataRoot: '/d', query } });
154
+ expect(r).toEqual({ ok: false, reason: 'transcript-unavailable' });
155
+ });
156
+ });
@@ -11,6 +11,13 @@ export interface ActivityRecord {
11
11
  * a record written before this field existed, or any malformed value.
12
12
  */
13
13
  tool?: string;
14
+ /**
15
+ * oc-22 — ONLY ever 'needs_you' (the OpenCode plugin's permission/question
16
+ * prompt). Every other kind the hooks write ('tool', 'stop') is deliberately
17
+ * left off: the outward frame for Claude Code stays byte-identical, and the
18
+ * relay still tells a stop from a tool use by shape.
19
+ */
20
+ kind?: 'needs_you';
14
21
  }
15
22
  export interface ActivityTailOpts {
16
23
  /** Path to the JSONL file the local hooks append to. */
@@ -105,6 +105,7 @@ function startActivityTail(opts) {
105
105
  ...(Array.isArray(rec.filePaths) && rec.filePaths.length ? { filePaths: rec.filePaths } : {}),
106
106
  ...(typeof rec.thinking === 'string' ? { thinking: rec.thinking } : {}),
107
107
  ...(typeof rec.tool === 'string' ? { tool: rec.tool } : {}),
108
+ ...(rec.kind === 'needs_you' ? { kind: 'needs_you' } : {}),
108
109
  });
109
110
  }
110
111
  catch {
@@ -78,7 +78,35 @@ export interface SessionPin {
78
78
  * (claudeSessionHistory.ts's hasAgentSessionHistory). */
79
79
  exists: boolean;
80
80
  }
81
+ /**
82
+ * oc-08 — what the SERVER delivers with a session start for a local OpenCode
83
+ * agent (never the OpenRouter key; the token is the per-agent gateway token,
84
+ * ADR-0013). Absent / engine !== 'opencode' → the Claude path, byte-identical.
85
+ */
86
+ export interface OpencodeLaunchInput {
87
+ engine?: string;
88
+ model?: string | null;
89
+ /** Full OpenAI-compatible base URL of the metering gateway. */
90
+ gatewayUrl?: string | null;
91
+ gatewayToken?: string | null;
92
+ /** OpenCode's own captured session id (`ses_...`); absent on first launch. */
93
+ sessionId?: string | null;
94
+ }
95
+ /** The message shown when a launch command is not usable on this machine. */
96
+ export declare function missingCommandMessage(command: string): string;
81
97
  export declare function resolveAgentLaunch(opts: {
98
+ /** oc-08 — engine-aware launch; see OpencodeLaunchInput. */
99
+ opencode?: OpencodeLaunchInput;
100
+ /** Connect token (WENRWA_CONNECT_TOKEN) — adds the MCP server for opencode. */
101
+ connectToken?: string;
102
+ /** oc-07 — the installed MCP server (mcpInstall.ts), the same absolute
103
+ * command mcpRegister gives Claude. Absent → npx fallback. */
104
+ mcpServer?: {
105
+ command: string;
106
+ args: string[];
107
+ };
108
+ /** oc-07 — MARKETPLACE_API_URL for the MCP server (as mcpRegister passes it). */
109
+ apiUrl?: string;
82
110
  autonomous?: boolean;
83
111
  activity?: AgentActivityWiring;
84
112
  sessionPin?: SessionPin;
@@ -1,15 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HIVEOP_CONTEXT_ENV_VALUE = exports.HIVEOP_CONTEXT_ENV_VAR = void 0;
4
+ exports.missingCommandMessage = missingCommandMessage;
4
5
  exports.resolveAgentLaunch = resolveAgentLaunch;
5
6
  const localContextWriter_1 = require("./localContextWriter");
7
+ const opencodeConfig_1 = require("./opencodeConfig");
6
8
  /** lws-08 — env var name a spawned `claude` (or any tooling inspecting its
7
9
  * own environment) can read to find the pushed context manifest. */
8
10
  exports.HIVEOP_CONTEXT_ENV_VAR = 'HIVEOP_CONTEXT_FILE';
9
11
  /** lws-08 — re-exported from localContextWriter.ts (the single source of
10
12
  * truth for where the manifest actually lands) so this can never drift. */
11
13
  exports.HIVEOP_CONTEXT_ENV_VALUE = localContextWriter_1.LOCAL_CONTEXT_RELATIVE_PATH;
14
+ /** The message shown when a launch command is not usable on this machine. */
15
+ function missingCommandMessage(command) {
16
+ return command === 'opencode'
17
+ ? `opencode not found on PATH (or older than the minimum) — ${opencodeConfig_1.OPENCODE_INSTALL_HINT}`
18
+ : `${command} not found on PATH — install Claude Code CLI on this machine`;
19
+ }
12
20
  function resolveAgentLaunch(opts) {
21
+ const oc = opts.opencode;
22
+ if (oc && oc.engine === 'opencode' && oc.model && oc.gatewayUrl && oc.gatewayToken) {
23
+ return resolveOpencodeLaunch(opts, { model: oc.model, gatewayUrl: oc.gatewayUrl, gatewayToken: oc.gatewayToken, sessionId: oc.sessionId });
24
+ }
13
25
  const args = opts.autonomous ? ['--dangerously-skip-permissions'] : [];
14
26
  if (opts.activity)
15
27
  args.push('--settings', opts.activity.settingsPath);
@@ -27,3 +39,40 @@ function resolveAgentLaunch(opts) {
27
39
  env.WENRWA_WORKSPACE_ID = opts.workspaceId;
28
40
  return { command: 'claude', args, env };
29
41
  }
42
+ /** oc-08 — the OpenCode branch. Resume is `-s <captured id>` only; the id is
43
+ * captured from the first run, never chosen (oc-04 s3). No Claude flags, no
44
+ * --settings hook file: activity for OpenCode rides its plugin (oc-06). */
45
+ function resolveOpencodeLaunch(opts, oc) {
46
+ const args = [];
47
+ if ((0, opencodeConfig_1.isCapturedOpencodeSessionId)(oc.sessionId))
48
+ args.push('-s', oc.sessionId);
49
+ const config = (0, opencodeConfig_1.buildOpencodeConfig)({
50
+ model: oc.model,
51
+ gatewayBaseUrl: oc.gatewayUrl,
52
+ autonomous: !!opts.autonomous,
53
+ withMcp: !!opts.connectToken,
54
+ // oc-23: Claude loads .hiveop/context.md via its SessionStart hook; OpenCode has none, so it reads it as instructions.
55
+ instructions: [exports.HIVEOP_CONTEXT_ENV_VALUE],
56
+ ...(opts.mcpServer ? { mcpCommand: [opts.mcpServer.command, ...opts.mcpServer.args] } : {}),
57
+ ...(opts.apiUrl ? { mcpEnvPassthrough: ['MARKETPLACE_API_URL'] } : {}),
58
+ });
59
+ const env = {
60
+ [exports.HIVEOP_CONTEXT_ENV_VAR]: exports.HIVEOP_CONTEXT_ENV_VALUE,
61
+ ...opencodeConfig_1.OPENCODE_QUIET_ENV,
62
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
63
+ [opencodeConfig_1.OPENCODE_GATEWAY_TOKEN_ENV]: oc.gatewayToken,
64
+ };
65
+ if (opts.connectToken)
66
+ env.WENRWA_CONNECT_TOKEN = opts.connectToken;
67
+ if (opts.connectToken && opts.apiUrl)
68
+ env.MARKETPLACE_API_URL = opts.apiUrl;
69
+ if (opts.activity) {
70
+ env.WENRWA_SESSION_ID = opts.activity.sessionId;
71
+ env.HIVEOP_ACTIVITY_FILE = opts.activity.activityFile;
72
+ }
73
+ if (opts.sessionPin)
74
+ env.WENRWA_AGENT_ID = opts.sessionPin.agentId;
75
+ if (opts.workspaceId)
76
+ env.WENRWA_WORKSPACE_ID = opts.workspaceId;
77
+ return { command: 'opencode', args, env };
78
+ }
@@ -39,6 +39,20 @@ export declare function attachmentRootFor(sessions: {
39
39
  list(): string[];
40
40
  cwdOf(id: string): string | undefined;
41
41
  }, sessionId: unknown): string | null;
42
+ /**
43
+ * oc-16 — what to type into the PTY after the file is written, per engine.
44
+ * Claude Code: nothing (the pane types the path itself; bytes unchanged).
45
+ * OpenCode: the path as a BRACKETED PASTE with no Enter — its TUI detects an
46
+ * image file path on the paste event (a bare typed path is not guaranteed to
47
+ * attach) and the user keeps typing. Spaces are backslash-escaped.
48
+ */
49
+ export declare function pastePayloadFor(engine: string | undefined, filePath: string): string | null;
50
+ /** Paste the path into an OpenCode session's prompt; true when it was written
51
+ * (`pasted` in the reply tells the pane not to type the path itself). */
52
+ export declare function pasteIfOpencode(sessions: {
53
+ engineOf?(id: string): string | undefined;
54
+ writeTo?(id: string, data: string): boolean;
55
+ }, sessionId: string, filePath: string): boolean;
42
56
  /**
43
57
  * The whole `attach-file` frame → `file-attached` reply, shared verbatim by
44
58
  * the local server and the relay client so the two lanes cannot drift.
@@ -51,4 +65,6 @@ export declare function handleAttachFrame(frame: {
51
65
  }, sessions: {
52
66
  list(): string[];
53
67
  cwdOf(id: string): string | undefined;
68
+ engineOf?(id: string): string | undefined;
69
+ writeTo?(id: string, data: string): boolean;
54
70
  }, deps?: AttachmentDeps): Record<string, unknown>;