@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.
Files changed (61) 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-attach-screenshot.test.d.ts +1 -0
  5. package/dist/__tests__/bridge-attach-screenshot.test.js +250 -0
  6. package/dist/__tests__/bridge-bundle-installer-opencode.test.d.ts +1 -0
  7. package/dist/__tests__/bridge-bundle-installer-opencode.test.js +69 -0
  8. package/dist/__tests__/bridge-context-engine.test.d.ts +1 -0
  9. package/dist/__tests__/bridge-context-engine.test.js +112 -0
  10. package/dist/__tests__/bridge-context-manifest-frame.test.js +19 -0
  11. package/dist/__tests__/bridge-crew-translator.test.d.ts +1 -0
  12. package/dist/__tests__/bridge-crew-translator.test.js +103 -0
  13. package/dist/__tests__/bridge-install-bundle-frame.test.js +24 -0
  14. package/dist/__tests__/bridge-loopback-opencode-open.test.d.ts +1 -0
  15. package/dist/__tests__/bridge-loopback-opencode-open.test.js +51 -0
  16. package/dist/__tests__/bridge-opencode-launch.test.d.ts +1 -0
  17. package/dist/__tests__/bridge-opencode-launch.test.js +179 -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/opencodeConfig.d.ts +41 -0
  46. package/dist/bridge/opencodeConfig.js +107 -0
  47. package/dist/bridge/opencodePlugin.d.ts +13 -0
  48. package/dist/bridge/opencodePlugin.js +54 -0
  49. package/dist/bridge/opencodeTranscriptReader.d.ts +24 -0
  50. package/dist/bridge/opencodeTranscriptReader.js +178 -0
  51. package/dist/bridge/relayClient.d.ts +27 -1
  52. package/dist/bridge/relayClient.js +24 -7
  53. package/dist/bridge/screenshotAttach.d.ts +35 -0
  54. package/dist/bridge/screenshotAttach.js +173 -0
  55. package/dist/bridge/server.d.ts +9 -0
  56. package/dist/bridge/server.js +19 -1
  57. package/dist/bridge/sessionManager.d.ts +5 -0
  58. package/dist/bridge/sessionManager.js +21 -1
  59. package/dist/commands/bridge.js +36 -9
  60. package/hooks/opencode-plugin.mjs +186 -0
  61. 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
+ }
@@ -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
@@ -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: '\r\n\x1b[31mclaude not found on PATH — install Claude Code CLI, then reopen this pane.\x1b[0m\r\n',
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
- const gated = this.launchGate && (isClaude || this.everyOpenIsAgentLaunch);
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
@@ -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.7.7",
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",