@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,173 @@
|
|
|
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.realScreenshotDeps = exports.SCREENSHOT_POLL_INTERVAL_MS = exports.SCREENSHOT_POLL_MS = void 0;
|
|
37
|
+
exports.resolveScreenshotFolder = resolveScreenshotFolder;
|
|
38
|
+
exports.isSafeScreenshotName = isSafeScreenshotName;
|
|
39
|
+
exports.handleScreenshotFrame = handleScreenshotFrame;
|
|
40
|
+
/**
|
|
41
|
+
* Screenshot drop — LOCAL bridge only.
|
|
42
|
+
*
|
|
43
|
+
* Dragging the macOS floating screenshot thumbnail onto a browser hands the
|
|
44
|
+
* page a File backed by a *file promise*: the browser cannot read its bytes
|
|
45
|
+
* (NotReadableError), and macOS writes the real PNG into the screenshot
|
|
46
|
+
* folder only after the drop. The browser still knows the file's NAME, so it
|
|
47
|
+
* sends `attach-screenshot {rid, sessionId, name}` and this module:
|
|
48
|
+
*
|
|
49
|
+
* 1. resolves the screenshot folder (`defaults read com.apple.screencapture
|
|
50
|
+
* location`, `~` expanded; default ~/Desktop; non-macOS → not-supported),
|
|
51
|
+
* 2. accepts `name` only as a plain image basename (no separators, no `..`,
|
|
52
|
+
* png/jpg/jpeg/gif/webp),
|
|
53
|
+
* 3. polls for `<folder>/<name>` for up to ~6s (a regular file, ≤ 10 MB,
|
|
54
|
+
* whose bytes sniff as an image — a half-written file keeps polling),
|
|
55
|
+
* 4. saves it through the SAME writer as `attach-file` (attachmentWriter).
|
|
56
|
+
*
|
|
57
|
+
* Never wired into the relay client: a hosted container cannot see the
|
|
58
|
+
* user's Desktop, and must not be asked to read its own disk by name.
|
|
59
|
+
*/
|
|
60
|
+
const child_process_1 = require("child_process");
|
|
61
|
+
const fs = __importStar(require("fs"));
|
|
62
|
+
const os = __importStar(require("os"));
|
|
63
|
+
const path = __importStar(require("path"));
|
|
64
|
+
const attachmentWriter_1 = require("./attachmentWriter");
|
|
65
|
+
exports.SCREENSHOT_POLL_MS = 6000;
|
|
66
|
+
exports.SCREENSHOT_POLL_INTERVAL_MS = 250;
|
|
67
|
+
const SCREENSHOT_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp']);
|
|
68
|
+
exports.realScreenshotDeps = {
|
|
69
|
+
platform: process.platform,
|
|
70
|
+
homedir: () => os.homedir(),
|
|
71
|
+
readScreencaptureLocation: () => new Promise((resolve) => {
|
|
72
|
+
(0, child_process_1.execFile)('defaults', ['read', 'com.apple.screencapture', 'location'], { timeout: 2000 }, (err, stdout) => {
|
|
73
|
+
resolve(err ? null : String(stdout));
|
|
74
|
+
});
|
|
75
|
+
}),
|
|
76
|
+
stat: (p) => {
|
|
77
|
+
try {
|
|
78
|
+
const s = fs.lstatSync(p);
|
|
79
|
+
return { isFile: s.isFile(), size: s.size };
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
readFile: (p) => fs.readFileSync(p),
|
|
86
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
87
|
+
now: () => Date.now(),
|
|
88
|
+
attachment: attachmentWriter_1.realAttachmentDeps,
|
|
89
|
+
};
|
|
90
|
+
/** `~` expansion + absolute check; anything odd falls back to ~/Desktop (macOS's own default). */
|
|
91
|
+
function resolveScreenshotFolder(raw, home) {
|
|
92
|
+
const fallback = path.join(home, 'Desktop');
|
|
93
|
+
const v = (raw ?? '').trim();
|
|
94
|
+
if (!v)
|
|
95
|
+
return fallback;
|
|
96
|
+
let expanded = v;
|
|
97
|
+
if (v === '~')
|
|
98
|
+
expanded = home;
|
|
99
|
+
else if (v.startsWith('~/'))
|
|
100
|
+
expanded = path.join(home, v.slice(2));
|
|
101
|
+
if (!path.isAbsolute(expanded))
|
|
102
|
+
return fallback;
|
|
103
|
+
return path.normalize(expanded);
|
|
104
|
+
}
|
|
105
|
+
/** A plain image basename: no path separators, no traversal, no NUL, image extension. */
|
|
106
|
+
function isSafeScreenshotName(name) {
|
|
107
|
+
if (typeof name !== 'string' || name.length === 0 || name.length > 255)
|
|
108
|
+
return false;
|
|
109
|
+
if (/[\\/\0]/.test(name))
|
|
110
|
+
return false;
|
|
111
|
+
if (name === '.' || name === '..' || name.includes('..'))
|
|
112
|
+
return false;
|
|
113
|
+
if (name.startsWith('.'))
|
|
114
|
+
return false;
|
|
115
|
+
const ext = /\.([A-Za-z0-9]+)$/.exec(name)?.[1]?.toLowerCase();
|
|
116
|
+
return !!ext && SCREENSHOT_EXTS.has(ext);
|
|
117
|
+
}
|
|
118
|
+
/** The `attach-screenshot` frame → `file-attached` reply. */
|
|
119
|
+
async function handleScreenshotFrame(frame, sessions, deps = exports.realScreenshotDeps) {
|
|
120
|
+
const head = {
|
|
121
|
+
t: 'file-attached',
|
|
122
|
+
rid: frame.rid,
|
|
123
|
+
...(typeof frame.sessionId === 'string' ? { sessionId: frame.sessionId } : {}),
|
|
124
|
+
};
|
|
125
|
+
const fail = (reason) => ({ ...head, ok: false, reason });
|
|
126
|
+
if (deps.platform !== 'darwin')
|
|
127
|
+
return fail('not-supported');
|
|
128
|
+
const root = (0, attachmentWriter_1.attachmentRootFor)(sessions, frame.sessionId);
|
|
129
|
+
if (!root)
|
|
130
|
+
return fail('no-session');
|
|
131
|
+
if (!isSafeScreenshotName(frame.name))
|
|
132
|
+
return fail('bad-name');
|
|
133
|
+
const name = frame.name;
|
|
134
|
+
try {
|
|
135
|
+
const folder = resolveScreenshotFolder(await deps.readScreencaptureLocation(), deps.homedir());
|
|
136
|
+
const file = path.join(folder, name);
|
|
137
|
+
// Belt and braces: the joined path must sit directly in the folder.
|
|
138
|
+
if (path.dirname(file) !== folder)
|
|
139
|
+
return fail('bad-name');
|
|
140
|
+
const deadline = deps.now() + exports.SCREENSHOT_POLL_MS;
|
|
141
|
+
let last = 'not-found';
|
|
142
|
+
for (;;) {
|
|
143
|
+
const st = deps.stat(file);
|
|
144
|
+
if (st && st.isFile) {
|
|
145
|
+
if (st.size > attachmentWriter_1.MAX_ATTACHMENT_BYTES)
|
|
146
|
+
return fail('too-large');
|
|
147
|
+
if (st.size > 0) {
|
|
148
|
+
const data = deps.readFile(file);
|
|
149
|
+
if (data.length > attachmentWriter_1.MAX_ATTACHMENT_BYTES)
|
|
150
|
+
return fail('too-large');
|
|
151
|
+
if ((0, attachmentWriter_1.sniffImageExt)(data)) {
|
|
152
|
+
const r = (0, attachmentWriter_1.writeAttachment)({ root, name, dataBase64: data.toString('base64') }, deps.attachment);
|
|
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);
|
|
156
|
+
}
|
|
157
|
+
// Present but not (yet) an image — maybe still being written.
|
|
158
|
+
last = 'not-image';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (st && !st.isFile) {
|
|
162
|
+
// A directory or symlink by that name is never a screenshot.
|
|
163
|
+
return fail('not-image');
|
|
164
|
+
}
|
|
165
|
+
if (deps.now() >= deadline)
|
|
166
|
+
return fail(last);
|
|
167
|
+
await deps.sleep(exports.SCREENSHOT_POLL_INTERVAL_MS);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return fail('error');
|
|
172
|
+
}
|
|
173
|
+
}
|
package/dist/bridge/server.d.ts
CHANGED
|
@@ -9,10 +9,13 @@ import { WriteGitignoreResult } from './writeGitignore';
|
|
|
9
9
|
import { LocalContextWriteResult } from './localContextWriter';
|
|
10
10
|
import { GitRun } from './changesReporter';
|
|
11
11
|
import { buildChangesReport as buildChangesReportReal } from './changesReport';
|
|
12
|
+
import { ScreenshotDeps } from './screenshotAttach';
|
|
12
13
|
export interface BridgeServerOpts {
|
|
13
14
|
sessionToken: string;
|
|
14
15
|
allowedOrigin: string;
|
|
15
16
|
sessions: SessionManager;
|
|
17
|
+
/** Screenshot drop (`attach-screenshot`) seams — tests only; defaults to the real macOS/fs. */
|
|
18
|
+
screenshotDeps?: ScreenshotDeps;
|
|
16
19
|
/** 0 (default) = ephemeral port. */
|
|
17
20
|
port?: number;
|
|
18
21
|
/** Workspace-scoped terminals: resolve a repo URL to a local checkout dir
|
|
@@ -226,6 +229,12 @@ export interface BridgeServerOpts {
|
|
|
226
229
|
* bridge.ts) — see the 'open' handler's agentId branch.
|
|
227
230
|
*/
|
|
228
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;
|
|
229
238
|
/**
|
|
230
239
|
* rival-10 — every file touch of a LOCAL WORKSPACE AGENT (an `open` with
|
|
231
240
|
* agentId + workspaceId), handed on for the ranked rival feed. The hook
|
package/dist/bridge/server.js
CHANGED
|
@@ -63,6 +63,7 @@ const sessionAnchor_1 = require("./sessionAnchor");
|
|
|
63
63
|
// the SAME activity records fcct-24's tail already parses below.
|
|
64
64
|
const sessionTouchTracker_1 = require("./sessionTouchTracker");
|
|
65
65
|
const attachmentWriter_1 = require("./attachmentWriter");
|
|
66
|
+
const screenshotAttach_1 = require("./screenshotAttach");
|
|
66
67
|
const PROTOCOL_VERSION = 1;
|
|
67
68
|
function send(ws, frame) {
|
|
68
69
|
if (ws.readyState === ws_1.WebSocket.OPEN)
|
|
@@ -392,6 +393,8 @@ function startBridgeServer(opts) {
|
|
|
392
393
|
...(opts.writeLocalFile ? ['local-file-write'] : []),
|
|
393
394
|
// Terminal attach — this bridge answers `attach-file` frames.
|
|
394
395
|
'terminal-attach',
|
|
396
|
+
// Screenshot drop — answers `attach-screenshot` (macOS only).
|
|
397
|
+
...((opts.screenshotDeps ?? screenshotAttach_1.realScreenshotDeps).platform === 'darwin' ? ['attach-screenshot'] : []),
|
|
395
398
|
],
|
|
396
399
|
});
|
|
397
400
|
return;
|
|
@@ -558,6 +561,8 @@ function startBridgeServer(opts) {
|
|
|
558
561
|
sessionId: rec.sessionId,
|
|
559
562
|
...(rec.filePaths ? { filePaths: rec.filePaths } : {}),
|
|
560
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 } : {}),
|
|
561
566
|
});
|
|
562
567
|
}
|
|
563
568
|
},
|
|
@@ -730,6 +735,10 @@ function startBridgeServer(opts) {
|
|
|
730
735
|
// arena client actually asked to open.
|
|
731
736
|
const launch = (0, agentLaunch_1.resolveAgentLaunch)({
|
|
732
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 } : {}),
|
|
733
742
|
...(cwd ? { sessionPin: { agentId, exists: agentSessionExists } } : {}),
|
|
734
743
|
...(opts.activityFile && opts.hiveOpSettingsPath
|
|
735
744
|
? { activity: { settingsPath: opts.hiveOpSettingsPath, sessionId: agentId, activityFile: opts.activityFile } }
|
|
@@ -762,7 +771,9 @@ function startBridgeServer(opts) {
|
|
|
762
771
|
send(ws, {
|
|
763
772
|
t: 'data',
|
|
764
773
|
sessionId,
|
|
765
|
-
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',
|
|
766
777
|
});
|
|
767
778
|
}).catch((err) => {
|
|
768
779
|
console.error('[bridge] session attach failed:', err);
|
|
@@ -938,6 +949,13 @@ function startBridgeServer(opts) {
|
|
|
938
949
|
// input. Shared handler with relayClient.ts (both lanes).
|
|
939
950
|
send(ws, (0, attachmentWriter_1.handleAttachFrame)(frame, opts.sessions));
|
|
940
951
|
break;
|
|
952
|
+
case 'attach-screenshot':
|
|
953
|
+
// Local lane ONLY (never relayClient.ts): reads the user's macOS
|
|
954
|
+
// screenshot folder by a validated basename, then the same writer.
|
|
955
|
+
void (0, screenshotAttach_1.handleScreenshotFrame)(frame, opts.sessions, opts.screenshotDeps ?? screenshotAttach_1.realScreenshotDeps)
|
|
956
|
+
.then((reply) => send(ws, reply))
|
|
957
|
+
.catch(() => send(ws, { t: 'file-attached', rid: frame.rid, sessionId: frame.sessionId, ok: false, reason: 'error' }));
|
|
958
|
+
break;
|
|
941
959
|
default:
|
|
942
960
|
send(ws, { t: 'error', message: 'unknown frame type' });
|
|
943
961
|
}
|
|
@@ -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 }),
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// HiveOp OpenCode activity plugin (oc-06). Self-contained ESM: no imports beyond node:*.
|
|
2
|
+
// Writes the SAME JSONL lines as the Claude Code hooks (hooks/post-tool-use.js, stop.js)
|
|
3
|
+
// to $HIVEOP_ACTIVITY_FILE, which the bridge already tails. Copies of this file live in
|
|
4
|
+
// infra/terminal-image/opencode/ (hosted) and cli/hooks/ (local); a test keeps them identical.
|
|
5
|
+
//
|
|
6
|
+
// Event -> line:
|
|
7
|
+
// tool.execute.before / .after -> kind 'tool' (working, thinking "<Verb> <file>")
|
|
8
|
+
// session.idle, session.status idle -> kind 'stop' (idle, thinking '')
|
|
9
|
+
// permission.asked, question.asked -> kind 'needs_you' (thinking 'Waiting for you')
|
|
10
|
+
// permission.replied -> kind 'tool', tool '' (working, thinking 'Working')
|
|
11
|
+
// session.updated (primary session, real title) -> OSC 0 pane title (oc-17), written to /dev/tty
|
|
12
|
+
// because OpenCode's own title ("OpenCode" / "OC | <title>") is switched off by the launch
|
|
13
|
+
// (OPENCODE_DISABLE_TERMINAL_TITLE=1): the app's auto-title ingestion shows the title verbatim.
|
|
14
|
+
// No-op without HIVEOP_ACTIVITY_FILE and WENRWA_SESSION_ID; nothing ever throws into OpenCode.
|
|
15
|
+
// ONLY plugin functions may be exported: OpenCode loads every export as a plugin, and a helper
|
|
16
|
+
// export breaks every prompt (Plugin.trigger reads hooks off its return value).
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
|
|
20
|
+
// OpenCode tool ids -> the Claude Code names the bridge/touch tracker already understand.
|
|
21
|
+
const TOOL_NAME = {
|
|
22
|
+
read: 'Read', edit: 'Edit', write: 'Write', patch: 'MultiEdit', multiedit: 'MultiEdit', bash: 'Bash',
|
|
23
|
+
}
|
|
24
|
+
const FILE_TOOLS = new Set(['Read', 'Edit', 'Write', 'Update', 'Create', 'MultiEdit', 'NotebookEdit'])
|
|
25
|
+
const VERB = {
|
|
26
|
+
Read: 'Reading', Edit: 'Editing', Write: 'Writing', Update: 'Updating',
|
|
27
|
+
Create: 'Creating', MultiEdit: 'Editing', NotebookEdit: 'Editing', Bash: 'Running',
|
|
28
|
+
}
|
|
29
|
+
const MAX_BASH_PATHS = 5
|
|
30
|
+
|
|
31
|
+
// Same algorithm as hooks/post-tool-use.js extractBashPaths (kept in step by a parity test).
|
|
32
|
+
function extractBashPaths(command, pathExists, cwd) {
|
|
33
|
+
if (!command || typeof command !== 'string') return []
|
|
34
|
+
const tokens = command.match(/"[^"]*"|'[^']*'|[^\s;|&()<>]+/g) || []
|
|
35
|
+
const root = (cwd || '').replace(/\/+$/, '')
|
|
36
|
+
const out = []
|
|
37
|
+
const seen = new Set()
|
|
38
|
+
const bare = (s) => s.replace(/^['"]|['"]$/g, '').replace(/[),;]+$/, '')
|
|
39
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
40
|
+
const prev = bare(tokens[i - 1])
|
|
41
|
+
if (prev === 'cd' || prev === 'pushd') continue
|
|
42
|
+
const t = bare(tokens[i])
|
|
43
|
+
if (!t || t.startsWith('-')) continue
|
|
44
|
+
const norm = t.replace(/\/+$/, '')
|
|
45
|
+
if (!norm || seen.has(norm)) continue
|
|
46
|
+
if (norm === '.' || (root && norm === root)) continue
|
|
47
|
+
let ok = false
|
|
48
|
+
try { ok = !!pathExists(t) } catch { ok = false }
|
|
49
|
+
if (!ok) continue
|
|
50
|
+
seen.add(norm)
|
|
51
|
+
out.push(t)
|
|
52
|
+
if (out.length >= MAX_BASH_PATHS) break
|
|
53
|
+
}
|
|
54
|
+
return out
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const exists = (p) => { try { return fs.existsSync(p) } catch { return false } }
|
|
58
|
+
|
|
59
|
+
/** Build the 'tool' record fields for an OpenCode tool call. */
|
|
60
|
+
function toolRecord(toolId, args) {
|
|
61
|
+
const tool = typeof toolId === 'string' ? (TOOL_NAME[toolId.toLowerCase()] || toolId) : ''
|
|
62
|
+
const a = args && typeof args === 'object' ? args : {}
|
|
63
|
+
const paths = []
|
|
64
|
+
for (const k of ['filePath', 'file_path', 'notebook_path']) if (typeof a[k] === 'string') paths.push(a[k])
|
|
65
|
+
const filePaths = FILE_TOOLS.has(tool)
|
|
66
|
+
? paths.filter((p, i) => p && paths.indexOf(p) === i)
|
|
67
|
+
: tool === 'Bash' ? extractBashPaths(a.command, exists, process.cwd()) : []
|
|
68
|
+
const base = filePaths.length ? String(filePaths[0]).split('/').pop() : ''
|
|
69
|
+
return { kind: 'tool', tool, filePaths, thinking: (VERB[tool] || 'Working') + (base ? ' ' + base : '') }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function append(extra, env) {
|
|
73
|
+
try {
|
|
74
|
+
const file = env.HIVEOP_ACTIVITY_FILE
|
|
75
|
+
const sessionId = env.WENRWA_SESSION_ID || ''
|
|
76
|
+
if (!file || !sessionId) return
|
|
77
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
78
|
+
fs.appendFileSync(file, JSON.stringify({ sessionId, workspaceId: env.WENRWA_WORKSPACE_ID || '', ...extra }) + '\n')
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// OpenCode's placeholder titles until the title generator runs ("New session - <iso>").
|
|
83
|
+
const DEFAULT_TITLE = /^(New session|Child session) - \d{4}-\d{2}-\d{2}T/
|
|
84
|
+
const MAX_TITLE = 80
|
|
85
|
+
|
|
86
|
+
/** oc-17: the OSC 0 sequence for a session.updated event's info, or '' (sub-agent, placeholder, empty). */
|
|
87
|
+
function titleSequence(info) {
|
|
88
|
+
if (!info || info.parentID || typeof info.title !== 'string') return ''
|
|
89
|
+
const t = info.title.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').replace(/\s+/g, ' ').trim()
|
|
90
|
+
if (!t || DEFAULT_TITLE.test(t)) return ''
|
|
91
|
+
return '\x1b]0;' + (t.length > MAX_TITLE ? t.slice(0, MAX_TITLE - 1) + '\u2026' : t) + '\x07'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let lastTitle = ''
|
|
95
|
+
function setPaneTitle(info, ttyPath = '/dev/tty') {
|
|
96
|
+
try {
|
|
97
|
+
const seq = titleSequence(info)
|
|
98
|
+
if (!seq || seq === lastTitle) return
|
|
99
|
+
const fd = fs.openSync(ttyPath, 'a')
|
|
100
|
+
try { fs.writeSync(fd, seq) } finally { fs.closeSync(fd) }
|
|
101
|
+
lastTitle = seq
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let reportedToGateway = ''
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* oc-05 (hosted): POST the primary session's id to $HIVEOP_GATEWAY_URL/hiveop/session, authenticated
|
|
109
|
+
* by the per-agent gateway token (it already identifies the agent). Best-effort: one retry, never
|
|
110
|
+
* throws, no-op without gateway env. Last one wins server-side, so a fresh session after a
|
|
111
|
+
* "Session not found" fallback replaces the dead id.
|
|
112
|
+
*/
|
|
113
|
+
async function reportSessionToGateway(id, env = process.env) {
|
|
114
|
+
try {
|
|
115
|
+
const base = String(env.HIVEOP_GATEWAY_URL || '').replace(/\/+$/, '')
|
|
116
|
+
const token = env.HIVEOP_GATEWAY_TOKEN || ''
|
|
117
|
+
if (!id || !base || !token || id === reportedToGateway) return
|
|
118
|
+
reportedToGateway = id // claimed up front so a burst of events posts once; released on failure
|
|
119
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch(`${base}/hiveop/session`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
124
|
+
body: JSON.stringify({ sessionId: id }),
|
|
125
|
+
signal: AbortSignal.timeout(5000),
|
|
126
|
+
})
|
|
127
|
+
if (res.ok) return
|
|
128
|
+
if (res.status < 500) break
|
|
129
|
+
} catch {}
|
|
130
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
131
|
+
}
|
|
132
|
+
if (reportedToGateway === id) reportedToGateway = ''
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* oc-05 hook point: capture the generated id of an agent's PRIMARY session so the next launch
|
|
138
|
+
* can `opencode -s <id>`. Sub-agent sessions (info.parentID) never overwrite it.
|
|
139
|
+
* Local lane: first id is written to a file under HIVEOP_OPENCODE_SESSIONS_DIR (keyed by agent).
|
|
140
|
+
* Hosted lane: the id is POSTed to the gateway (see reportSessionToGateway).
|
|
141
|
+
*/
|
|
142
|
+
function captureSessionId(event, env = process.env) {
|
|
143
|
+
try {
|
|
144
|
+
const p = (event && event.properties) || {}
|
|
145
|
+
if (!event || event.type !== 'session.created' || (p.info && p.info.parentID)) return
|
|
146
|
+
const id = p.sessionID || (p.info && p.info.id) || ''
|
|
147
|
+
void reportSessionToGateway(id, env)
|
|
148
|
+
const agentId = env.WENRWA_AGENT_ID || ''
|
|
149
|
+
if (!agentId) return
|
|
150
|
+
const dir = env.HIVEOP_OPENCODE_SESSIONS_DIR || '/data/.hiveop/opencode-sessions'
|
|
151
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
152
|
+
const f = path.join(dir, agentId)
|
|
153
|
+
if (!fs.existsSync(f)) fs.writeFileSync(f, id)
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const HiveOp = async () => {
|
|
158
|
+
const env = process.env
|
|
159
|
+
let lastKind = ''
|
|
160
|
+
const emit = (extra) => {
|
|
161
|
+
// session.status idle and session.idle both fire per turn end: write one stop line.
|
|
162
|
+
if (extra.kind === 'stop' && lastKind === 'stop') return
|
|
163
|
+
lastKind = extra.kind
|
|
164
|
+
append({ ...extra, ...(extra.kind === 'stop' ? { thinking: '' } : {}) }, env)
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
event: async ({ event }) => {
|
|
168
|
+
try {
|
|
169
|
+
if (!event) return
|
|
170
|
+
const p = event.properties || {}
|
|
171
|
+
if (event.type === 'session.created') captureSessionId(event, env)
|
|
172
|
+
else if (event.type === 'session.updated') setPaneTitle(p.info, env.HIVEOP_TITLE_TTY || '/dev/tty')
|
|
173
|
+
else if (event.type === 'session.idle') emit({ kind: 'stop' })
|
|
174
|
+
else if (event.type === 'session.status' && p.status && p.status.type === 'idle') emit({ kind: 'stop' })
|
|
175
|
+
else if (event.type === 'permission.asked' || event.type === 'question.asked') emit({ kind: 'needs_you', thinking: 'Waiting for you' })
|
|
176
|
+
else if (event.type === 'permission.replied') emit({ kind: 'tool', tool: '', filePaths: [], thinking: 'Working' })
|
|
177
|
+
} catch {}
|
|
178
|
+
},
|
|
179
|
+
'tool.execute.before': async (input, output) => {
|
|
180
|
+
try { emit(toolRecord(input && input.tool, (output && output.args) || (input && input.args))) } catch {}
|
|
181
|
+
},
|
|
182
|
+
'tool.execute.after': async (input, output) => {
|
|
183
|
+
try { emit(toolRecord(input && input.tool, (input && input.args) || (output && output.args))) } catch {}
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenrwa/marketplace-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "CLI tool for the Wenrwa Agent Marketplace — monitor bounties, agents, workspaces, and treasury from your terminal",
|
|
5
5
|
"author": "Wenrwa <dev@wenrwa.com>",
|
|
6
6
|
"license": "MIT",
|