@wenrwa/marketplace-cli 0.7.7 → 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-attach-screenshot.test.d.ts +1 -0
- package/dist/__tests__/bridge-attach-screenshot.test.js +250 -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 +35 -0
- package/dist/bridge/screenshotAttach.js +173 -0
- package/dist/bridge/server.d.ts +9 -0
- package/dist/bridge/server.js +19 -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,107 @@
|
|
|
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 = 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_MIN_VERSION = '1.18.35';
|
|
19
|
+
exports.OPENCODE_INSTALL_HINT = `Install OpenCode: npm i -g opencode-ai@>=${exports.OPENCODE_MIN_VERSION}`;
|
|
20
|
+
/** Every outbound call OpenCode makes at startup besides the gateway (oc-04 s5). */
|
|
21
|
+
exports.OPENCODE_QUIET_ENV = Object.freeze({
|
|
22
|
+
OPENCODE_DISABLE_MODELS_FETCH: '1',
|
|
23
|
+
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
|
24
|
+
OPENCODE_DISABLE_SHARE: '1',
|
|
25
|
+
OPENCODE_DISABLE_LSP_DOWNLOAD: '1',
|
|
26
|
+
// oc-17: the HiveOp plugin sets the pane title itself (a bare "OpenCode" would clobber the auto-title).
|
|
27
|
+
OPENCODE_DISABLE_TERMINAL_TITLE: '1',
|
|
28
|
+
npm_config_offline: 'true',
|
|
29
|
+
});
|
|
30
|
+
exports.OPENCODE_PROVIDER_ID = 'hiveop';
|
|
31
|
+
exports.OPENCODE_GATEWAY_TOKEN_ENV = 'HIVEOP_GATEWAY_TOKEN';
|
|
32
|
+
function buildOpencodeConfig(input) {
|
|
33
|
+
const { model } = input;
|
|
34
|
+
const config = {
|
|
35
|
+
$schema: 'https://opencode.ai/config.json',
|
|
36
|
+
autoupdate: false,
|
|
37
|
+
share: 'disabled',
|
|
38
|
+
enabled_providers: [exports.OPENCODE_PROVIDER_ID],
|
|
39
|
+
model: `${exports.OPENCODE_PROVIDER_ID}/${model}`,
|
|
40
|
+
// OpenCode makes a second, billed "title" call on the small model.
|
|
41
|
+
small_model: `${exports.OPENCODE_PROVIDER_ID}/${model}`,
|
|
42
|
+
provider: {
|
|
43
|
+
[exports.OPENCODE_PROVIDER_ID]: {
|
|
44
|
+
npm: '@ai-sdk/openai-compatible',
|
|
45
|
+
name: 'HiveOp',
|
|
46
|
+
options: {
|
|
47
|
+
baseURL: input.gatewayBaseUrl,
|
|
48
|
+
apiKey: `{env:${exports.OPENCODE_GATEWAY_TOKEN_ENV}}`,
|
|
49
|
+
},
|
|
50
|
+
whitelist: [model],
|
|
51
|
+
models: {
|
|
52
|
+
[model]: {
|
|
53
|
+
name: model,
|
|
54
|
+
attachment: true,
|
|
55
|
+
tool_call: true,
|
|
56
|
+
modalities: { input: ['text', 'image'], output: ['text'] },
|
|
57
|
+
limit: { context: 200000, output: 8192 },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
if (input.instructions && input.instructions.length > 0)
|
|
64
|
+
config.instructions = [...input.instructions];
|
|
65
|
+
if (input.autonomous)
|
|
66
|
+
config.permission = 'allow';
|
|
67
|
+
if (input.withMcp) {
|
|
68
|
+
// oc-07: OPENCODE_QUIET_ENV sets npm_config_offline=true for the pane; a bare
|
|
69
|
+
// npx MCP server inherits it and cannot fetch itself on a cold npm cache
|
|
70
|
+
// ("MCP error -32000: Connection closed"). Override it for npx only.
|
|
71
|
+
const cmd = input.mcpCommand ?? ['npx', '@wenrwa/marketplace-mcp'];
|
|
72
|
+
const usesNpx = /(^|[\\/])npx(\.cmd)?$/.test(cmd[0] ?? '');
|
|
73
|
+
config.mcp = {
|
|
74
|
+
'wenrwa-marketplace': {
|
|
75
|
+
type: 'local',
|
|
76
|
+
command: cmd,
|
|
77
|
+
environment: {
|
|
78
|
+
WENRWA_CONNECT_TOKEN: '{env:WENRWA_CONNECT_TOKEN}',
|
|
79
|
+
...Object.fromEntries((input.mcpEnvPassthrough ?? []).map((n) => [n, `{env:${n}}`])),
|
|
80
|
+
...(usesNpx ? { npm_config_offline: 'false' } : {}),
|
|
81
|
+
},
|
|
82
|
+
enabled: true,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return config;
|
|
87
|
+
}
|
|
88
|
+
/** OpenCode session ids are generated by OpenCode (`ses_...`); anything else
|
|
89
|
+
* (e.g. the uuid oc-01 pre-mints on the row) would fail "Session not found". */
|
|
90
|
+
const isCapturedOpencodeSessionId = (id) => typeof id === 'string' && /^ses_[A-Za-z0-9]+$/.test(id);
|
|
91
|
+
exports.isCapturedOpencodeSessionId = isCapturedOpencodeSessionId;
|
|
92
|
+
const parseVersion = (v) => {
|
|
93
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
94
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
95
|
+
};
|
|
96
|
+
/** True iff `versionOutput` (from `opencode --version`) is >= the minimum. */
|
|
97
|
+
function opencodeVersionOk(versionOutput, min = exports.OPENCODE_MIN_VERSION) {
|
|
98
|
+
const got = parseVersion(versionOutput ?? '');
|
|
99
|
+
const want = parseVersion(min);
|
|
100
|
+
if (!got)
|
|
101
|
+
return false;
|
|
102
|
+
for (let i = 0; i < 3; i++) {
|
|
103
|
+
if (got[i] !== want[i])
|
|
104
|
+
return got[i] > want[i];
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
@@ -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,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 */ }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AttachmentDeps } from './attachmentWriter';
|
|
2
|
+
export declare const SCREENSHOT_POLL_MS = 6000;
|
|
3
|
+
export declare const SCREENSHOT_POLL_INTERVAL_MS = 250;
|
|
4
|
+
export type ScreenshotReason = 'not-supported' | 'bad-name' | 'not-found' | 'not-image' | 'too-large' | 'no-session' | 'error';
|
|
5
|
+
export interface ScreenshotDeps {
|
|
6
|
+
platform: NodeJS.Platform;
|
|
7
|
+
homedir: () => string;
|
|
8
|
+
/** Raw output of `defaults read com.apple.screencapture location`, or null when unset/failed. */
|
|
9
|
+
readScreencaptureLocation: () => Promise<string | null>;
|
|
10
|
+
/** lstat-style: null when absent. Never follows a symlink. */
|
|
11
|
+
stat: (p: string) => {
|
|
12
|
+
isFile: boolean;
|
|
13
|
+
size: number;
|
|
14
|
+
} | null;
|
|
15
|
+
readFile: (p: string) => Buffer;
|
|
16
|
+
sleep: (ms: number) => Promise<void>;
|
|
17
|
+
now: () => number;
|
|
18
|
+
attachment: AttachmentDeps;
|
|
19
|
+
}
|
|
20
|
+
export declare const realScreenshotDeps: ScreenshotDeps;
|
|
21
|
+
/** `~` expansion + absolute check; anything odd falls back to ~/Desktop (macOS's own default). */
|
|
22
|
+
export declare function resolveScreenshotFolder(raw: string | null, home: string): string;
|
|
23
|
+
/** A plain image basename: no path separators, no traversal, no NUL, image extension. */
|
|
24
|
+
export declare function isSafeScreenshotName(name: unknown): name is string;
|
|
25
|
+
/** The `attach-screenshot` frame → `file-attached` reply. */
|
|
26
|
+
export declare function handleScreenshotFrame(frame: {
|
|
27
|
+
rid?: unknown;
|
|
28
|
+
sessionId?: unknown;
|
|
29
|
+
name?: unknown;
|
|
30
|
+
}, sessions: {
|
|
31
|
+
list(): string[];
|
|
32
|
+
cwdOf(id: string): string | undefined;
|
|
33
|
+
engineOf?(id: string): string | undefined;
|
|
34
|
+
writeTo?(id: string, data: string): boolean;
|
|
35
|
+
}, deps?: ScreenshotDeps): Promise<Record<string, unknown>>;
|