@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,178 @@
|
|
|
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
|
+
exports.defaultSqliteQuery = void 0;
|
|
37
|
+
exports.opencodeDbPath = opencodeDbPath;
|
|
38
|
+
exports.opencodeRowsToJsonl = opencodeRowsToJsonl;
|
|
39
|
+
exports.readOpencodeTranscript = readOpencodeTranscript;
|
|
40
|
+
/**
|
|
41
|
+
* oc-17 — the OpenCode half of the wte-10 transcript read. Claude Code keeps
|
|
42
|
+
* one jsonl per agent; OpenCode (1.18.x, oc-04 findings) keeps every session
|
|
43
|
+
* in ONE SQLite file, `$XDG_DATA_HOME/opencode/opencode.db`, tables
|
|
44
|
+
* session(id 'ses_…', parent_id, title, time_created, time_updated, …)
|
|
45
|
+
* message(id, session_id, time_created, data JSON {role, time, …})
|
|
46
|
+
* part(id, message_id, session_id, data JSON {type: text|tool|reasoning|…})
|
|
47
|
+
* Hosted lane: XDG_DATA_HOME is per AGENT (autolaunch.sh):
|
|
48
|
+
* <WENRWA_OPENCODE_DATA_ROOT or /data/.opencode>/<agentId>/share/opencode/opencode.db
|
|
49
|
+
*
|
|
50
|
+
* The result is re-serialised as Claude-shaped jsonl (user / assistant lines,
|
|
51
|
+
* tool_use + tool_result blocks) so every consumer of the Claude transcript
|
|
52
|
+
* reads an OpenCode agent's without knowing the engine. Read-only: the DB is
|
|
53
|
+
* opened read-only (node:sqlite, else the `sqlite3` CLI); nothing is written,
|
|
54
|
+
* cached or kept. With neither available the answer is the typed
|
|
55
|
+
* 'transcript-unavailable' refusal, never a crash.
|
|
56
|
+
*/
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const path = __importStar(require("path"));
|
|
59
|
+
const child_process_1 = require("child_process");
|
|
60
|
+
const claudeTranscriptReader_1 = require("./claudeTranscriptReader");
|
|
61
|
+
const SESSION_ID = /^ses_[A-Za-z0-9]+$/;
|
|
62
|
+
/** Where the hosted launch put this agent's OpenCode database. */
|
|
63
|
+
function opencodeDbPath(agentId, deps = {}) {
|
|
64
|
+
const root = deps.dataRoot ?? process.env.WENRWA_OPENCODE_DATA_ROOT ?? '/data/.opencode';
|
|
65
|
+
return path.join(root, agentId, 'share', 'opencode', 'opencode.db');
|
|
66
|
+
}
|
|
67
|
+
/** Default driver: node:sqlite (Node >= 22.5, the image runs 24), else the sqlite3 CLI. */
|
|
68
|
+
const defaultSqliteQuery = (dbPath, sql) => {
|
|
69
|
+
try {
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
71
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
72
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
73
|
+
try {
|
|
74
|
+
return db.prepare(sql).all();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
db.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
const code = e?.code;
|
|
82
|
+
if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_UNKNOWN_BUILTIN_MODULE')
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const out = (0, child_process_1.execFileSync)('sqlite3', ['-readonly', '-json', dbPath, sql], {
|
|
87
|
+
encoding: 'utf-8',
|
|
88
|
+
maxBuffer: 4 * claudeTranscriptReader_1.MAX_TRANSCRIPT_BYTES,
|
|
89
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
90
|
+
});
|
|
91
|
+
return out.trim() ? JSON.parse(out) : [];
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
if (e?.code === 'ENOENT')
|
|
95
|
+
throw new Error('sqlite-unavailable');
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
exports.defaultSqliteQuery = defaultSqliteQuery;
|
|
100
|
+
function parseJson(v) {
|
|
101
|
+
try {
|
|
102
|
+
const o = JSON.parse(String(v));
|
|
103
|
+
return o && typeof o === 'object' ? o : {};
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const iso = (ms) => (typeof ms === 'number' ? new Date(ms).toISOString() : undefined);
|
|
110
|
+
/** Pure: OpenCode message/part rows -> Claude-shaped jsonl lines. */
|
|
111
|
+
function opencodeRowsToJsonl(sessionId, messages, parts) {
|
|
112
|
+
const byMessage = new Map();
|
|
113
|
+
for (const p of parts) {
|
|
114
|
+
const list = byMessage.get(String(p.message_id)) ?? [];
|
|
115
|
+
list.push(parseJson(p.data));
|
|
116
|
+
byMessage.set(String(p.message_id), list);
|
|
117
|
+
}
|
|
118
|
+
const lines = [];
|
|
119
|
+
const emit = (type, content, ts) => {
|
|
120
|
+
if (!content.length)
|
|
121
|
+
return;
|
|
122
|
+
lines.push(JSON.stringify({ type, sessionId, timestamp: iso(ts), message: { role: type, content } }));
|
|
123
|
+
};
|
|
124
|
+
for (const m of messages) {
|
|
125
|
+
const info = parseJson(m.data);
|
|
126
|
+
const role = info.role === 'assistant' ? 'assistant' : 'user';
|
|
127
|
+
const content = [];
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const part of byMessage.get(String(m.id)) ?? []) {
|
|
130
|
+
if (part.type === 'text' && typeof part.text === 'string' && part.text) {
|
|
131
|
+
content.push({ type: 'text', text: part.text });
|
|
132
|
+
}
|
|
133
|
+
else if (part.type === 'file') {
|
|
134
|
+
content.push({ type: 'text', text: `[file: ${part.filename ?? part.mime ?? 'attachment'}]` });
|
|
135
|
+
}
|
|
136
|
+
else if (part.type === 'tool' && role === 'assistant') {
|
|
137
|
+
const st = part.state ?? {};
|
|
138
|
+
content.push({ type: 'tool_use', id: part.callID, name: part.tool, input: st.input ?? {} });
|
|
139
|
+
if (st.status === 'completed') {
|
|
140
|
+
results.push({ type: 'tool_result', tool_use_id: part.callID, content: String(st.output ?? '') });
|
|
141
|
+
}
|
|
142
|
+
else if (st.status === 'error') {
|
|
143
|
+
results.push({ type: 'tool_result', tool_use_id: part.callID, is_error: true, content: String(st.error ?? '') });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
emit(role, content, m.time_created);
|
|
148
|
+
emit('user', results, m.time_created);
|
|
149
|
+
}
|
|
150
|
+
return lines.length ? lines.join('\n') + '\n' : '';
|
|
151
|
+
}
|
|
152
|
+
/** Read ONE agent's OpenCode transcript. `sessionId` pins a session; absent -> the newest root session. */
|
|
153
|
+
function readOpencodeTranscript(agentId, sessionId, deps = {}) {
|
|
154
|
+
const existsSync = deps.existsSync ?? fs.existsSync;
|
|
155
|
+
const query = deps.query ?? exports.defaultSqliteQuery;
|
|
156
|
+
if (sessionId !== undefined && !SESSION_ID.test(sessionId))
|
|
157
|
+
return { ok: false, reason: 'not-found' };
|
|
158
|
+
const db = opencodeDbPath(agentId, deps);
|
|
159
|
+
if (!existsSync(db))
|
|
160
|
+
return { ok: false, reason: 'not-found' };
|
|
161
|
+
try {
|
|
162
|
+
const sessions = sessionId
|
|
163
|
+
? query(db, `SELECT id FROM session WHERE id = '${sessionId}'`)
|
|
164
|
+
: query(db, 'SELECT id FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC LIMIT 1');
|
|
165
|
+
const id = sessions[0]?.id;
|
|
166
|
+
if (typeof id !== 'string' || !SESSION_ID.test(id))
|
|
167
|
+
return { ok: false, reason: 'not-found' };
|
|
168
|
+
const messages = query(db, `SELECT id, time_created, data FROM message WHERE session_id = '${id}' ORDER BY time_created, id`);
|
|
169
|
+
const parts = query(db, `SELECT message_id, data FROM part WHERE session_id = '${id}' ORDER BY message_id, id`);
|
|
170
|
+
const content = opencodeRowsToJsonl(id, messages, parts);
|
|
171
|
+
if (Buffer.byteLength(content) > claudeTranscriptReader_1.MAX_TRANSCRIPT_BYTES)
|
|
172
|
+
return { ok: false, reason: 'too-large' };
|
|
173
|
+
return { ok: true, content };
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return { ok: false, reason: 'transcript-unavailable' };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ht-05 — Terminal Bridge relay mode (dial-out).
|
|
3
|
+
*
|
|
4
|
+
* Instead of listening on localhost for browser connections (server.ts),
|
|
5
|
+
* this DIALS OUT to the platform relay over a single outbound wss
|
|
6
|
+
* connection, authenticates with a per-container relay token, and serves
|
|
7
|
+
* the same v1 frame protocol (see PROTOCOL.md) through the existing
|
|
8
|
+
* SessionManager. Multi-session multiplexing is unchanged: every frame
|
|
9
|
+
* already carries a sessionId, so one outbound socket serves every session
|
|
10
|
+
* exactly like the local server serves them over N inbound sockets.
|
|
11
|
+
*
|
|
12
|
+
* PTYs live in the SessionManager, not the socket — a relay drop never
|
|
13
|
+
* kills a session. This module owns only: dialing out, the auth frame,
|
|
14
|
+
* frame routing through SessionManager, and reconnect-with-backoff. It
|
|
15
|
+
* does no scrollback bookkeeping of its own (SessionManager already does
|
|
16
|
+
* that) — on re-pair the pane sends `attach` again and gets a fresh
|
|
17
|
+
* `replay`, exactly as it would against the local server.
|
|
18
|
+
*/
|
|
19
|
+
import { type OpencodeLaunchInput } from './agentLaunch';
|
|
1
20
|
import { SessionManager } from './sessionManager';
|
|
2
21
|
import { MemInfo } from './containerMemory';
|
|
3
22
|
import { BrowserDriver } from './browserDriver';
|
|
@@ -163,6 +182,8 @@ export interface RelayClientOpts {
|
|
|
163
182
|
workspaceId?: string;
|
|
164
183
|
sessionId?: string;
|
|
165
184
|
autonomous?: boolean;
|
|
185
|
+
opencode?: OpencodeLaunchInput;
|
|
186
|
+
connectToken?: string;
|
|
166
187
|
}) => {
|
|
167
188
|
command: string;
|
|
168
189
|
args: string[];
|
|
@@ -214,6 +235,7 @@ export interface RelayClientOpts {
|
|
|
214
235
|
installBundle?: (bundle: {
|
|
215
236
|
workspaceId: string;
|
|
216
237
|
files: Record<string, string>;
|
|
238
|
+
engine?: string;
|
|
217
239
|
}) => {
|
|
218
240
|
written: string[];
|
|
219
241
|
skipped: Array<{
|
|
@@ -237,10 +259,12 @@ export interface RelayClientOpts {
|
|
|
237
259
|
*/
|
|
238
260
|
refreshContextPointers?: (args: {
|
|
239
261
|
workspaceId: string;
|
|
262
|
+
engine?: string;
|
|
240
263
|
}) => void | Promise<void>;
|
|
241
264
|
writeContext?: (manifest: {
|
|
242
265
|
workspaceId: string;
|
|
243
266
|
content: string;
|
|
267
|
+
engine?: string;
|
|
244
268
|
}) => {
|
|
245
269
|
action: string;
|
|
246
270
|
claudeMdPath: string | null;
|
|
@@ -307,12 +331,14 @@ export interface RelayClientOpts {
|
|
|
307
331
|
readTranscript?: (args: {
|
|
308
332
|
workspaceId: string;
|
|
309
333
|
agentId: string;
|
|
334
|
+
engine?: 'claude_code' | 'opencode';
|
|
335
|
+
opencodeSessionId?: string;
|
|
310
336
|
}) => {
|
|
311
337
|
ok: true;
|
|
312
338
|
content: string;
|
|
313
339
|
} | {
|
|
314
340
|
ok: false;
|
|
315
|
-
reason: 'not-found' | 'invalid-agent-id' | 'too-large';
|
|
341
|
+
reason: 'not-found' | 'invalid-agent-id' | 'too-large' | 'transcript-unavailable';
|
|
316
342
|
};
|
|
317
343
|
/** Injectable for tests: defaults to `new WebSocket(url)` from `ws`. */
|
|
318
344
|
wsFactory?: (url: string) => WebSocketLike;
|
|
@@ -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,9 @@ 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
|
-
const launch = opts.resolveLaunch?.({ workspaceId, sessionId: relaySessionId, autonomous }) ?? null;
|
|
1063
|
+
const launch = opts.resolveLaunch?.({ workspaceId, sessionId: relaySessionId, autonomous, opencode, connectToken }) ?? null;
|
|
1063
1064
|
if (launch && opts.commandExists && !opts.commandExists(launch.command)) {
|
|
1064
|
-
const message =
|
|
1065
|
+
const message = (0, agentLaunch_1.missingCommandMessage)(launch.command);
|
|
1065
1066
|
console.error(`[bridge] refusing PTY spawn — ${message}`);
|
|
1066
1067
|
send(socket, {
|
|
1067
1068
|
t: 'error',
|
|
@@ -1116,7 +1117,11 @@ function startRelayClient(opts) {
|
|
|
1116
1117
|
delay or fail an `opened`.
|
|
1117
1118
|
*/
|
|
1118
1119
|
if (workspaceId && opts.refreshContextPointers) {
|
|
1119
|
-
void Promise.resolve(opts.refreshContextPointers({
|
|
1120
|
+
void Promise.resolve(opts.refreshContextPointers({
|
|
1121
|
+
workspaceId,
|
|
1122
|
+
// oc-23: the relay-injected env names the agent's engine; absent = Claude.
|
|
1123
|
+
...(injectedEnv?.WENRWA_AGENT_ENGINE ? { engine: injectedEnv.WENRWA_AGENT_ENGINE } : {}),
|
|
1124
|
+
})).catch((err) => {
|
|
1120
1125
|
console.error('[bridge] context pointer refresh failed (non-fatal):', err);
|
|
1121
1126
|
});
|
|
1122
1127
|
}
|
|
@@ -1294,7 +1299,12 @@ function startRelayClient(opts) {
|
|
|
1294
1299
|
});
|
|
1295
1300
|
break;
|
|
1296
1301
|
}
|
|
1297
|
-
const result = opts.installBundle({
|
|
1302
|
+
const result = opts.installBundle({
|
|
1303
|
+
workspaceId: frame.workspaceId,
|
|
1304
|
+
files: frame.files ?? {},
|
|
1305
|
+
// oc-18: the arena names the target agent's engine; absent = Claude.
|
|
1306
|
+
...(frame.engine ? { engine: frame.engine } : {}),
|
|
1307
|
+
});
|
|
1298
1308
|
send(socket, {
|
|
1299
1309
|
t: 'bundle-installed',
|
|
1300
1310
|
workspaceId: frame.workspaceId,
|
|
@@ -1401,7 +1411,11 @@ function startRelayClient(opts) {
|
|
|
1401
1411
|
// rejection can only come from a bug in the writer, never from the
|
|
1402
1412
|
// brief fetch (which resolves null on any failure, PRD D2), so it
|
|
1403
1413
|
// is answered rather than thrown into an unhandled rejection.
|
|
1404
|
-
Promise.resolve(opts.writeContext({
|
|
1414
|
+
Promise.resolve(opts.writeContext({
|
|
1415
|
+
workspaceId: frame.workspaceId,
|
|
1416
|
+
content: frame.content ?? '',
|
|
1417
|
+
...(frame.engine ? { engine: frame.engine } : {}),
|
|
1418
|
+
}))
|
|
1405
1419
|
.then((result) => {
|
|
1406
1420
|
send(socket, {
|
|
1407
1421
|
t: 'context-written',
|
|
@@ -1568,7 +1582,7 @@ function startRelayClient(opts) {
|
|
|
1568
1582
|
});
|
|
1569
1583
|
break;
|
|
1570
1584
|
}
|
|
1571
|
-
const result = opts.readTranscript({ workspaceId, agentId: frame.agentId });
|
|
1585
|
+
const result = opts.readTranscript({ workspaceId, agentId: frame.agentId, engine: frame.engine, opencodeSessionId: frame.opencodeSessionId });
|
|
1572
1586
|
send(socket, {
|
|
1573
1587
|
t: 'transcript-content',
|
|
1574
1588
|
requestId,
|
|
@@ -1975,6 +1989,9 @@ function startRelayClient(opts) {
|
|
|
1975
1989
|
sessionId: rec.sessionId,
|
|
1976
1990
|
...(Array.isArray(rec.filePaths) && rec.filePaths.length ? { filePaths: rec.filePaths } : {}),
|
|
1977
1991
|
...(typeof rec.thinking === 'string' ? { thinking: rec.thinking } : {}),
|
|
1992
|
+
// oc-22 — carry ONLY 'needs_you' (the OpenCode plugin's prompt signal);
|
|
1993
|
+
// Claude's 'tool'/'stop' kinds stay dropped so its frames are unchanged.
|
|
1994
|
+
...(rec.kind === 'needs_you' ? { kind: 'needs_you' } : {}),
|
|
1978
1995
|
});
|
|
1979
1996
|
}
|
|
1980
1997
|
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
|
@@ -229,6 +229,12 @@ export interface BridgeServerOpts {
|
|
|
229
229
|
* bridge.ts) — see the 'open' handler's agentId branch.
|
|
230
230
|
*/
|
|
231
231
|
hiveOpSettingsPath?: string;
|
|
232
|
+
/** oc-07 — resolved installed MCP server + API URL for local OpenCode agents (same as mcpRegister's). */
|
|
233
|
+
mcpServer?: {
|
|
234
|
+
command: string;
|
|
235
|
+
args: string[];
|
|
236
|
+
};
|
|
237
|
+
mcpApiUrl?: string;
|
|
232
238
|
/**
|
|
233
239
|
* rival-10 — every file touch of a LOCAL WORKSPACE AGENT (an `open` with
|
|
234
240
|
* 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
|
},
|
|
@@ -733,6 +735,10 @@ function startBridgeServer(opts) {
|
|
|
733
735
|
// arena client actually asked to open.
|
|
734
736
|
const 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 } }
|
|
@@ -765,7 +771,9 @@ function startBridgeServer(opts) {
|
|
|
765
771
|
send(ws, {
|
|
766
772
|
t: 'data',
|
|
767
773
|
sessionId,
|
|
768
|
-
data:
|
|
774
|
+
data: launch.command === 'opencode'
|
|
775
|
+
? `\r\n\x1b[31m${(0, agentLaunch_1.missingCommandMessage)('opencode')}, then reopen this pane.\x1b[0m\r\n`
|
|
776
|
+
: '\r\n\x1b[31mclaude not found on PATH — install Claude Code CLI, then reopen this pane.\x1b[0m\r\n',
|
|
769
777
|
});
|
|
770
778
|
}).catch((err) => {
|
|
771
779
|
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
|
@@ -74,6 +74,7 @@ const autostart_1 = require("../bridge/autostart");
|
|
|
74
74
|
const repoCheckout_1 = require("../bridge/repoCheckout");
|
|
75
75
|
const bundleInstaller_1 = require("../bridge/bundleInstaller");
|
|
76
76
|
const agentLaunch_1 = require("../bridge/agentLaunch");
|
|
77
|
+
const opencodeConfig_1 = require("../bridge/opencodeConfig");
|
|
77
78
|
const contextWriter_1 = require("../bridge/contextWriter");
|
|
78
79
|
const memoryBrief_1 = require("../bridge/memoryBrief");
|
|
79
80
|
const containerFileRelay_1 = require("../bridge/containerFileRelay");
|
|
@@ -170,6 +171,20 @@ function bridgeApiUrl() {
|
|
|
170
171
|
// RELAY_URL/RELAY_TOKEN env vars are the container-friendly fallback (the
|
|
171
172
|
// image sets these instead of passing CLI flags), and a URL with no token
|
|
172
173
|
// anywhere is a startup error rather than a confusing runtime failure.
|
|
174
|
+
/**
|
|
175
|
+
* oc-08 — OpenCode is usable only if it is on PATH AND at least the minimum
|
|
176
|
+
* version oc-04 verified (the local lane runs the user's own install, which
|
|
177
|
+
* can be anything). Missing and too-old get the same install hint.
|
|
178
|
+
*/
|
|
179
|
+
function opencodeUsable() {
|
|
180
|
+
try {
|
|
181
|
+
const out = (0, child_process_1.execFileSync)('opencode', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
|
|
182
|
+
return (0, opencodeConfig_1.opencodeVersionOk)(out);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
173
188
|
/**
|
|
174
189
|
* lws-06 / fcct-27 — is a binary on PATH? Best-effort and shared: the loopback
|
|
175
190
|
* lane degrades to a shell + a red in-pane line when `claude` is missing, the
|
|
@@ -178,6 +193,8 @@ function bridgeApiUrl() {
|
|
|
178
193
|
* this machine can run Claude Code.
|
|
179
194
|
*/
|
|
180
195
|
function commandExistsOnPath(command) {
|
|
196
|
+
if (command === 'opencode')
|
|
197
|
+
return opencodeUsable();
|
|
181
198
|
try {
|
|
182
199
|
(0, child_process_1.execFileSync)(process.platform === 'win32' ? 'where' : 'command', process.platform === 'win32' ? [command] : ['-v', command], { stdio: 'ignore', shell: process.platform !== 'win32' });
|
|
183
200
|
return true;
|
|
@@ -722,8 +739,8 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
722
739
|
// workspace checkout under the same /data/repos/<workspaceId> root. Pure
|
|
723
740
|
// file writes (path-safe + idempotent) — Claude Code orchestrates the
|
|
724
741
|
// crew natively next run; the credits crewExecutionEngine is never called.
|
|
725
|
-
installBundle: ({ workspaceId, files }) => {
|
|
726
|
-
const result = (0, bundleInstaller_1.installBundle)({ workspaceId, files }, {
|
|
742
|
+
installBundle: ({ workspaceId, files, engine }) => {
|
|
743
|
+
const result = (0, bundleInstaller_1.installBundle)({ workspaceId, files, engine }, {
|
|
727
744
|
reposBaseDir,
|
|
728
745
|
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
729
746
|
writeFile: (p, content) => fs.writeFileSync(p, content),
|
|
@@ -755,7 +772,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
755
772
|
only sends one when the pilot has files selected, so most workspaces
|
|
756
773
|
never got the pointers at all.
|
|
757
774
|
*/
|
|
758
|
-
refreshContextPointers: async ({ workspaceId }) => {
|
|
775
|
+
refreshContextPointers: async ({ workspaceId, engine }) => {
|
|
759
776
|
const memoryAvailable = Boolean(process.env.WENRWA_CONNECT_TOKEN);
|
|
760
777
|
// 2026-10-02 — the same reply says whether this is a MATCH workspace.
|
|
761
778
|
const briefInfo = memoryAvailable ? await (0, memoryBrief_1.fetchWorkspaceBrief)(workspaceId) : null;
|
|
@@ -763,6 +780,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
763
780
|
const result = (0, contextWriter_1.writeContext)({
|
|
764
781
|
workspaceId,
|
|
765
782
|
pointersOnly: true,
|
|
783
|
+
engine,
|
|
766
784
|
workGraphAvailable: Boolean(process.env.WENRWA_CONNECT_TOKEN),
|
|
767
785
|
memoryAvailable,
|
|
768
786
|
memoryBrief,
|
|
@@ -778,7 +796,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
778
796
|
console.log(chalk_1.default.green(`✓ Context pointers ${result.action} → workspace ${workspaceId}`));
|
|
779
797
|
}
|
|
780
798
|
},
|
|
781
|
-
writeContext: async ({ workspaceId, content }) => {
|
|
799
|
+
writeContext: async ({ workspaceId, content, engine }) => {
|
|
782
800
|
// wmr-03 — the workspace's memory brief, fetched at manifest time so
|
|
783
801
|
// the agent has it in context from its next turn. Never blocks the
|
|
784
802
|
// write: fetchWorkspaceBrief resolves null on any failure (PRD D2), and
|
|
@@ -791,6 +809,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
791
809
|
const result = (0, contextWriter_1.writeContext)({
|
|
792
810
|
workspaceId,
|
|
793
811
|
content,
|
|
812
|
+
engine,
|
|
794
813
|
// wgcp-01 — same gate as checkStorageQuota above: WENRWA_CONNECT_TOKEN
|
|
795
814
|
// is only injected at container boot when this workspace actually has
|
|
796
815
|
// MCP world-wiring (hosted-container.service.ts's machineEnv). Absent
|
|
@@ -809,7 +828,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
809
828
|
writeFile: (p, c) => fs.writeFileSync(p, c),
|
|
810
829
|
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
811
830
|
});
|
|
812
|
-
console.log(chalk_1.default.green(`✓ Context manifest ${result.action} → workspace ${workspaceId} (CLAUDE.md)`));
|
|
831
|
+
console.log(chalk_1.default.green(`✓ Context manifest ${result.action} → workspace ${workspaceId} (${path.posix.basename(result.claudeMdPath ?? 'CLAUDE.md')})`));
|
|
813
832
|
return { action: result.action, claudeMdPath: result.claudeMdPath };
|
|
814
833
|
},
|
|
815
834
|
// cve-06 (ADR-0047 / ADR-0032): read ONE file out of
|
|
@@ -844,7 +863,9 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
844
863
|
// straight to the SAME pure claudeTranscriptReader.ts that derives
|
|
845
864
|
// the path exactly as autolaunch.sh names it (psp-01: agentId ==
|
|
846
865
|
// the .jsonl filename).
|
|
847
|
-
readTranscript: ({ workspaceId, agentId }) => (0, claudeTranscriptReader_1.readAgentTranscript)(reposBaseDir, workspaceId, agentId, {
|
|
866
|
+
readTranscript: ({ workspaceId, agentId, engine, opencodeSessionId }) => (0, claudeTranscriptReader_1.readAgentTranscript)(reposBaseDir, workspaceId, agentId, {
|
|
867
|
+
engine,
|
|
868
|
+
opencodeSessionId,
|
|
848
869
|
existsSync: (p) => fs.existsSync(p),
|
|
849
870
|
statSize: (p) => fs.statSync(p).size,
|
|
850
871
|
readFileUtf8: (p) => fs.readFileSync(p, 'utf-8'),
|
|
@@ -1066,6 +1087,8 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1066
1087
|
port: Number(opts.port),
|
|
1067
1088
|
activityFile,
|
|
1068
1089
|
hiveOpSettingsPath,
|
|
1090
|
+
...(mcpInstall.ok ? { mcpServer: { command: mcpInstall.command, args: mcpInstall.args } } : {}),
|
|
1091
|
+
mcpApiUrl: bridgeApiUrl(),
|
|
1069
1092
|
// tchp-18: the loopback changes reporter is opt-in; without this the
|
|
1070
1093
|
// tchp-12 machinery never constructs on a laptop and local panels sit
|
|
1071
1094
|
// on "Waiting for changes…" forever.
|
|
@@ -1288,8 +1311,12 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1288
1311
|
// opt-in consents to, and the consent copy says so; it does NOT change
|
|
1289
1312
|
// what an ordinary local agent does (that still honours its own flag,
|
|
1290
1313
|
// default off — ADR-0035).
|
|
1291
|
-
resolveLaunch: ({ sessionId }) => (0, agentLaunch_1.resolveAgentLaunch)({
|
|
1314
|
+
resolveLaunch: ({ sessionId, opencode, connectToken }) => (0, agentLaunch_1.resolveAgentLaunch)({
|
|
1292
1315
|
autonomous: true,
|
|
1316
|
+
...(opencode ? { opencode } : {}),
|
|
1317
|
+
...(connectToken ? { connectToken } : {}),
|
|
1318
|
+
...(mcpInstall.ok ? { mcpServer: { command: mcpInstall.command, args: mcpInstall.args } } : {}),
|
|
1319
|
+
apiUrl: bridgeApiUrl(),
|
|
1293
1320
|
...(activityFile && hiveOpSettingsPath && sessionId
|
|
1294
1321
|
? { activity: { settingsPath: hiveOpSettingsPath, sessionId, activityFile } }
|
|
1295
1322
|
: {}),
|
|
@@ -1318,13 +1345,13 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1318
1345
|
// with no brief and no error anywhere. The local workspace's root comes
|
|
1319
1346
|
// from the folder registry — no base dir can derive a folder the user
|
|
1320
1347
|
// named themselves.
|
|
1321
|
-
installBundle: ({ workspaceId, files }) => {
|
|
1348
|
+
installBundle: ({ workspaceId, files, engine }) => {
|
|
1322
1349
|
const entry = (0, folderRegistry_1.findEntryByWorkspaceId)((0, folderRegistry_1.loadFolderRegistry)(), workspaceId);
|
|
1323
1350
|
if (!entry) {
|
|
1324
1351
|
console.error(chalk_1.default.red(`✗ Crew bundle for unknown local workspace ${workspaceId} — nothing written`));
|
|
1325
1352
|
return { written: [], skipped: [] };
|
|
1326
1353
|
}
|
|
1327
|
-
const result = (0, bundleInstaller_1.installBundle)({ workspaceId, files }, {
|
|
1354
|
+
const result = (0, bundleInstaller_1.installBundle)({ workspaceId, files, engine }, {
|
|
1328
1355
|
reposBaseDir: '',
|
|
1329
1356
|
workspaceRoot: entry.path,
|
|
1330
1357
|
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|