@wenrwa/marketplace-cli 0.7.8 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/bridge-activity-tail.test.js +16 -0
- package/dist/__tests__/bridge-attach-paste-engine.test.d.ts +1 -0
- package/dist/__tests__/bridge-attach-paste-engine.test.js +108 -0
- package/dist/__tests__/bridge-bundle-installer-opencode.test.d.ts +1 -0
- package/dist/__tests__/bridge-bundle-installer-opencode.test.js +69 -0
- package/dist/__tests__/bridge-context-engine.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-engine.test.js +112 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.js +19 -0
- package/dist/__tests__/bridge-crew-translator.test.d.ts +1 -0
- package/dist/__tests__/bridge-crew-translator.test.js +103 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.js +24 -0
- package/dist/__tests__/bridge-loopback-opencode-open.test.d.ts +1 -0
- package/dist/__tests__/bridge-loopback-opencode-open.test.js +51 -0
- package/dist/__tests__/bridge-opencode-binary.test.d.ts +1 -0
- package/dist/__tests__/bridge-opencode-binary.test.js +128 -0
- package/dist/__tests__/bridge-opencode-launch.test.d.ts +1 -0
- package/dist/__tests__/bridge-opencode-launch.test.js +216 -0
- package/dist/__tests__/bridge-relay-pucc14.test.js +28 -0
- package/dist/__tests__/bridge-server.test.js +32 -0
- package/dist/__tests__/opencode-config-parity.test.d.ts +1 -0
- package/dist/__tests__/opencode-config-parity.test.js +85 -0
- package/dist/__tests__/opencode-plugin.test.d.ts +1 -0
- package/dist/__tests__/opencode-plugin.test.js +197 -0
- package/dist/__tests__/opencode-title.test.d.ts +1 -0
- package/dist/__tests__/opencode-title.test.js +99 -0
- package/dist/__tests__/opencode-transcript-reader.test.d.ts +1 -0
- package/dist/__tests__/opencode-transcript-reader.test.js +156 -0
- package/dist/bridge/activityTail.d.ts +7 -0
- package/dist/bridge/activityTail.js +1 -0
- package/dist/bridge/agentLaunch.d.ts +28 -0
- package/dist/bridge/agentLaunch.js +49 -0
- package/dist/bridge/attachmentWriter.d.ts +16 -0
- package/dist/bridge/attachmentWriter.js +24 -1
- package/dist/bridge/bundleInstaller.d.ts +3 -0
- package/dist/bridge/bundleInstaller.js +22 -8
- package/dist/bridge/claudeTranscriptReader.d.ts +7 -1
- package/dist/bridge/claudeTranscriptReader.js +12 -2
- package/dist/bridge/contextFile.d.ts +20 -0
- package/dist/bridge/contextFile.js +30 -0
- package/dist/bridge/contextWriter.d.ts +10 -1
- package/dist/bridge/contextWriter.js +29 -9
- package/dist/bridge/crewTranslator.d.ts +41 -0
- package/dist/bridge/crewTranslator.js +129 -0
- package/dist/bridge/launchSupervisor.d.ts +4 -0
- package/dist/bridge/opencodeBinary.d.ts +47 -0
- package/dist/bridge/opencodeBinary.js +161 -0
- package/dist/bridge/opencodeConfig.d.ts +43 -0
- package/dist/bridge/opencodeConfig.js +109 -0
- package/dist/bridge/opencodePlugin.d.ts +13 -0
- package/dist/bridge/opencodePlugin.js +54 -0
- package/dist/bridge/opencodeTranscriptReader.d.ts +24 -0
- package/dist/bridge/opencodeTranscriptReader.js +178 -0
- package/dist/bridge/relayClient.d.ts +34 -1
- package/dist/bridge/relayClient.js +33 -8
- package/dist/bridge/screenshotAttach.d.ts +2 -0
- package/dist/bridge/screenshotAttach.js +3 -1
- package/dist/bridge/server.d.ts +15 -0
- package/dist/bridge/server.js +21 -3
- package/dist/bridge/sessionManager.d.ts +5 -0
- package/dist/bridge/sessionManager.js +21 -1
- package/dist/commands/bridge.js +76 -9
- package/hooks/opencode-plugin.mjs +186 -0
- package/package.json +1 -1
|
@@ -56,6 +56,7 @@ exports.startRelayClient = startRelayClient;
|
|
|
56
56
|
* that) — on re-pair the pane sends `attach` again and gets a fresh
|
|
57
57
|
* `replay`, exactly as it would against the local server.
|
|
58
58
|
*/
|
|
59
|
+
const agentLaunch_1 = require("./agentLaunch");
|
|
59
60
|
const resumeDetector_1 = require("./resumeDetector");
|
|
60
61
|
const fs = __importStar(require("fs"));
|
|
61
62
|
const path = __importStar(require("path"));
|
|
@@ -976,7 +977,7 @@ function startRelayClient(opts) {
|
|
|
976
977
|
case 'open': {
|
|
977
978
|
// Capture into locals so the async .then closures below keep the
|
|
978
979
|
// discriminated-union narrowing TS would otherwise drop.
|
|
979
|
-
const { cols, rows, workspaceId, repoUrl, autonomous, connectToken, gitCredential, gitIdentity } = frame;
|
|
980
|
+
const { cols, rows, workspaceId, repoUrl, autonomous, connectToken, gitCredential, gitIdentity, opencode } = frame;
|
|
980
981
|
// pucc-14 multiplex: the RELAY owns the session id (== agentId) and
|
|
981
982
|
// stamps it on the open + every later frame. Honor it so the PTY is
|
|
982
983
|
// registered under exactly that id and container→pane frames demux
|
|
@@ -1059,9 +1060,17 @@ function startRelayClient(opts) {
|
|
|
1059
1060
|
// no override, so realPty spawns $SHELL and autolaunch.sh turns it
|
|
1060
1061
|
// into claude. Present (laptop) → `claude` explicitly, since
|
|
1061
1062
|
// nothing on a user's machine does that for us.
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1063
|
+
let launch = opts.resolveLaunch?.({ workspaceId, sessionId: relaySessionId, autonomous, opencode, connectToken }) ?? null;
|
|
1064
|
+
let refusal = null;
|
|
1065
|
+
if (launch && launch.command === 'opencode' && opts.prepareOpencodeLaunch) {
|
|
1066
|
+
const prepared = opts.prepareOpencodeLaunch({ ...launch, env: launch.env ?? {} });
|
|
1067
|
+
if (prepared.ok)
|
|
1068
|
+
launch = prepared.launch;
|
|
1069
|
+
else
|
|
1070
|
+
refusal = prepared.message;
|
|
1071
|
+
}
|
|
1072
|
+
if (launch && (refusal || (opts.commandExists && !opts.commandExists(launch.command)))) {
|
|
1073
|
+
const message = refusal ?? (0, agentLaunch_1.missingCommandMessage)(launch.command);
|
|
1065
1074
|
console.error(`[bridge] refusing PTY spawn — ${message}`);
|
|
1066
1075
|
send(socket, {
|
|
1067
1076
|
t: 'error',
|
|
@@ -1116,7 +1125,11 @@ function startRelayClient(opts) {
|
|
|
1116
1125
|
delay or fail an `opened`.
|
|
1117
1126
|
*/
|
|
1118
1127
|
if (workspaceId && opts.refreshContextPointers) {
|
|
1119
|
-
void Promise.resolve(opts.refreshContextPointers({
|
|
1128
|
+
void Promise.resolve(opts.refreshContextPointers({
|
|
1129
|
+
workspaceId,
|
|
1130
|
+
// oc-23: the relay-injected env names the agent's engine; absent = Claude.
|
|
1131
|
+
...(injectedEnv?.WENRWA_AGENT_ENGINE ? { engine: injectedEnv.WENRWA_AGENT_ENGINE } : {}),
|
|
1132
|
+
})).catch((err) => {
|
|
1120
1133
|
console.error('[bridge] context pointer refresh failed (non-fatal):', err);
|
|
1121
1134
|
});
|
|
1122
1135
|
}
|
|
@@ -1294,7 +1307,12 @@ function startRelayClient(opts) {
|
|
|
1294
1307
|
});
|
|
1295
1308
|
break;
|
|
1296
1309
|
}
|
|
1297
|
-
const result = opts.installBundle({
|
|
1310
|
+
const result = opts.installBundle({
|
|
1311
|
+
workspaceId: frame.workspaceId,
|
|
1312
|
+
files: frame.files ?? {},
|
|
1313
|
+
// oc-18: the arena names the target agent's engine; absent = Claude.
|
|
1314
|
+
...(frame.engine ? { engine: frame.engine } : {}),
|
|
1315
|
+
});
|
|
1298
1316
|
send(socket, {
|
|
1299
1317
|
t: 'bundle-installed',
|
|
1300
1318
|
workspaceId: frame.workspaceId,
|
|
@@ -1401,7 +1419,11 @@ function startRelayClient(opts) {
|
|
|
1401
1419
|
// rejection can only come from a bug in the writer, never from the
|
|
1402
1420
|
// brief fetch (which resolves null on any failure, PRD D2), so it
|
|
1403
1421
|
// is answered rather than thrown into an unhandled rejection.
|
|
1404
|
-
Promise.resolve(opts.writeContext({
|
|
1422
|
+
Promise.resolve(opts.writeContext({
|
|
1423
|
+
workspaceId: frame.workspaceId,
|
|
1424
|
+
content: frame.content ?? '',
|
|
1425
|
+
...(frame.engine ? { engine: frame.engine } : {}),
|
|
1426
|
+
}))
|
|
1405
1427
|
.then((result) => {
|
|
1406
1428
|
send(socket, {
|
|
1407
1429
|
t: 'context-written',
|
|
@@ -1568,7 +1590,7 @@ function startRelayClient(opts) {
|
|
|
1568
1590
|
});
|
|
1569
1591
|
break;
|
|
1570
1592
|
}
|
|
1571
|
-
const result = opts.readTranscript({ workspaceId, agentId: frame.agentId });
|
|
1593
|
+
const result = opts.readTranscript({ workspaceId, agentId: frame.agentId, engine: frame.engine, opencodeSessionId: frame.opencodeSessionId });
|
|
1572
1594
|
send(socket, {
|
|
1573
1595
|
t: 'transcript-content',
|
|
1574
1596
|
requestId,
|
|
@@ -1975,6 +1997,9 @@ function startRelayClient(opts) {
|
|
|
1975
1997
|
sessionId: rec.sessionId,
|
|
1976
1998
|
...(Array.isArray(rec.filePaths) && rec.filePaths.length ? { filePaths: rec.filePaths } : {}),
|
|
1977
1999
|
...(typeof rec.thinking === 'string' ? { thinking: rec.thinking } : {}),
|
|
2000
|
+
// oc-22 — carry ONLY 'needs_you' (the OpenCode plugin's prompt signal);
|
|
2001
|
+
// Claude's 'tool'/'stop' kinds stay dropped so its frames are unchanged.
|
|
2002
|
+
...(rec.kind === 'needs_you' ? { kind: 'needs_you' } : {}),
|
|
1978
2003
|
});
|
|
1979
2004
|
}
|
|
1980
2005
|
catch { /* skip a malformed line, keep tailing */ }
|
|
@@ -30,4 +30,6 @@ export declare function handleScreenshotFrame(frame: {
|
|
|
30
30
|
}, sessions: {
|
|
31
31
|
list(): string[];
|
|
32
32
|
cwdOf(id: string): string | undefined;
|
|
33
|
+
engineOf?(id: string): string | undefined;
|
|
34
|
+
writeTo?(id: string, data: string): boolean;
|
|
33
35
|
}, deps?: ScreenshotDeps): Promise<Record<string, unknown>>;
|
|
@@ -150,7 +150,9 @@ async function handleScreenshotFrame(frame, sessions, deps = exports.realScreens
|
|
|
150
150
|
return fail('too-large');
|
|
151
151
|
if ((0, attachmentWriter_1.sniffImageExt)(data)) {
|
|
152
152
|
const r = (0, attachmentWriter_1.writeAttachment)({ root, name, dataBase64: data.toString('base64') }, deps.attachment);
|
|
153
|
-
return r.ok
|
|
153
|
+
return r.ok
|
|
154
|
+
? { ...head, ok: true, path: r.path, bytes: r.bytes, ...((0, attachmentWriter_1.pasteIfOpencode)(sessions, frame.sessionId, r.path) ? { pasted: true } : {}) }
|
|
155
|
+
: fail(r.reason);
|
|
154
156
|
}
|
|
155
157
|
// Present but not (yet) an image — maybe still being written.
|
|
156
158
|
last = 'not-image';
|
package/dist/bridge/server.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { SessionManager } from './sessionManager';
|
|
|
2
2
|
import { ValidatePathResult } from './localPathValidation';
|
|
3
3
|
import { PickFolderResult } from './folderPicker';
|
|
4
4
|
import { LocalAddCoreResult } from './localAdd';
|
|
5
|
+
import type { PreparedLaunch } from './opencodeBinary';
|
|
5
6
|
import { VerifyOriginResult } from './originVerification';
|
|
6
7
|
import { PublishResult } from './localPublish';
|
|
7
8
|
import { PublishPreflightResult } from './publishPreflight';
|
|
@@ -108,6 +109,14 @@ export interface BridgeServerOpts {
|
|
|
108
109
|
* crashes the open — see the 'open' handler below.
|
|
109
110
|
*/
|
|
110
111
|
commandExists?: (command: string) => boolean;
|
|
112
|
+
/** oc-25 — resolves/provisions OpenCode for an `opencode` launch: returns the
|
|
113
|
+
* launch to spawn (absolute binary, or a progress waiter while the bridge
|
|
114
|
+
* installs the pinned copy) or a refusal message. Absent → bare `opencode`. */
|
|
115
|
+
prepareOpencodeLaunch?: (launch: {
|
|
116
|
+
command: string;
|
|
117
|
+
args: string[];
|
|
118
|
+
env: Record<string, string>;
|
|
119
|
+
}) => PreparedLaunch;
|
|
111
120
|
/**
|
|
112
121
|
* psp-01 (production bug, 2026-09-14; supersedes bres-01's `hasSessionHistory`)
|
|
113
122
|
* — does Claude Code already have a conversation on disk for THIS AGENT at
|
|
@@ -229,6 +238,12 @@ export interface BridgeServerOpts {
|
|
|
229
238
|
* bridge.ts) — see the 'open' handler's agentId branch.
|
|
230
239
|
*/
|
|
231
240
|
hiveOpSettingsPath?: string;
|
|
241
|
+
/** oc-07 — resolved installed MCP server + API URL for local OpenCode agents (same as mcpRegister's). */
|
|
242
|
+
mcpServer?: {
|
|
243
|
+
command: string;
|
|
244
|
+
args: string[];
|
|
245
|
+
};
|
|
246
|
+
mcpApiUrl?: string;
|
|
232
247
|
/**
|
|
233
248
|
* rival-10 — every file touch of a LOCAL WORKSPACE AGENT (an `open` with
|
|
234
249
|
* agentId + workspaceId), handed on for the ranked rival feed. The hook
|
package/dist/bridge/server.js
CHANGED
|
@@ -561,6 +561,8 @@ function startBridgeServer(opts) {
|
|
|
561
561
|
sessionId: rec.sessionId,
|
|
562
562
|
...(rec.filePaths ? { filePaths: rec.filePaths } : {}),
|
|
563
563
|
...(rec.thinking !== undefined ? { thinking: rec.thinking } : {}),
|
|
564
|
+
// oc-22 — an OpenCode permission/question prompt; absent for everything else.
|
|
565
|
+
...(rec.kind ? { kind: rec.kind } : {}),
|
|
564
566
|
});
|
|
565
567
|
}
|
|
566
568
|
},
|
|
@@ -731,15 +733,27 @@ function startBridgeServer(opts) {
|
|
|
731
733
|
// folder still falls back to $HOME for its PTY cwd, but the
|
|
732
734
|
// workgraph MCP pin should still match whatever workspace the
|
|
733
735
|
// arena client actually asked to open.
|
|
734
|
-
|
|
736
|
+
let launch = (0, agentLaunch_1.resolveAgentLaunch)({
|
|
735
737
|
autonomous: !!frame.autonomous,
|
|
738
|
+
...(frame.opencode ? { opencode: frame.opencode } : {}),
|
|
739
|
+
...(frame.connectToken ? { connectToken: frame.connectToken } : {}),
|
|
740
|
+
...(opts.mcpServer ? { mcpServer: opts.mcpServer } : {}),
|
|
741
|
+
...(opts.mcpApiUrl ? { apiUrl: opts.mcpApiUrl } : {}),
|
|
736
742
|
...(cwd ? { sessionPin: { agentId, exists: agentSessionExists } } : {}),
|
|
737
743
|
...(opts.activityFile && opts.hiveOpSettingsPath
|
|
738
744
|
? { activity: { settingsPath: opts.hiveOpSettingsPath, sessionId: agentId, activityFile: opts.activityFile } }
|
|
739
745
|
: {}),
|
|
740
746
|
...(frame.workspaceId ? { workspaceId: frame.workspaceId } : {}),
|
|
741
747
|
});
|
|
742
|
-
|
|
748
|
+
let refusal = null;
|
|
749
|
+
if (launch.command === 'opencode' && opts.prepareOpencodeLaunch) {
|
|
750
|
+
const prepared = opts.prepareOpencodeLaunch(launch);
|
|
751
|
+
if (prepared.ok)
|
|
752
|
+
launch = prepared.launch;
|
|
753
|
+
else
|
|
754
|
+
refusal = prepared.message;
|
|
755
|
+
}
|
|
756
|
+
const claudeAvailable = !refusal && (opts.commandExists ? opts.commandExists(launch.command) : true);
|
|
743
757
|
if (!claudeAvailable) {
|
|
744
758
|
// Never crash on a missing binary: open a plain session (no
|
|
745
759
|
// command override — realPty's $SHELL default) so the pane
|
|
@@ -765,7 +779,11 @@ function startBridgeServer(opts) {
|
|
|
765
779
|
send(ws, {
|
|
766
780
|
t: 'data',
|
|
767
781
|
sessionId,
|
|
768
|
-
data:
|
|
782
|
+
data: refusal
|
|
783
|
+
? `\r\n\x1b[31m${refusal}\x1b[0m\r\n`
|
|
784
|
+
: launch.command === 'opencode'
|
|
785
|
+
? `\r\n\x1b[31m${(0, agentLaunch_1.missingCommandMessage)('opencode')}, then reopen this pane.\x1b[0m\r\n`
|
|
786
|
+
: '\r\n\x1b[31mclaude not found on PATH — install Claude Code CLI, then reopen this pane.\x1b[0m\r\n',
|
|
769
787
|
});
|
|
770
788
|
}).catch((err) => {
|
|
771
789
|
console.error('[bridge] session attach failed:', err);
|
|
@@ -141,6 +141,11 @@ export declare class SessionManager {
|
|
|
141
141
|
* session or one opened without a resolved folder (a bare shell). Lets the
|
|
142
142
|
* attach path re-announce the root to a reattaching pane — see Session.cwd. */
|
|
143
143
|
cwdOf(id: string): string | undefined;
|
|
144
|
+
/** oc-16 — the engine a live session was launched as (undefined: bare shell/unknown). */
|
|
145
|
+
engineOf(id: string): 'claude' | 'opencode' | undefined;
|
|
146
|
+
/** oc-16 — write straight to a live session's PTY, no pane attachment needed
|
|
147
|
+
* (the `attach-file` frame is handled outside any attach handle). */
|
|
148
|
+
writeTo(id: string, data: string): boolean;
|
|
144
149
|
/**
|
|
145
150
|
* `id` (pucc-14): honor a caller-supplied session id instead of minting a
|
|
146
151
|
* random one. In relay multiplex mode the RELAY owns the session id (it is the
|
|
@@ -114,6 +114,19 @@ class SessionManager {
|
|
|
114
114
|
cwdOf(id) {
|
|
115
115
|
return this.sessions.get(id)?.cwd;
|
|
116
116
|
}
|
|
117
|
+
/** oc-16 — the engine a live session was launched as (undefined: bare shell/unknown). */
|
|
118
|
+
engineOf(id) {
|
|
119
|
+
return this.sessions.get(id)?.engine;
|
|
120
|
+
}
|
|
121
|
+
/** oc-16 — write straight to a live session's PTY, no pane attachment needed
|
|
122
|
+
* (the `attach-file` frame is handled outside any attach handle). */
|
|
123
|
+
writeTo(id, data) {
|
|
124
|
+
const s = this.sessions.get(id);
|
|
125
|
+
if (!s || s.exited)
|
|
126
|
+
return false;
|
|
127
|
+
s.pty.write(data);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
117
130
|
/**
|
|
118
131
|
* `id` (pucc-14): honor a caller-supplied session id instead of minting a
|
|
119
132
|
* random one. In relay multiplex mode the RELAY owns the session id (it is the
|
|
@@ -140,7 +153,13 @@ class SessionManager {
|
|
|
140
153
|
const spawnOne = (size) => isClaude
|
|
141
154
|
? (0, launchSupervisor_1.superviseLaunch)(this.factory, { ...opts, ...size }, this.launchSupervisor)
|
|
142
155
|
: this.factory.spawn({ ...opts, ...size });
|
|
143
|
-
|
|
156
|
+
// oc-08 — an OpenCode launch is staggered like any agent launch, but is NOT
|
|
157
|
+
// supervised: launchSupervisor's retries are Claude Code wording
|
|
158
|
+
// ("Session ID ... already in use" / "No conversation found") and would
|
|
159
|
+
// never match OpenCode's output. OpenCode resumes by a captured id
|
|
160
|
+
// (`-s`), and a stale one surfaces as OpenCode's own error in the pane.
|
|
161
|
+
const isOpencode = opts.command === 'opencode';
|
|
162
|
+
const gated = this.launchGate && (isClaude || isOpencode || this.everyOpenIsAgentLaunch);
|
|
144
163
|
const pty = gated
|
|
145
164
|
? this.launchGate.launch(spawnOne, { cols: opts.cols, rows: opts.rows })
|
|
146
165
|
: spawnOne({ cols: opts.cols, rows: opts.rows });
|
|
@@ -154,6 +173,7 @@ class SessionManager {
|
|
|
154
173
|
attachEpoch: 0,
|
|
155
174
|
exited: false,
|
|
156
175
|
cwd: opts.cwd,
|
|
176
|
+
engine: isOpencode ? 'opencode' : isClaude ? 'claude' : undefined,
|
|
157
177
|
};
|
|
158
178
|
pty.onData((data) => {
|
|
159
179
|
// Single write path to the PTY itself is untouched (ADR-0028) — this is
|
package/dist/commands/bridge.js
CHANGED
|
@@ -70,10 +70,12 @@ const realPty_1 = require("../bridge/realPty");
|
|
|
70
70
|
const config_1 = require("../bridge/config");
|
|
71
71
|
const mcpRegister_1 = require("../bridge/mcpRegister");
|
|
72
72
|
const mcpInstall_1 = require("../bridge/mcpInstall");
|
|
73
|
+
const opencodeBinary_1 = require("../bridge/opencodeBinary");
|
|
73
74
|
const autostart_1 = require("../bridge/autostart");
|
|
74
75
|
const repoCheckout_1 = require("../bridge/repoCheckout");
|
|
75
76
|
const bundleInstaller_1 = require("../bridge/bundleInstaller");
|
|
76
77
|
const agentLaunch_1 = require("../bridge/agentLaunch");
|
|
78
|
+
const opencodeConfig_1 = require("../bridge/opencodeConfig");
|
|
77
79
|
const contextWriter_1 = require("../bridge/contextWriter");
|
|
78
80
|
const memoryBrief_1 = require("../bridge/memoryBrief");
|
|
79
81
|
const containerFileRelay_1 = require("../bridge/containerFileRelay");
|
|
@@ -170,6 +172,20 @@ function bridgeApiUrl() {
|
|
|
170
172
|
// RELAY_URL/RELAY_TOKEN env vars are the container-friendly fallback (the
|
|
171
173
|
// image sets these instead of passing CLI flags), and a URL with no token
|
|
172
174
|
// anywhere is a startup error rather than a confusing runtime failure.
|
|
175
|
+
/**
|
|
176
|
+
* oc-08 — OpenCode is usable only if it is on PATH AND at least the minimum
|
|
177
|
+
* version oc-04 verified (the local lane runs the user's own install, which
|
|
178
|
+
* can be anything). Missing and too-old get the same install hint.
|
|
179
|
+
*/
|
|
180
|
+
function opencodeUsable() {
|
|
181
|
+
try {
|
|
182
|
+
const out = (0, child_process_1.execFileSync)('opencode', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
|
|
183
|
+
return (0, opencodeConfig_1.opencodeVersionOk)(out);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
173
189
|
/**
|
|
174
190
|
* lws-06 / fcct-27 — is a binary on PATH? Best-effort and shared: the loopback
|
|
175
191
|
* lane degrades to a shell + a red in-pane line when `claude` is missing, the
|
|
@@ -177,7 +193,46 @@ function bridgeApiUrl() {
|
|
|
177
193
|
* pane). One implementation so the two lanes can never disagree about whether
|
|
178
194
|
* this machine can run Claude Code.
|
|
179
195
|
*/
|
|
196
|
+
/**
|
|
197
|
+
* oc-25 — ONE provisioner for the process, so the loopback lane and the crew
|
|
198
|
+
* relay lane (and every pane) share a single in-flight OpenCode install.
|
|
199
|
+
*/
|
|
200
|
+
const opencodeProvisioner = (0, opencodeBinary_1.createOpencodeProvisioner)(path.join(os.homedir(), '.wen-market', 'opencode'), {
|
|
201
|
+
exists: (p) => fs.existsSync(p),
|
|
202
|
+
writeFile: (p, c) => fs.writeFileSync(p, c),
|
|
203
|
+
mkdirp: (d) => fs.mkdirSync(d, { recursive: true }),
|
|
204
|
+
rm: (p) => fs.rmSync(p, { force: true }),
|
|
205
|
+
runNpm: (args, env) => new Promise((resolve) => {
|
|
206
|
+
(0, child_process_1.execFile)(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { env, timeout: opencodeBinary_1.OPENCODE_INSTALL_TIMEOUT_MS }, (err, _out, stderr) => {
|
|
207
|
+
resolve({ code: err ? (typeof err.code === 'number' ? err.code : 1) : 0, stderr: String(stderr ?? '') });
|
|
208
|
+
});
|
|
209
|
+
}),
|
|
210
|
+
pathOpencode: () => {
|
|
211
|
+
try {
|
|
212
|
+
const abs = (0, child_process_1.execFileSync)(process.platform === 'win32' ? 'where' : 'command', process.platform === 'win32' ? ['opencode'] : ['-v', 'opencode'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).split(/\r?\n/)[0].trim();
|
|
213
|
+
if (!abs)
|
|
214
|
+
return null;
|
|
215
|
+
return { path: abs, version: opencodeVersionOf(abs) };
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
versionOf: (bin) => opencodeVersionOf(bin),
|
|
222
|
+
env: process.env,
|
|
223
|
+
platform: process.platform,
|
|
224
|
+
});
|
|
225
|
+
function opencodeVersionOf(bin) {
|
|
226
|
+
try {
|
|
227
|
+
return (0, child_process_1.execFileSync)(bin, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 15000 });
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
180
233
|
function commandExistsOnPath(command) {
|
|
234
|
+
if (command === 'opencode')
|
|
235
|
+
return opencodeUsable();
|
|
181
236
|
try {
|
|
182
237
|
(0, child_process_1.execFileSync)(process.platform === 'win32' ? 'where' : 'command', process.platform === 'win32' ? [command] : ['-v', command], { stdio: 'ignore', shell: process.platform !== 'win32' });
|
|
183
238
|
return true;
|
|
@@ -722,8 +777,8 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
722
777
|
// workspace checkout under the same /data/repos/<workspaceId> root. Pure
|
|
723
778
|
// file writes (path-safe + idempotent) — Claude Code orchestrates the
|
|
724
779
|
// crew natively next run; the credits crewExecutionEngine is never called.
|
|
725
|
-
installBundle: ({ workspaceId, files }) => {
|
|
726
|
-
const result = (0, bundleInstaller_1.installBundle)({ workspaceId, files }, {
|
|
780
|
+
installBundle: ({ workspaceId, files, engine }) => {
|
|
781
|
+
const result = (0, bundleInstaller_1.installBundle)({ workspaceId, files, engine }, {
|
|
727
782
|
reposBaseDir,
|
|
728
783
|
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
729
784
|
writeFile: (p, content) => fs.writeFileSync(p, content),
|
|
@@ -755,7 +810,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
755
810
|
only sends one when the pilot has files selected, so most workspaces
|
|
756
811
|
never got the pointers at all.
|
|
757
812
|
*/
|
|
758
|
-
refreshContextPointers: async ({ workspaceId }) => {
|
|
813
|
+
refreshContextPointers: async ({ workspaceId, engine }) => {
|
|
759
814
|
const memoryAvailable = Boolean(process.env.WENRWA_CONNECT_TOKEN);
|
|
760
815
|
// 2026-10-02 — the same reply says whether this is a MATCH workspace.
|
|
761
816
|
const briefInfo = memoryAvailable ? await (0, memoryBrief_1.fetchWorkspaceBrief)(workspaceId) : null;
|
|
@@ -763,6 +818,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
763
818
|
const result = (0, contextWriter_1.writeContext)({
|
|
764
819
|
workspaceId,
|
|
765
820
|
pointersOnly: true,
|
|
821
|
+
engine,
|
|
766
822
|
workGraphAvailable: Boolean(process.env.WENRWA_CONNECT_TOKEN),
|
|
767
823
|
memoryAvailable,
|
|
768
824
|
memoryBrief,
|
|
@@ -778,7 +834,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
778
834
|
console.log(chalk_1.default.green(`✓ Context pointers ${result.action} → workspace ${workspaceId}`));
|
|
779
835
|
}
|
|
780
836
|
},
|
|
781
|
-
writeContext: async ({ workspaceId, content }) => {
|
|
837
|
+
writeContext: async ({ workspaceId, content, engine }) => {
|
|
782
838
|
// wmr-03 — the workspace's memory brief, fetched at manifest time so
|
|
783
839
|
// the agent has it in context from its next turn. Never blocks the
|
|
784
840
|
// write: fetchWorkspaceBrief resolves null on any failure (PRD D2), and
|
|
@@ -791,6 +847,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
791
847
|
const result = (0, contextWriter_1.writeContext)({
|
|
792
848
|
workspaceId,
|
|
793
849
|
content,
|
|
850
|
+
engine,
|
|
794
851
|
// wgcp-01 — same gate as checkStorageQuota above: WENRWA_CONNECT_TOKEN
|
|
795
852
|
// is only injected at container boot when this workspace actually has
|
|
796
853
|
// MCP world-wiring (hosted-container.service.ts's machineEnv). Absent
|
|
@@ -809,7 +866,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
809
866
|
writeFile: (p, c) => fs.writeFileSync(p, c),
|
|
810
867
|
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
811
868
|
});
|
|
812
|
-
console.log(chalk_1.default.green(`✓ Context manifest ${result.action} → workspace ${workspaceId} (CLAUDE.md)`));
|
|
869
|
+
console.log(chalk_1.default.green(`✓ Context manifest ${result.action} → workspace ${workspaceId} (${path.posix.basename(result.claudeMdPath ?? 'CLAUDE.md')})`));
|
|
813
870
|
return { action: result.action, claudeMdPath: result.claudeMdPath };
|
|
814
871
|
},
|
|
815
872
|
// cve-06 (ADR-0047 / ADR-0032): read ONE file out of
|
|
@@ -844,7 +901,9 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
844
901
|
// straight to the SAME pure claudeTranscriptReader.ts that derives
|
|
845
902
|
// the path exactly as autolaunch.sh names it (psp-01: agentId ==
|
|
846
903
|
// the .jsonl filename).
|
|
847
|
-
readTranscript: ({ workspaceId, agentId }) => (0, claudeTranscriptReader_1.readAgentTranscript)(reposBaseDir, workspaceId, agentId, {
|
|
904
|
+
readTranscript: ({ workspaceId, agentId, engine, opencodeSessionId }) => (0, claudeTranscriptReader_1.readAgentTranscript)(reposBaseDir, workspaceId, agentId, {
|
|
905
|
+
engine,
|
|
906
|
+
opencodeSessionId,
|
|
848
907
|
existsSync: (p) => fs.existsSync(p),
|
|
849
908
|
statSize: (p) => fs.statSync(p).size,
|
|
850
909
|
readFileUtf8: (p) => fs.readFileSync(p, 'utf-8'),
|
|
@@ -1066,6 +1125,8 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1066
1125
|
port: Number(opts.port),
|
|
1067
1126
|
activityFile,
|
|
1068
1127
|
hiveOpSettingsPath,
|
|
1128
|
+
...(mcpInstall.ok ? { mcpServer: { command: mcpInstall.command, args: mcpInstall.args } } : {}),
|
|
1129
|
+
mcpApiUrl: bridgeApiUrl(),
|
|
1069
1130
|
// tchp-18: the loopback changes reporter is opt-in; without this the
|
|
1070
1131
|
// tchp-12 machinery never constructs on a laptop and local panels sit
|
|
1071
1132
|
// on "Waiting for changes…" forever.
|
|
@@ -1187,6 +1248,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1187
1248
|
// agent's PTY, so a missing install surfaces a clear in-pane error
|
|
1188
1249
|
// line (server.ts) instead of a raw spawn ENOENT crash.
|
|
1189
1250
|
commandExists: (command) => commandExistsOnPath(command),
|
|
1251
|
+
prepareOpencodeLaunch: (l) => opencodeProvisioner.prepare(l),
|
|
1190
1252
|
// psp-01 — real on-disk check: does THIS AGENT already have a Claude
|
|
1191
1253
|
// Code conversation for this cwd (a "<agentId>.jsonl" transcript)?
|
|
1192
1254
|
// Drives server.ts's per-agent session-pin decision on the
|
|
@@ -1288,8 +1350,12 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1288
1350
|
// opt-in consents to, and the consent copy says so; it does NOT change
|
|
1289
1351
|
// what an ordinary local agent does (that still honours its own flag,
|
|
1290
1352
|
// default off — ADR-0035).
|
|
1291
|
-
resolveLaunch: ({ sessionId }) => (0, agentLaunch_1.resolveAgentLaunch)({
|
|
1353
|
+
resolveLaunch: ({ sessionId, opencode, connectToken }) => (0, agentLaunch_1.resolveAgentLaunch)({
|
|
1292
1354
|
autonomous: true,
|
|
1355
|
+
...(opencode ? { opencode } : {}),
|
|
1356
|
+
...(connectToken ? { connectToken } : {}),
|
|
1357
|
+
...(mcpInstall.ok ? { mcpServer: { command: mcpInstall.command, args: mcpInstall.args } } : {}),
|
|
1358
|
+
apiUrl: bridgeApiUrl(),
|
|
1293
1359
|
...(activityFile && hiveOpSettingsPath && sessionId
|
|
1294
1360
|
? { activity: { settingsPath: hiveOpSettingsPath, sessionId, activityFile } }
|
|
1295
1361
|
: {}),
|
|
@@ -1297,6 +1363,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1297
1363
|
// A missing binary is refused rather than degraded to a shell — a crew
|
|
1298
1364
|
// member has no pane to print a red line into.
|
|
1299
1365
|
commandExists: (command) => commandExistsOnPath(command),
|
|
1366
|
+
prepareOpencodeLaunch: (l) => opencodeProvisioner.prepare(l),
|
|
1300
1367
|
// fcct-27 — this is what keeps "crews may run here" from becoming "the
|
|
1301
1368
|
// platform may drive any terminal on this machine". Both lanes share one
|
|
1302
1369
|
// SessionManager by necessity, so without it the relay could attach to,
|
|
@@ -1318,13 +1385,13 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1318
1385
|
// with no brief and no error anywhere. The local workspace's root comes
|
|
1319
1386
|
// from the folder registry — no base dir can derive a folder the user
|
|
1320
1387
|
// named themselves.
|
|
1321
|
-
installBundle: ({ workspaceId, files }) => {
|
|
1388
|
+
installBundle: ({ workspaceId, files, engine }) => {
|
|
1322
1389
|
const entry = (0, folderRegistry_1.findEntryByWorkspaceId)((0, folderRegistry_1.loadFolderRegistry)(), workspaceId);
|
|
1323
1390
|
if (!entry) {
|
|
1324
1391
|
console.error(chalk_1.default.red(`✗ Crew bundle for unknown local workspace ${workspaceId} — nothing written`));
|
|
1325
1392
|
return { written: [], skipped: [] };
|
|
1326
1393
|
}
|
|
1327
|
-
const result = (0, bundleInstaller_1.installBundle)({ workspaceId, files }, {
|
|
1394
|
+
const result = (0, bundleInstaller_1.installBundle)({ workspaceId, files, engine }, {
|
|
1328
1395
|
reposBaseDir: '',
|
|
1329
1396
|
workspaceRoot: entry.path,
|
|
1330
1397
|
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// HiveOp OpenCode activity plugin (oc-06). Self-contained ESM: no imports beyond node:*.
|
|
2
|
+
// Writes the SAME JSONL lines as the Claude Code hooks (hooks/post-tool-use.js, stop.js)
|
|
3
|
+
// to $HIVEOP_ACTIVITY_FILE, which the bridge already tails. Copies of this file live in
|
|
4
|
+
// infra/terminal-image/opencode/ (hosted) and cli/hooks/ (local); a test keeps them identical.
|
|
5
|
+
//
|
|
6
|
+
// Event -> line:
|
|
7
|
+
// tool.execute.before / .after -> kind 'tool' (working, thinking "<Verb> <file>")
|
|
8
|
+
// session.idle, session.status idle -> kind 'stop' (idle, thinking '')
|
|
9
|
+
// permission.asked, question.asked -> kind 'needs_you' (thinking 'Waiting for you')
|
|
10
|
+
// permission.replied -> kind 'tool', tool '' (working, thinking 'Working')
|
|
11
|
+
// session.updated (primary session, real title) -> OSC 0 pane title (oc-17), written to /dev/tty
|
|
12
|
+
// because OpenCode's own title ("OpenCode" / "OC | <title>") is switched off by the launch
|
|
13
|
+
// (OPENCODE_DISABLE_TERMINAL_TITLE=1): the app's auto-title ingestion shows the title verbatim.
|
|
14
|
+
// No-op without HIVEOP_ACTIVITY_FILE and WENRWA_SESSION_ID; nothing ever throws into OpenCode.
|
|
15
|
+
// ONLY plugin functions may be exported: OpenCode loads every export as a plugin, and a helper
|
|
16
|
+
// export breaks every prompt (Plugin.trigger reads hooks off its return value).
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
|
|
20
|
+
// OpenCode tool ids -> the Claude Code names the bridge/touch tracker already understand.
|
|
21
|
+
const TOOL_NAME = {
|
|
22
|
+
read: 'Read', edit: 'Edit', write: 'Write', patch: 'MultiEdit', multiedit: 'MultiEdit', bash: 'Bash',
|
|
23
|
+
}
|
|
24
|
+
const FILE_TOOLS = new Set(['Read', 'Edit', 'Write', 'Update', 'Create', 'MultiEdit', 'NotebookEdit'])
|
|
25
|
+
const VERB = {
|
|
26
|
+
Read: 'Reading', Edit: 'Editing', Write: 'Writing', Update: 'Updating',
|
|
27
|
+
Create: 'Creating', MultiEdit: 'Editing', NotebookEdit: 'Editing', Bash: 'Running',
|
|
28
|
+
}
|
|
29
|
+
const MAX_BASH_PATHS = 5
|
|
30
|
+
|
|
31
|
+
// Same algorithm as hooks/post-tool-use.js extractBashPaths (kept in step by a parity test).
|
|
32
|
+
function extractBashPaths(command, pathExists, cwd) {
|
|
33
|
+
if (!command || typeof command !== 'string') return []
|
|
34
|
+
const tokens = command.match(/"[^"]*"|'[^']*'|[^\s;|&()<>]+/g) || []
|
|
35
|
+
const root = (cwd || '').replace(/\/+$/, '')
|
|
36
|
+
const out = []
|
|
37
|
+
const seen = new Set()
|
|
38
|
+
const bare = (s) => s.replace(/^['"]|['"]$/g, '').replace(/[),;]+$/, '')
|
|
39
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
40
|
+
const prev = bare(tokens[i - 1])
|
|
41
|
+
if (prev === 'cd' || prev === 'pushd') continue
|
|
42
|
+
const t = bare(tokens[i])
|
|
43
|
+
if (!t || t.startsWith('-')) continue
|
|
44
|
+
const norm = t.replace(/\/+$/, '')
|
|
45
|
+
if (!norm || seen.has(norm)) continue
|
|
46
|
+
if (norm === '.' || (root && norm === root)) continue
|
|
47
|
+
let ok = false
|
|
48
|
+
try { ok = !!pathExists(t) } catch { ok = false }
|
|
49
|
+
if (!ok) continue
|
|
50
|
+
seen.add(norm)
|
|
51
|
+
out.push(t)
|
|
52
|
+
if (out.length >= MAX_BASH_PATHS) break
|
|
53
|
+
}
|
|
54
|
+
return out
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const exists = (p) => { try { return fs.existsSync(p) } catch { return false } }
|
|
58
|
+
|
|
59
|
+
/** Build the 'tool' record fields for an OpenCode tool call. */
|
|
60
|
+
function toolRecord(toolId, args) {
|
|
61
|
+
const tool = typeof toolId === 'string' ? (TOOL_NAME[toolId.toLowerCase()] || toolId) : ''
|
|
62
|
+
const a = args && typeof args === 'object' ? args : {}
|
|
63
|
+
const paths = []
|
|
64
|
+
for (const k of ['filePath', 'file_path', 'notebook_path']) if (typeof a[k] === 'string') paths.push(a[k])
|
|
65
|
+
const filePaths = FILE_TOOLS.has(tool)
|
|
66
|
+
? paths.filter((p, i) => p && paths.indexOf(p) === i)
|
|
67
|
+
: tool === 'Bash' ? extractBashPaths(a.command, exists, process.cwd()) : []
|
|
68
|
+
const base = filePaths.length ? String(filePaths[0]).split('/').pop() : ''
|
|
69
|
+
return { kind: 'tool', tool, filePaths, thinking: (VERB[tool] || 'Working') + (base ? ' ' + base : '') }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function append(extra, env) {
|
|
73
|
+
try {
|
|
74
|
+
const file = env.HIVEOP_ACTIVITY_FILE
|
|
75
|
+
const sessionId = env.WENRWA_SESSION_ID || ''
|
|
76
|
+
if (!file || !sessionId) return
|
|
77
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
78
|
+
fs.appendFileSync(file, JSON.stringify({ sessionId, workspaceId: env.WENRWA_WORKSPACE_ID || '', ...extra }) + '\n')
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// OpenCode's placeholder titles until the title generator runs ("New session - <iso>").
|
|
83
|
+
const DEFAULT_TITLE = /^(New session|Child session) - \d{4}-\d{2}-\d{2}T/
|
|
84
|
+
const MAX_TITLE = 80
|
|
85
|
+
|
|
86
|
+
/** oc-17: the OSC 0 sequence for a session.updated event's info, or '' (sub-agent, placeholder, empty). */
|
|
87
|
+
function titleSequence(info) {
|
|
88
|
+
if (!info || info.parentID || typeof info.title !== 'string') return ''
|
|
89
|
+
const t = info.title.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').replace(/\s+/g, ' ').trim()
|
|
90
|
+
if (!t || DEFAULT_TITLE.test(t)) return ''
|
|
91
|
+
return '\x1b]0;' + (t.length > MAX_TITLE ? t.slice(0, MAX_TITLE - 1) + '\u2026' : t) + '\x07'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let lastTitle = ''
|
|
95
|
+
function setPaneTitle(info, ttyPath = '/dev/tty') {
|
|
96
|
+
try {
|
|
97
|
+
const seq = titleSequence(info)
|
|
98
|
+
if (!seq || seq === lastTitle) return
|
|
99
|
+
const fd = fs.openSync(ttyPath, 'a')
|
|
100
|
+
try { fs.writeSync(fd, seq) } finally { fs.closeSync(fd) }
|
|
101
|
+
lastTitle = seq
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let reportedToGateway = ''
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* oc-05 (hosted): POST the primary session's id to $HIVEOP_GATEWAY_URL/hiveop/session, authenticated
|
|
109
|
+
* by the per-agent gateway token (it already identifies the agent). Best-effort: one retry, never
|
|
110
|
+
* throws, no-op without gateway env. Last one wins server-side, so a fresh session after a
|
|
111
|
+
* "Session not found" fallback replaces the dead id.
|
|
112
|
+
*/
|
|
113
|
+
async function reportSessionToGateway(id, env = process.env) {
|
|
114
|
+
try {
|
|
115
|
+
const base = String(env.HIVEOP_GATEWAY_URL || '').replace(/\/+$/, '')
|
|
116
|
+
const token = env.HIVEOP_GATEWAY_TOKEN || ''
|
|
117
|
+
if (!id || !base || !token || id === reportedToGateway) return
|
|
118
|
+
reportedToGateway = id // claimed up front so a burst of events posts once; released on failure
|
|
119
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch(`${base}/hiveop/session`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
124
|
+
body: JSON.stringify({ sessionId: id }),
|
|
125
|
+
signal: AbortSignal.timeout(5000),
|
|
126
|
+
})
|
|
127
|
+
if (res.ok) return
|
|
128
|
+
if (res.status < 500) break
|
|
129
|
+
} catch {}
|
|
130
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
131
|
+
}
|
|
132
|
+
if (reportedToGateway === id) reportedToGateway = ''
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* oc-05 hook point: capture the generated id of an agent's PRIMARY session so the next launch
|
|
138
|
+
* can `opencode -s <id>`. Sub-agent sessions (info.parentID) never overwrite it.
|
|
139
|
+
* Local lane: first id is written to a file under HIVEOP_OPENCODE_SESSIONS_DIR (keyed by agent).
|
|
140
|
+
* Hosted lane: the id is POSTed to the gateway (see reportSessionToGateway).
|
|
141
|
+
*/
|
|
142
|
+
function captureSessionId(event, env = process.env) {
|
|
143
|
+
try {
|
|
144
|
+
const p = (event && event.properties) || {}
|
|
145
|
+
if (!event || event.type !== 'session.created' || (p.info && p.info.parentID)) return
|
|
146
|
+
const id = p.sessionID || (p.info && p.info.id) || ''
|
|
147
|
+
void reportSessionToGateway(id, env)
|
|
148
|
+
const agentId = env.WENRWA_AGENT_ID || ''
|
|
149
|
+
if (!agentId) return
|
|
150
|
+
const dir = env.HIVEOP_OPENCODE_SESSIONS_DIR || '/data/.hiveop/opencode-sessions'
|
|
151
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
152
|
+
const f = path.join(dir, agentId)
|
|
153
|
+
if (!fs.existsSync(f)) fs.writeFileSync(f, id)
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const HiveOp = async () => {
|
|
158
|
+
const env = process.env
|
|
159
|
+
let lastKind = ''
|
|
160
|
+
const emit = (extra) => {
|
|
161
|
+
// session.status idle and session.idle both fire per turn end: write one stop line.
|
|
162
|
+
if (extra.kind === 'stop' && lastKind === 'stop') return
|
|
163
|
+
lastKind = extra.kind
|
|
164
|
+
append({ ...extra, ...(extra.kind === 'stop' ? { thinking: '' } : {}) }, env)
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
event: async ({ event }) => {
|
|
168
|
+
try {
|
|
169
|
+
if (!event) return
|
|
170
|
+
const p = event.properties || {}
|
|
171
|
+
if (event.type === 'session.created') captureSessionId(event, env)
|
|
172
|
+
else if (event.type === 'session.updated') setPaneTitle(p.info, env.HIVEOP_TITLE_TTY || '/dev/tty')
|
|
173
|
+
else if (event.type === 'session.idle') emit({ kind: 'stop' })
|
|
174
|
+
else if (event.type === 'session.status' && p.status && p.status.type === 'idle') emit({ kind: 'stop' })
|
|
175
|
+
else if (event.type === 'permission.asked' || event.type === 'question.asked') emit({ kind: 'needs_you', thinking: 'Waiting for you' })
|
|
176
|
+
else if (event.type === 'permission.replied') emit({ kind: 'tool', tool: '', filePaths: [], thinking: 'Working' })
|
|
177
|
+
} catch {}
|
|
178
|
+
},
|
|
179
|
+
'tool.execute.before': async (input, output) => {
|
|
180
|
+
try { emit(toolRecord(input && input.tool, (output && output.args) || (input && input.args))) } catch {}
|
|
181
|
+
},
|
|
182
|
+
'tool.execute.after': async (input, output) => {
|
|
183
|
+
try { emit(toolRecord(input && input.tool, (input && input.args) || (output && output.args))) } catch {}
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|