@wenrwa/marketplace-cli 0.7.8 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/__tests__/bridge-activity-tail.test.js +16 -0
  2. package/dist/__tests__/bridge-attach-paste-engine.test.d.ts +1 -0
  3. package/dist/__tests__/bridge-attach-paste-engine.test.js +108 -0
  4. package/dist/__tests__/bridge-bundle-installer-opencode.test.d.ts +1 -0
  5. package/dist/__tests__/bridge-bundle-installer-opencode.test.js +69 -0
  6. package/dist/__tests__/bridge-context-engine.test.d.ts +1 -0
  7. package/dist/__tests__/bridge-context-engine.test.js +112 -0
  8. package/dist/__tests__/bridge-context-manifest-frame.test.js +19 -0
  9. package/dist/__tests__/bridge-crew-translator.test.d.ts +1 -0
  10. package/dist/__tests__/bridge-crew-translator.test.js +103 -0
  11. package/dist/__tests__/bridge-install-bundle-frame.test.js +24 -0
  12. package/dist/__tests__/bridge-loopback-opencode-open.test.d.ts +1 -0
  13. package/dist/__tests__/bridge-loopback-opencode-open.test.js +51 -0
  14. package/dist/__tests__/bridge-opencode-binary.test.d.ts +1 -0
  15. package/dist/__tests__/bridge-opencode-binary.test.js +128 -0
  16. package/dist/__tests__/bridge-opencode-launch.test.d.ts +1 -0
  17. package/dist/__tests__/bridge-opencode-launch.test.js +216 -0
  18. package/dist/__tests__/bridge-relay-pucc14.test.js +28 -0
  19. package/dist/__tests__/bridge-server.test.js +32 -0
  20. package/dist/__tests__/opencode-config-parity.test.d.ts +1 -0
  21. package/dist/__tests__/opencode-config-parity.test.js +85 -0
  22. package/dist/__tests__/opencode-plugin.test.d.ts +1 -0
  23. package/dist/__tests__/opencode-plugin.test.js +197 -0
  24. package/dist/__tests__/opencode-title.test.d.ts +1 -0
  25. package/dist/__tests__/opencode-title.test.js +99 -0
  26. package/dist/__tests__/opencode-transcript-reader.test.d.ts +1 -0
  27. package/dist/__tests__/opencode-transcript-reader.test.js +156 -0
  28. package/dist/bridge/activityTail.d.ts +7 -0
  29. package/dist/bridge/activityTail.js +1 -0
  30. package/dist/bridge/agentLaunch.d.ts +28 -0
  31. package/dist/bridge/agentLaunch.js +49 -0
  32. package/dist/bridge/attachmentWriter.d.ts +16 -0
  33. package/dist/bridge/attachmentWriter.js +24 -1
  34. package/dist/bridge/bundleInstaller.d.ts +3 -0
  35. package/dist/bridge/bundleInstaller.js +22 -8
  36. package/dist/bridge/claudeTranscriptReader.d.ts +7 -1
  37. package/dist/bridge/claudeTranscriptReader.js +12 -2
  38. package/dist/bridge/contextFile.d.ts +20 -0
  39. package/dist/bridge/contextFile.js +30 -0
  40. package/dist/bridge/contextWriter.d.ts +10 -1
  41. package/dist/bridge/contextWriter.js +29 -9
  42. package/dist/bridge/crewTranslator.d.ts +41 -0
  43. package/dist/bridge/crewTranslator.js +129 -0
  44. package/dist/bridge/launchSupervisor.d.ts +4 -0
  45. package/dist/bridge/opencodeBinary.d.ts +47 -0
  46. package/dist/bridge/opencodeBinary.js +161 -0
  47. package/dist/bridge/opencodeConfig.d.ts +43 -0
  48. package/dist/bridge/opencodeConfig.js +109 -0
  49. package/dist/bridge/opencodePlugin.d.ts +13 -0
  50. package/dist/bridge/opencodePlugin.js +54 -0
  51. package/dist/bridge/opencodeTranscriptReader.d.ts +24 -0
  52. package/dist/bridge/opencodeTranscriptReader.js +178 -0
  53. package/dist/bridge/relayClient.d.ts +34 -1
  54. package/dist/bridge/relayClient.js +33 -8
  55. package/dist/bridge/screenshotAttach.d.ts +2 -0
  56. package/dist/bridge/screenshotAttach.js +3 -1
  57. package/dist/bridge/server.d.ts +15 -0
  58. package/dist/bridge/server.js +21 -3
  59. package/dist/bridge/sessionManager.d.ts +5 -0
  60. package/dist/bridge/sessionManager.js +21 -1
  61. package/dist/commands/bridge.js +76 -9
  62. package/hooks/opencode-plugin.mjs +186 -0
  63. package/package.json +1 -1
@@ -0,0 +1,161 @@
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.OPENCODE_WAIT_SCRIPT = exports.managedBin = exports.OPENCODE_INSTALL_TIMEOUT_MS = exports.OPENCODE_PACKAGE = void 0;
37
+ exports.managedPrefix = managedPrefix;
38
+ exports.opencodeInstallFailureLines = opencodeInstallFailureLines;
39
+ exports.createOpencodeProvisioner = createOpencodeProvisioner;
40
+ /**
41
+ * oc-25 — the bridge provides OpenCode itself (ADR-0013 local-lane parity).
42
+ *
43
+ * A user must never install OpenCode by hand. Binary choice, in order:
44
+ * 1. the bridge-managed pinned copy (~/.wen-market/opencode/<pinned>), if there;
45
+ * 2. else a PATH `opencode` at or above the minimum;
46
+ * 3. else install the pinned version into the managed prefix (lazy: only on
47
+ * the first OpenCode launch, so Claude-only users never download it).
48
+ *
49
+ * The launch always carries an ABSOLUTE binary path. While an install runs the
50
+ * PTY runs a tiny `sh` waiter (progress line, then exec) — the pane shows
51
+ * "Setting up OpenCode" instead of a blank wait or a bare shell. Concurrent
52
+ * opens share ONE install (in-flight promise). The install must NOT inherit
53
+ * OPENCODE_QUIET_ENV's npm_config_offline=true, so it runs with a scrubbed env.
54
+ */
55
+ const path = __importStar(require("path"));
56
+ const opencodeConfig_1 = require("./opencodeConfig");
57
+ exports.OPENCODE_PACKAGE = 'opencode-ai';
58
+ exports.OPENCODE_INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
59
+ function managedPrefix(baseDir, version = opencodeConfig_1.OPENCODE_PINNED_VERSION) {
60
+ return path.join(baseDir, version);
61
+ }
62
+ const managedBin = (prefix) => path.join(prefix, 'node_modules', '.bin', 'opencode');
63
+ exports.managedBin = managedBin;
64
+ const readyMarker = (prefix) => path.join(prefix, '.ready');
65
+ const failMarker = (prefix) => path.join(prefix, '.install-failed');
66
+ /** Short lines on purpose: a narrow pane wraps long ones badly. */
67
+ function opencodeInstallFailureLines(reason) {
68
+ const why = reason.replace(/\s+/g, ' ').trim().slice(0, 60);
69
+ return [
70
+ 'Could not set up OpenCode.',
71
+ ...(why ? [why] : []),
72
+ 'Install it by hand:',
73
+ `npm i -g ${exports.OPENCODE_PACKAGE}@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`,
74
+ 'Then reopen this pane.',
75
+ ];
76
+ }
77
+ /** POSIX sh waiter: progress now, then exec once the bridge marks the install ready (or print the failure). */
78
+ exports.OPENCODE_WAIT_SCRIPT = [
79
+ 'ready=$1; fail=$2; shift 2',
80
+ "printf '\\r\\n\\033[2mSetting up OpenCode (one-time, ~30s)...\\033[0m\\r\\n'",
81
+ 'while [ ! -e "$ready" ]; do',
82
+ ' if [ -e "$fail" ]; then',
83
+ " printf '\\r\\n'",
84
+ " while IFS= read -r l; do printf '\\033[31m%s\\033[0m\\r\\n' \"$l\"; done < \"$fail\"",
85
+ ' exit 1',
86
+ ' fi',
87
+ ' sleep 1',
88
+ 'done',
89
+ 'exec "$@"',
90
+ ].join('\n');
91
+ function createOpencodeProvisioner(baseDir, deps) {
92
+ const prefix = managedPrefix(baseDir);
93
+ const bin = (0, exports.managedBin)(prefix);
94
+ let inflight = null;
95
+ function install() {
96
+ if (inflight)
97
+ return inflight;
98
+ // Clear the previous verdict synchronously so a waiter spawned right after never reads a stale one.
99
+ try {
100
+ deps.rm(failMarker(prefix));
101
+ }
102
+ catch { /* none */ }
103
+ const env = { ...deps.env };
104
+ for (const k of Object.keys(env))
105
+ if (k.toLowerCase() === 'npm_config_offline')
106
+ delete env[k];
107
+ const fail = (reason) => {
108
+ try {
109
+ deps.writeFile(failMarker(prefix), opencodeInstallFailureLines(reason).join('\n') + '\n');
110
+ }
111
+ catch { /* best effort */ }
112
+ };
113
+ const run = (async () => {
114
+ try {
115
+ deps.mkdirp(prefix);
116
+ const pkg = path.join(prefix, 'package.json');
117
+ if (!deps.exists(pkg))
118
+ deps.writeFile(pkg, JSON.stringify({ name: 'wen-market-opencode', private: true }) + '\n');
119
+ const res = await deps.runNpm(['install', '--prefix', prefix, `${exports.OPENCODE_PACKAGE}@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`, '--no-audit', '--no-fund'], env);
120
+ if (res.code !== 0)
121
+ return fail(`npm exited ${res.code}`);
122
+ if (!deps.exists(bin) || !(0, opencodeConfig_1.opencodeVersionOk)(deps.versionOf(bin)))
123
+ return fail('installed binary will not run');
124
+ deps.writeFile(readyMarker(prefix), opencodeConfig_1.OPENCODE_PINNED_VERSION);
125
+ }
126
+ catch (err) {
127
+ fail(err instanceof Error ? err.message : String(err));
128
+ }
129
+ })();
130
+ inflight = run;
131
+ void run.then(() => { if (inflight === run)
132
+ inflight = null; });
133
+ return run;
134
+ }
135
+ return {
136
+ managedBin: bin,
137
+ /** Resolve (or start provisioning) and return the launch to spawn. Synchronous. */
138
+ prepare(launch) {
139
+ const withBin = (b) => ({ ...launch, command: b });
140
+ if (deps.exists(bin) && deps.exists(readyMarker(prefix)))
141
+ return { ok: true, launch: withBin(bin) };
142
+ const onPath = deps.pathOpencode();
143
+ if (onPath && (0, opencodeConfig_1.opencodeVersionOk)(onPath.version))
144
+ return { ok: true, launch: withBin(onPath.path) };
145
+ if (deps.platform === 'win32') {
146
+ return { ok: false, message: opencodeInstallFailureLines('automatic setup is not on Windows yet').join('\r\n') };
147
+ }
148
+ void install();
149
+ return {
150
+ ok: true,
151
+ launch: {
152
+ ...launch,
153
+ command: '/bin/sh',
154
+ args: ['-c', exports.OPENCODE_WAIT_SCRIPT, 'opencode-wait', readyMarker(prefix), failMarker(prefix), bin, ...launch.args],
155
+ },
156
+ };
157
+ },
158
+ /** Test seam: resolves when the in-flight install (if any) settles. */
159
+ settled: () => inflight ?? Promise.resolve(),
160
+ };
161
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * oc-08 — the CANONICAL OpenCode launch config (docs/prd/opencode-engine.md,
3
+ * ADR-0013, shape verified in oc-04). Pure: no I/O, no process.env.
4
+ *
5
+ * The hosted lane (infra/terminal-image/opencode/build-config.js, oc-05) must
6
+ * emit the SAME JSON for the same inputs; a parity test on each side should pin
7
+ * it. The OpenRouter key is NEVER an input here: the only credential OpenCode
8
+ * ever holds is the per-agent gateway token, and it is passed by reference
9
+ * (`{env:HIVEOP_GATEWAY_TOKEN}`) so it never appears inside the JSON.
10
+ */
11
+ /** Oldest OpenCode the local lane accepts. 1.18.35 is the version oc-04 verified
12
+ * (`permission.asked` / `session.idle` events, `-s <id>` resume, quiet env). */
13
+ export declare const OPENCODE_PINNED_VERSION = "1.18.35";
14
+ /** oc-25: the version the bridge installs for itself IS the minimum it accepts from PATH. */
15
+ export declare const OPENCODE_MIN_VERSION = "1.18.35";
16
+ export declare const OPENCODE_INSTALL_HINT = "Install OpenCode: npm i -g opencode-ai@>=1.18.35";
17
+ /** Every outbound call OpenCode makes at startup besides the gateway (oc-04 s5). */
18
+ export declare const OPENCODE_QUIET_ENV: Readonly<Record<string, string>>;
19
+ export declare const OPENCODE_PROVIDER_ID = "hiveop";
20
+ export declare const OPENCODE_GATEWAY_TOKEN_ENV = "HIVEOP_GATEWAY_TOKEN";
21
+ export interface OpencodeConfigInput {
22
+ /** Chosen model id (the gateway's catalogue id, e.g. an OpenRouter slug). */
23
+ model: string;
24
+ /** Full OpenAI-compatible base URL, i.e. `<api>/api/v1/opencode/v1`. */
25
+ gatewayBaseUrl: string;
26
+ /** permission "allow" when true (autonomous agent), OpenCode's default otherwise. */
27
+ autonomous?: boolean;
28
+ /** Add the wenrwa-marketplace MCP server (token read from env, not inlined). */
29
+ withMcp?: boolean;
30
+ /** argv of the MCP server; defaults to npx. */
31
+ mcpCommand?: string[];
32
+ /** Extra env var NAMES the MCP server needs, passed by reference (`{env:NAME}`), never inlined. */
33
+ mcpEnvPassthrough?: string[];
34
+ /** Project-relative files OpenCode loads as extra instructions. The local lane passes the
35
+ * bridge's `.hiveop/context.md` (oc-23): OpenCode has no SessionStart hook to inject it. */
36
+ instructions?: string[];
37
+ }
38
+ export declare function buildOpencodeConfig(input: OpencodeConfigInput): Record<string, unknown>;
39
+ /** OpenCode session ids are generated by OpenCode (`ses_...`); anything else
40
+ * (e.g. the uuid oc-01 pre-mints on the row) would fail "Session not found". */
41
+ export declare const isCapturedOpencodeSessionId: (id: unknown) => id is string;
42
+ /** True iff `versionOutput` (from `opencode --version`) is >= the minimum. */
43
+ export declare function opencodeVersionOk(versionOutput: string | null | undefined, min?: string): boolean;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * oc-08 — the CANONICAL OpenCode launch config (docs/prd/opencode-engine.md,
4
+ * ADR-0013, shape verified in oc-04). Pure: no I/O, no process.env.
5
+ *
6
+ * The hosted lane (infra/terminal-image/opencode/build-config.js, oc-05) must
7
+ * emit the SAME JSON for the same inputs; a parity test on each side should pin
8
+ * it. The OpenRouter key is NEVER an input here: the only credential OpenCode
9
+ * ever holds is the per-agent gateway token, and it is passed by reference
10
+ * (`{env:HIVEOP_GATEWAY_TOKEN}`) so it never appears inside the JSON.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.isCapturedOpencodeSessionId = exports.OPENCODE_GATEWAY_TOKEN_ENV = exports.OPENCODE_PROVIDER_ID = exports.OPENCODE_QUIET_ENV = exports.OPENCODE_INSTALL_HINT = exports.OPENCODE_MIN_VERSION = exports.OPENCODE_PINNED_VERSION = void 0;
14
+ exports.buildOpencodeConfig = buildOpencodeConfig;
15
+ exports.opencodeVersionOk = opencodeVersionOk;
16
+ /** Oldest OpenCode the local lane accepts. 1.18.35 is the version oc-04 verified
17
+ * (`permission.asked` / `session.idle` events, `-s <id>` resume, quiet env). */
18
+ exports.OPENCODE_PINNED_VERSION = '1.18.35';
19
+ /** oc-25: the version the bridge installs for itself IS the minimum it accepts from PATH. */
20
+ exports.OPENCODE_MIN_VERSION = exports.OPENCODE_PINNED_VERSION;
21
+ exports.OPENCODE_INSTALL_HINT = `Install OpenCode: npm i -g opencode-ai@>=${exports.OPENCODE_MIN_VERSION}`;
22
+ /** Every outbound call OpenCode makes at startup besides the gateway (oc-04 s5). */
23
+ exports.OPENCODE_QUIET_ENV = Object.freeze({
24
+ OPENCODE_DISABLE_MODELS_FETCH: '1',
25
+ OPENCODE_DISABLE_AUTOUPDATE: '1',
26
+ OPENCODE_DISABLE_SHARE: '1',
27
+ OPENCODE_DISABLE_LSP_DOWNLOAD: '1',
28
+ // oc-17: the HiveOp plugin sets the pane title itself (a bare "OpenCode" would clobber the auto-title).
29
+ OPENCODE_DISABLE_TERMINAL_TITLE: '1',
30
+ npm_config_offline: 'true',
31
+ });
32
+ exports.OPENCODE_PROVIDER_ID = 'hiveop';
33
+ exports.OPENCODE_GATEWAY_TOKEN_ENV = 'HIVEOP_GATEWAY_TOKEN';
34
+ function buildOpencodeConfig(input) {
35
+ const { model } = input;
36
+ const config = {
37
+ $schema: 'https://opencode.ai/config.json',
38
+ autoupdate: false,
39
+ share: 'disabled',
40
+ enabled_providers: [exports.OPENCODE_PROVIDER_ID],
41
+ model: `${exports.OPENCODE_PROVIDER_ID}/${model}`,
42
+ // OpenCode makes a second, billed "title" call on the small model.
43
+ small_model: `${exports.OPENCODE_PROVIDER_ID}/${model}`,
44
+ provider: {
45
+ [exports.OPENCODE_PROVIDER_ID]: {
46
+ npm: '@ai-sdk/openai-compatible',
47
+ name: 'HiveOp',
48
+ options: {
49
+ baseURL: input.gatewayBaseUrl,
50
+ apiKey: `{env:${exports.OPENCODE_GATEWAY_TOKEN_ENV}}`,
51
+ },
52
+ whitelist: [model],
53
+ models: {
54
+ [model]: {
55
+ name: model,
56
+ attachment: true,
57
+ tool_call: true,
58
+ modalities: { input: ['text', 'image'], output: ['text'] },
59
+ limit: { context: 200000, output: 8192 },
60
+ },
61
+ },
62
+ },
63
+ },
64
+ };
65
+ if (input.instructions && input.instructions.length > 0)
66
+ config.instructions = [...input.instructions];
67
+ if (input.autonomous)
68
+ config.permission = 'allow';
69
+ if (input.withMcp) {
70
+ // oc-07: OPENCODE_QUIET_ENV sets npm_config_offline=true for the pane; a bare
71
+ // npx MCP server inherits it and cannot fetch itself on a cold npm cache
72
+ // ("MCP error -32000: Connection closed"). Override it for npx only.
73
+ const cmd = input.mcpCommand ?? ['npx', '@wenrwa/marketplace-mcp'];
74
+ const usesNpx = /(^|[\\/])npx(\.cmd)?$/.test(cmd[0] ?? '');
75
+ config.mcp = {
76
+ 'wenrwa-marketplace': {
77
+ type: 'local',
78
+ command: cmd,
79
+ environment: {
80
+ WENRWA_CONNECT_TOKEN: '{env:WENRWA_CONNECT_TOKEN}',
81
+ ...Object.fromEntries((input.mcpEnvPassthrough ?? []).map((n) => [n, `{env:${n}}`])),
82
+ ...(usesNpx ? { npm_config_offline: 'false' } : {}),
83
+ },
84
+ enabled: true,
85
+ },
86
+ };
87
+ }
88
+ return config;
89
+ }
90
+ /** OpenCode session ids are generated by OpenCode (`ses_...`); anything else
91
+ * (e.g. the uuid oc-01 pre-mints on the row) would fail "Session not found". */
92
+ const isCapturedOpencodeSessionId = (id) => typeof id === 'string' && /^ses_[A-Za-z0-9]+$/.test(id);
93
+ exports.isCapturedOpencodeSessionId = isCapturedOpencodeSessionId;
94
+ const parseVersion = (v) => {
95
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(v);
96
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
97
+ };
98
+ /** True iff `versionOutput` (from `opencode --version`) is >= the minimum. */
99
+ function opencodeVersionOk(versionOutput, min = exports.OPENCODE_MIN_VERSION) {
100
+ const got = parseVersion(versionOutput ?? '');
101
+ const want = parseVersion(min);
102
+ if (!got)
103
+ return false;
104
+ for (let i = 0; i < 3; i++) {
105
+ if (got[i] !== want[i])
106
+ return got[i] > want[i];
107
+ }
108
+ return true;
109
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * oc-06 — the HiveOp OpenCode activity plugin (cli/hooks/opencode-plugin.mjs,
3
+ * shipped in the npm package beside the Claude hooks). The local launch
4
+ * resolver (oc-08) merges `buildOpenCodePluginConfig()` into the agent's
5
+ * OPENCODE_CONFIG_CONTENT; OpenCode loads plugins from `plugin: [file URL]`
6
+ * (the path the oc-04 spike verified). Hosted uses the copy under
7
+ * /opt/hiveop/opencode/ via the same key.
8
+ */
9
+ export declare function resolveOpenCodePluginPath(hooksDir?: string): string;
10
+ /** Config fragment to merge into OpenCode's config. pathToFileURL encodes spaces. */
11
+ export declare function buildOpenCodePluginConfig(pluginPath?: string): {
12
+ plugin: string[];
13
+ };
@@ -0,0 +1,54 @@
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.resolveOpenCodePluginPath = resolveOpenCodePluginPath;
37
+ exports.buildOpenCodePluginConfig = buildOpenCodePluginConfig;
38
+ const path = __importStar(require("path"));
39
+ const url_1 = require("url");
40
+ /**
41
+ * oc-06 — the HiveOp OpenCode activity plugin (cli/hooks/opencode-plugin.mjs,
42
+ * shipped in the npm package beside the Claude hooks). The local launch
43
+ * resolver (oc-08) merges `buildOpenCodePluginConfig()` into the agent's
44
+ * OPENCODE_CONFIG_CONTENT; OpenCode loads plugins from `plugin: [file URL]`
45
+ * (the path the oc-04 spike verified). Hosted uses the copy under
46
+ * /opt/hiveop/opencode/ via the same key.
47
+ */
48
+ function resolveOpenCodePluginPath(hooksDir = path.join(__dirname, '..', '..', 'hooks')) {
49
+ return path.join(hooksDir, 'opencode-plugin.mjs');
50
+ }
51
+ /** Config fragment to merge into OpenCode's config. pathToFileURL encodes spaces. */
52
+ function buildOpenCodePluginConfig(pluginPath = resolveOpenCodePluginPath()) {
53
+ return { plugin: [(0, url_1.pathToFileURL)(pluginPath).href] };
54
+ }
@@ -0,0 +1,24 @@
1
+ export type SqliteRow = Record<string, unknown>;
2
+ /** Run one read-only SELECT. Throws 'sqlite-unavailable' when no driver exists. */
3
+ export type SqliteQuery = (dbPath: string, sql: string) => SqliteRow[];
4
+ export type OpencodeTranscriptResult = {
5
+ ok: true;
6
+ content: string;
7
+ } | {
8
+ ok: false;
9
+ reason: 'not-found' | 'too-large' | 'transcript-unavailable';
10
+ };
11
+ export interface OpencodeTranscriptDeps {
12
+ /** Root holding one dir per agent (default $WENRWA_OPENCODE_DATA_ROOT or /data/.opencode). */
13
+ dataRoot?: string;
14
+ existsSync?: (p: string) => boolean;
15
+ query?: SqliteQuery;
16
+ }
17
+ /** Where the hosted launch put this agent's OpenCode database. */
18
+ export declare function opencodeDbPath(agentId: string, deps?: OpencodeTranscriptDeps): string;
19
+ /** Default driver: node:sqlite (Node >= 22.5, the image runs 24), else the sqlite3 CLI. */
20
+ export declare const defaultSqliteQuery: SqliteQuery;
21
+ /** Pure: OpenCode message/part rows -> Claude-shaped jsonl lines. */
22
+ export declare function opencodeRowsToJsonl(sessionId: string, messages: SqliteRow[], parts: SqliteRow[]): string;
23
+ /** Read ONE agent's OpenCode transcript. `sessionId` pins a session; absent -> the newest root session. */
24
+ export declare function readOpencodeTranscript(agentId: string, sessionId: string | undefined, deps?: OpencodeTranscriptDeps): OpencodeTranscriptResult;
@@ -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,23 @@
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';
20
+ import type { PreparedLaunch } from './opencodeBinary';
1
21
  import { SessionManager } from './sessionManager';
2
22
  import { MemInfo } from './containerMemory';
3
23
  import { BrowserDriver } from './browserDriver';
@@ -163,6 +183,8 @@ export interface RelayClientOpts {
163
183
  workspaceId?: string;
164
184
  sessionId?: string;
165
185
  autonomous?: boolean;
186
+ opencode?: OpencodeLaunchInput;
187
+ connectToken?: string;
166
188
  }) => {
167
189
  command: string;
168
190
  args: string[];
@@ -179,6 +201,12 @@ export interface RelayClientOpts {
179
201
  * error frame the conductor can report.
180
202
  */
181
203
  commandExists?: (command: string) => boolean;
204
+ /** oc-25 — see BridgeServerOpts#prepareOpencodeLaunch. */
205
+ prepareOpencodeLaunch?: (launch: {
206
+ command: string;
207
+ args: string[];
208
+ env: Record<string, string>;
209
+ }) => PreparedLaunch;
182
210
  /**
183
211
  * fcct-27 — RELAY-OWNED SESSIONS ONLY. The consent boundary, made structural.
184
212
  *
@@ -214,6 +242,7 @@ export interface RelayClientOpts {
214
242
  installBundle?: (bundle: {
215
243
  workspaceId: string;
216
244
  files: Record<string, string>;
245
+ engine?: string;
217
246
  }) => {
218
247
  written: string[];
219
248
  skipped: Array<{
@@ -237,10 +266,12 @@ export interface RelayClientOpts {
237
266
  */
238
267
  refreshContextPointers?: (args: {
239
268
  workspaceId: string;
269
+ engine?: string;
240
270
  }) => void | Promise<void>;
241
271
  writeContext?: (manifest: {
242
272
  workspaceId: string;
243
273
  content: string;
274
+ engine?: string;
244
275
  }) => {
245
276
  action: string;
246
277
  claudeMdPath: string | null;
@@ -307,12 +338,14 @@ export interface RelayClientOpts {
307
338
  readTranscript?: (args: {
308
339
  workspaceId: string;
309
340
  agentId: string;
341
+ engine?: 'claude_code' | 'opencode';
342
+ opencodeSessionId?: string;
310
343
  }) => {
311
344
  ok: true;
312
345
  content: string;
313
346
  } | {
314
347
  ok: false;
315
- reason: 'not-found' | 'invalid-agent-id' | 'too-large';
348
+ reason: 'not-found' | 'invalid-agent-id' | 'too-large' | 'transcript-unavailable';
316
349
  };
317
350
  /** Injectable for tests: defaults to `new WebSocket(url)` from `ws`. */
318
351
  wsFactory?: (url: string) => WebSocketLike;