@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.
- package/dist/__tests__/bridge-activity-tail.test.js +16 -0
- package/dist/__tests__/bridge-attach-paste-engine.test.d.ts +1 -0
- package/dist/__tests__/bridge-attach-paste-engine.test.js +108 -0
- package/dist/__tests__/bridge-bundle-installer-opencode.test.d.ts +1 -0
- package/dist/__tests__/bridge-bundle-installer-opencode.test.js +69 -0
- package/dist/__tests__/bridge-context-engine.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-engine.test.js +112 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.js +19 -0
- package/dist/__tests__/bridge-crew-translator.test.d.ts +1 -0
- package/dist/__tests__/bridge-crew-translator.test.js +103 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.js +24 -0
- package/dist/__tests__/bridge-loopback-opencode-open.test.d.ts +1 -0
- package/dist/__tests__/bridge-loopback-opencode-open.test.js +51 -0
- package/dist/__tests__/bridge-opencode-launch.test.d.ts +1 -0
- package/dist/__tests__/bridge-opencode-launch.test.js +179 -0
- package/dist/__tests__/bridge-relay-pucc14.test.js +28 -0
- package/dist/__tests__/bridge-server.test.js +32 -0
- package/dist/__tests__/opencode-config-parity.test.d.ts +1 -0
- package/dist/__tests__/opencode-config-parity.test.js +85 -0
- package/dist/__tests__/opencode-plugin.test.d.ts +1 -0
- package/dist/__tests__/opencode-plugin.test.js +197 -0
- package/dist/__tests__/opencode-title.test.d.ts +1 -0
- package/dist/__tests__/opencode-title.test.js +99 -0
- package/dist/__tests__/opencode-transcript-reader.test.d.ts +1 -0
- package/dist/__tests__/opencode-transcript-reader.test.js +156 -0
- package/dist/bridge/activityTail.d.ts +7 -0
- package/dist/bridge/activityTail.js +1 -0
- package/dist/bridge/agentLaunch.d.ts +28 -0
- package/dist/bridge/agentLaunch.js +49 -0
- package/dist/bridge/attachmentWriter.d.ts +16 -0
- package/dist/bridge/attachmentWriter.js +24 -1
- package/dist/bridge/bundleInstaller.d.ts +3 -0
- package/dist/bridge/bundleInstaller.js +22 -8
- package/dist/bridge/claudeTranscriptReader.d.ts +7 -1
- package/dist/bridge/claudeTranscriptReader.js +12 -2
- package/dist/bridge/contextFile.d.ts +20 -0
- package/dist/bridge/contextFile.js +30 -0
- package/dist/bridge/contextWriter.d.ts +10 -1
- package/dist/bridge/contextWriter.js +29 -9
- package/dist/bridge/crewTranslator.d.ts +41 -0
- package/dist/bridge/crewTranslator.js +129 -0
- package/dist/bridge/launchSupervisor.d.ts +4 -0
- package/dist/bridge/opencodeConfig.d.ts +41 -0
- package/dist/bridge/opencodeConfig.js +107 -0
- package/dist/bridge/opencodePlugin.d.ts +13 -0
- package/dist/bridge/opencodePlugin.js +54 -0
- package/dist/bridge/opencodeTranscriptReader.d.ts +24 -0
- package/dist/bridge/opencodeTranscriptReader.js +178 -0
- package/dist/bridge/relayClient.d.ts +27 -1
- package/dist/bridge/relayClient.js +24 -7
- package/dist/bridge/screenshotAttach.d.ts +2 -0
- package/dist/bridge/screenshotAttach.js +3 -1
- package/dist/bridge/server.d.ts +6 -0
- package/dist/bridge/server.js +9 -1
- package/dist/bridge/sessionManager.d.ts +5 -0
- package/dist/bridge/sessionManager.js +21 -1
- package/dist/commands/bridge.js +36 -9
- package/hooks/opencode-plugin.mjs +186 -0
- 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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|