@wenrwa/marketplace-cli 0.8.2 → 0.8.4

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.
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * A re-open on a live id (SessionManager#open supersedes) kills the OLD PTY.
5
+ * That process exits asynchronously, AFTER the new session is registered
6
+ * under the same id. Its exit must not drop the NEW session: before this
7
+ * guard, the old onExit ran `sessions.delete(id)` unconditionally, so the
8
+ * fresh PTY kept running but untracked (input dropped, attach answered
9
+ * "Unknown session", nothing could close it) and onSessionExit announced the
10
+ * agent as ended while it was alive.
11
+ *
12
+ * Hit by an explicit restart on either lane: the hosted oc-21 relaunch
13
+ * (relay `open` on a live id) and the browser's local restart after an
14
+ * engine/model change (close → open).
15
+ */
16
+ const sessionManager_1 = require("../bridge/sessionManager");
17
+ class ExitablePty {
18
+ killed = false;
19
+ exitCbs = [];
20
+ write() { }
21
+ resize() { }
22
+ kill() { this.killed = true; }
23
+ onData() { }
24
+ onExit(cb) { this.exitCbs.push(cb); }
25
+ exit(code) { for (const cb of this.exitCbs)
26
+ cb(code); }
27
+ }
28
+ const make = () => {
29
+ const ptys = [];
30
+ const factory = { spawn: () => { const p = new ExitablePty(); ptys.push(p); return p; } };
31
+ const sm = new sessionManager_1.SessionManager(factory);
32
+ const exits = [];
33
+ sm.setOnSessionExit((id) => exits.push(id));
34
+ return { sm, ptys, exits };
35
+ };
36
+ describe('supersede: the old process exiting late never drops the new session', () => {
37
+ it('re-open, then the OLD pty exits → the new session is still tracked and attachable', async () => {
38
+ const { sm, ptys, exits } = make();
39
+ sm.open({ cols: 80, rows: 24, id: 'agent-1' });
40
+ sm.open({ cols: 80, rows: 24, id: 'agent-1' }); // supersede
41
+ expect(ptys[0].killed).toBe(true);
42
+ ptys[0].exit(129); // SIGHUP lands ~600ms later in real life
43
+ expect(sm.list()).toEqual(['agent-1']);
44
+ expect(exits).toEqual([]); // the agent did NOT end
45
+ await expect(sm.attach('agent-1', () => { })).resolves.toBeDefined();
46
+ });
47
+ it('close, re-open, then the OLD pty exits → same', () => {
48
+ const { sm, ptys, exits } = make();
49
+ sm.open({ cols: 80, rows: 24, id: 'agent-1' });
50
+ sm.close('agent-1');
51
+ sm.open({ cols: 80, rows: 24, id: 'agent-1' });
52
+ ptys[0].exit(129);
53
+ expect(sm.list()).toEqual(['agent-1']);
54
+ expect(exits).toEqual([]);
55
+ });
56
+ it('the CURRENT pty exiting still ends the session and notifies (unchanged)', () => {
57
+ const { sm, ptys, exits } = make();
58
+ sm.open({ cols: 80, rows: 24, id: 'agent-1' });
59
+ sm.open({ cols: 80, rows: 24, id: 'agent-1' });
60
+ ptys[1].exit(0);
61
+ expect(sm.list()).toEqual([]);
62
+ expect(exits).toEqual(['agent-1']);
63
+ });
64
+ });
65
+ describe('a plain close still reports the exit (unchanged)', () => {
66
+ it('close with nothing reopened → onSessionExit fires', () => {
67
+ const { sm, ptys, exits } = make();
68
+ sm.open({ cols: 80, rows: 24, id: 'agent-1' });
69
+ sm.close('agent-1');
70
+ ptys[0].exit(129);
71
+ expect(sm.list()).toEqual([]);
72
+ expect(exits).toEqual(['agent-1']);
73
+ });
74
+ });
@@ -0,0 +1,129 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ /**
40
+ * oc-27 — the ACTUAL local launch must carry the same keys the hosted config does:
41
+ * `plugin` (activity, titles, Needs-you, session-id capture), `mcp.wenrwa-marketplace`
42
+ * (when a connect token exists) and `theme`. A builder-only test missed this: the
43
+ * builder was fine, the local call sites never fed it a plugin or a token.
44
+ */
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const url_1 = require("url");
48
+ const ws_1 = __importDefault(require("ws"));
49
+ const agentLaunch_1 = require("../bridge/agentLaunch");
50
+ const server_1 = require("../bridge/server");
51
+ const sessionManager_1 = require("../bridge/sessionManager");
52
+ const hosted = require(path.join(__dirname, '..', '..', '..', 'infra', 'terminal-image', 'opencode', 'build-config.js'));
53
+ const GW = 'https://api.example.test/api/v1/opencode/v1';
54
+ const oc = { engine: 'opencode', model: 'moonshotai/kimi-k2', gatewayUrl: GW, gatewayToken: 'hgw_abc' };
55
+ const cfgOf = (l) => JSON.parse(l.env.OPENCODE_CONFIG_CONTENT);
56
+ // The shape cli/src/commands/bridge.ts hands resolveAgentLaunch (mcpInstall + bridgeApiUrl).
57
+ const realDeps = {
58
+ mcpServer: { command: '/usr/bin/node', args: ['/h/.wen-market/mcp/dist/index.js'] },
59
+ apiUrl: 'https://api.example.test/api/v1',
60
+ activity: { settingsPath: '/h/.wen-market/hiveop-settings.json', sessionId: 'a1', activityFile: '/h/.wen-market/activity.jsonl' },
61
+ sessionPin: { agentId: 'a1', exists: false },
62
+ };
63
+ describe('local OpenCode launch carries the hosted keys', () => {
64
+ const hostedCfg = hosted.buildConfig({
65
+ WENRWA_AGENT_MODEL: oc.model, HIVEOP_GATEWAY_URL: GW, HIVEOP_GATEWAY_TOKEN: 't', WENRWA_CONNECT_TOKEN: 'c',
66
+ });
67
+ it('plugin is a file:// URL to a file that exists in the installed package', () => {
68
+ const cfg = cfgOf((0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc, connectToken: 'c', ...realDeps }));
69
+ expect(cfg.plugin).toHaveLength(1);
70
+ expect(cfg.plugin[0]).toMatch(/^file:\/\//);
71
+ expect(fs.existsSync((0, url_1.fileURLToPath)(cfg.plugin[0]))).toBe(true);
72
+ expect(path.basename((0, url_1.fileURLToPath)(cfg.plugin[0]))).toBe('opencode-plugin.mjs');
73
+ });
74
+ it('mcp.wenrwa-marketplace is present with the token by reference only', () => {
75
+ const l = (0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc, connectToken: 'secret-conn-token', ...realDeps });
76
+ expect(cfgOf(l).mcp['wenrwa-marketplace'].environment.WENRWA_CONNECT_TOKEN).toBe('{env:WENRWA_CONNECT_TOKEN}');
77
+ expect(l.env.OPENCODE_CONFIG_CONTENT).not.toContain('secret-conn-token');
78
+ expect(l.env.WENRWA_CONNECT_TOKEN).toBe('secret-conn-token');
79
+ });
80
+ it('env carries HIVEOP_GATEWAY_URL so the plugin can report the session id (resume)', () => {
81
+ const l = (0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc, connectToken: 'c', ...realDeps });
82
+ expect(l.env.HIVEOP_GATEWAY_URL).toBe(GW);
83
+ expect(l.env.HIVEOP_GATEWAY_TOKEN).toBe('hgw_abc');
84
+ });
85
+ it('theme matches the hosted config (terminal palette, no own background)', () => {
86
+ const cfg = cfgOf((0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc, ...realDeps }));
87
+ expect(cfg.theme).toBe('system');
88
+ expect(cfg.theme).toBe(hostedCfg.theme);
89
+ });
90
+ it('the same top-level keys as hosted (modulo the hosted-only plugin path)', () => {
91
+ const local = cfgOf((0, agentLaunch_1.resolveAgentLaunch)({ opencode: oc, connectToken: 'c', ...realDeps }));
92
+ const keys = (o) => Object.keys(o).filter((k) => k !== 'instructions').sort();
93
+ expect(keys(local)).toEqual(keys(hostedCfg));
94
+ });
95
+ });
96
+ describe('loopback open (real bridge wiring) falls back to the bridge-config connect token', () => {
97
+ let handle;
98
+ let spawns;
99
+ beforeEach(async () => {
100
+ spawns = [];
101
+ const pty = { write() { }, resize() { }, kill() { }, onData() { }, onExit() { } };
102
+ handle = await (0, server_1.startBridgeServer)({
103
+ sessionToken: 'tok', allowedOrigin: 'https://app.hiveop.io',
104
+ sessions: new sessionManager_1.SessionManager({ spawn: (o) => { spawns.push(o); return pty; } }),
105
+ resolveLocalAgentCwd: () => '/tmp/proj',
106
+ commandExists: () => true,
107
+ activityFile: '/h/activity.jsonl', hiveOpSettingsPath: '/h/settings.json',
108
+ mcpServer: realDeps.mcpServer, mcpApiUrl: realDeps.apiUrl,
109
+ connectToken: 'bridge-cfg-token',
110
+ });
111
+ });
112
+ afterEach(async () => { await handle.close(); });
113
+ it('open frame without connectToken still yields plugin + mcp + theme', async () => {
114
+ const ws = new ws_1.default(`ws://127.0.0.1:${handle.port}`, { headers: { origin: 'https://app.hiveop.io' } });
115
+ await new Promise((r) => ws.on('open', r));
116
+ ws.send(JSON.stringify({ v: 1, token: 'tok' }));
117
+ const opened = new Promise((r) => ws.on('message', (m) => { if (JSON.parse(String(m)).t === 'opened')
118
+ r(null); }));
119
+ ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, agentId: 'a1', workspaceId: 'w1', opencode: oc }));
120
+ await opened;
121
+ ws.close();
122
+ const cfg = cfgOf({ env: spawns[0].env });
123
+ expect(cfg.plugin[0]).toMatch(/^file:\/\//);
124
+ expect(cfg.mcp['wenrwa-marketplace']).toBeDefined();
125
+ expect(cfg.theme).toBe('system');
126
+ expect(spawns[0].env.WENRWA_CONNECT_TOKEN).toBe('bridge-cfg-token');
127
+ expect(spawns[0].env.OPENCODE_CONFIG_CONTENT).not.toContain('bridge-cfg-token');
128
+ });
129
+ });
@@ -4,6 +4,7 @@ exports.HIVEOP_CONTEXT_ENV_VALUE = exports.HIVEOP_CONTEXT_ENV_VAR = void 0;
4
4
  exports.missingCommandMessage = missingCommandMessage;
5
5
  exports.resolveAgentLaunch = resolveAgentLaunch;
6
6
  const localContextWriter_1 = require("./localContextWriter");
7
+ const opencodePlugin_1 = require("./opencodePlugin");
7
8
  const opencodeConfig_1 = require("./opencodeConfig");
8
9
  /** lws-08 — env var name a spawned `claude` (or any tooling inspecting its
9
10
  * own environment) can read to find the pushed context manifest. */
@@ -46,21 +47,27 @@ function resolveOpencodeLaunch(opts, oc) {
46
47
  const args = [];
47
48
  if ((0, opencodeConfig_1.isCapturedOpencodeSessionId)(oc.sessionId))
48
49
  args.push('-s', oc.sessionId);
49
- const config = (0, opencodeConfig_1.buildOpencodeConfig)({
50
- model: oc.model,
51
- gatewayBaseUrl: oc.gatewayUrl,
52
- autonomous: !!opts.autonomous,
53
- withMcp: !!opts.connectToken,
54
- // oc-23: Claude loads .hiveop/context.md via its SessionStart hook; OpenCode has none, so it reads it as instructions.
55
- instructions: [exports.HIVEOP_CONTEXT_ENV_VALUE],
56
- ...(opts.mcpServer ? { mcpCommand: [opts.mcpServer.command, ...opts.mcpServer.args] } : {}),
57
- ...(opts.apiUrl ? { mcpEnvPassthrough: ['MARKETPLACE_API_URL'] } : {}),
58
- });
50
+ const config = {
51
+ ...(0, opencodeConfig_1.buildOpencodeConfig)({
52
+ model: oc.model,
53
+ gatewayBaseUrl: oc.gatewayUrl,
54
+ autonomous: !!opts.autonomous,
55
+ withMcp: !!opts.connectToken,
56
+ // oc-23: Claude loads .hiveop/context.md via its SessionStart hook; OpenCode has none, so it reads it as instructions.
57
+ instructions: [exports.HIVEOP_CONTEXT_ENV_VALUE],
58
+ ...(opts.mcpServer ? { mcpCommand: [opts.mcpServer.command, ...opts.mcpServer.args] } : {}),
59
+ ...(opts.apiUrl ? { mcpEnvPassthrough: ['MARKETPLACE_API_URL'] } : {}),
60
+ }),
61
+ // oc-27: the HiveOp plugin (activity, titles, Needs-you, session-id capture) — as hosted does.
62
+ ...(0, opencodePlugin_1.buildOpenCodePluginConfig)(),
63
+ };
59
64
  const env = {
60
65
  [exports.HIVEOP_CONTEXT_ENV_VAR]: exports.HIVEOP_CONTEXT_ENV_VALUE,
61
66
  ...opencodeConfig_1.OPENCODE_QUIET_ENV,
62
67
  OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
63
68
  [opencodeConfig_1.OPENCODE_GATEWAY_TOKEN_ENV]: oc.gatewayToken,
69
+ // oc-27: the plugin POSTs the captured session id to <gateway>/hiveop/session (resume).
70
+ HIVEOP_GATEWAY_URL: oc.gatewayUrl,
64
71
  };
65
72
  if (opts.connectToken)
66
73
  env.WENRWA_CONNECT_TOKEN = opts.connectToken;
@@ -16,6 +16,7 @@ export declare const OPENCODE_MIN_VERSION = "1.18.35";
16
16
  export declare const OPENCODE_INSTALL_HINT = "Install OpenCode: npm i -g opencode-ai@>=1.18.35";
17
17
  /** Every outbound call OpenCode makes at startup besides the gateway (oc-04 s5). */
18
18
  export declare const OPENCODE_QUIET_ENV: Readonly<Record<string, string>>;
19
+ export declare const OPENCODE_THEME = "system";
19
20
  export declare const OPENCODE_PROVIDER_ID = "hiveop";
20
21
  export declare const OPENCODE_GATEWAY_TOKEN_ENV = "HIVEOP_GATEWAY_TOKEN";
21
22
  export interface OpencodeConfigInput {
@@ -10,7 +10,7 @@
10
10
  * (`{env:HIVEOP_GATEWAY_TOKEN}`) so it never appears inside the JSON.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.isCapturedOpencodeSessionId = exports.OPENCODE_GATEWAY_TOKEN_ENV = exports.OPENCODE_PROVIDER_ID = exports.OPENCODE_QUIET_ENV = exports.OPENCODE_INSTALL_HINT = exports.OPENCODE_MIN_VERSION = exports.OPENCODE_PINNED_VERSION = void 0;
13
+ exports.isCapturedOpencodeSessionId = exports.OPENCODE_GATEWAY_TOKEN_ENV = exports.OPENCODE_PROVIDER_ID = exports.OPENCODE_THEME = exports.OPENCODE_QUIET_ENV = exports.OPENCODE_INSTALL_HINT = exports.OPENCODE_MIN_VERSION = exports.OPENCODE_PINNED_VERSION = void 0;
14
14
  exports.buildOpencodeConfig = buildOpencodeConfig;
15
15
  exports.opencodeVersionOk = opencodeVersionOk;
16
16
  /** Oldest OpenCode the local lane accepts. 1.18.35 is the version oc-04 verified
@@ -29,6 +29,7 @@ exports.OPENCODE_QUIET_ENV = Object.freeze({
29
29
  OPENCODE_DISABLE_TERMINAL_TITLE: '1',
30
30
  npm_config_offline: 'true',
31
31
  });
32
+ exports.OPENCODE_THEME = 'system';
32
33
  exports.OPENCODE_PROVIDER_ID = 'hiveop';
33
34
  exports.OPENCODE_GATEWAY_TOKEN_ENV = 'HIVEOP_GATEWAY_TOKEN';
34
35
  function buildOpencodeConfig(input) {
@@ -41,6 +42,9 @@ function buildOpencodeConfig(input) {
41
42
  model: `${exports.OPENCODE_PROVIDER_ID}/${model}`,
42
43
  // OpenCode makes a second, billed "title" call on the small model.
43
44
  small_model: `${exports.OPENCODE_PROVIDER_ID}/${model}`,
45
+ // oc-27: OpenCode's built-in "system" theme derives its palette from the terminal and
46
+ // leaves the background transparent, so the pane blends with the canvas like Claude's.
47
+ theme: exports.OPENCODE_THEME,
44
48
  provider: {
45
49
  [exports.OPENCODE_PROVIDER_ID]: {
46
50
  npm: '@ai-sdk/openai-compatible',
@@ -244,6 +244,9 @@ export interface BridgeServerOpts {
244
244
  args: string[];
245
245
  };
246
246
  mcpApiUrl?: string;
247
+ /** oc-27 — the bridge's own stored Connect Token: the fallback for an `open` frame that carries none
248
+ * (loopback frames from the browser never do), so local OpenCode agents get the same MCP as Claude. */
249
+ connectToken?: string;
247
250
  /**
248
251
  * rival-10 — every file touch of a LOCAL WORKSPACE AGENT (an `open` with
249
252
  * agentId + workspaceId), handed on for the ranked rival feed. The hook
@@ -736,7 +736,7 @@ function startBridgeServer(opts) {
736
736
  let launch = (0, agentLaunch_1.resolveAgentLaunch)({
737
737
  autonomous: !!frame.autonomous,
738
738
  ...(frame.opencode ? { opencode: frame.opencode } : {}),
739
- ...(frame.connectToken ? { connectToken: frame.connectToken } : {}),
739
+ ...((frame.connectToken || opts.connectToken) ? { connectToken: (frame.connectToken || opts.connectToken) } : {}),
740
740
  ...(opts.mcpServer ? { mcpServer: opts.mcpServer } : {}),
741
741
  ...(opts.mcpApiUrl ? { apiUrl: opts.mcpApiUrl } : {}),
742
742
  ...(cwd ? { sessionPin: { agentId, exists: agentSessionExists } } : {}),
@@ -188,6 +188,14 @@ class SessionManager {
188
188
  session.exited = true;
189
189
  session.sink = null;
190
190
  session.snapshot.dispose();
191
+ // A superseded (or closed-then-reopened) PTY exits AFTER its successor
192
+ // registered under the same id. That late exit is not this id ending:
193
+ // dropping the map entry here orphaned the live successor (untracked,
194
+ // input dropped, unclosable) and announced a running agent as ended.
195
+ // A plain close (nothing reopened) still notifies, as before.
196
+ const current = this.sessions.get(id);
197
+ if (current && current !== session)
198
+ return;
191
199
  this.sessions.delete(id);
192
200
  // fcct-26 — tell anyone listening that this session ENDED. Without an
193
201
  // outward signal the only way to know a crew member finished its turn is
@@ -1127,6 +1127,8 @@ exports.bridgeCommand = new commander_1.Command('bridge')
1127
1127
  hiveOpSettingsPath,
1128
1128
  ...(mcpInstall.ok ? { mcpServer: { command: mcpInstall.command, args: mcpInstall.args } } : {}),
1129
1129
  mcpApiUrl: bridgeApiUrl(),
1130
+ // oc-27: local OpenCode panes get the HiveOp MCP with the same token Claude's registration uses.
1131
+ connectToken: config.connectToken,
1130
1132
  // tchp-18: the loopback changes reporter is opt-in; without this the
1131
1133
  // tchp-12 machinery never constructs on a laptop and local panels sit
1132
1134
  // on "Waiting for changes…" forever.
@@ -1353,7 +1355,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
1353
1355
  resolveLaunch: ({ sessionId, opencode, connectToken }) => (0, agentLaunch_1.resolveAgentLaunch)({
1354
1356
  autonomous: true,
1355
1357
  ...(opencode ? { opencode } : {}),
1356
- ...(connectToken ? { connectToken } : {}),
1358
+ ...((connectToken || config.connectToken) ? { connectToken: (connectToken || config.connectToken) } : {}),
1357
1359
  ...(mcpInstall.ok ? { mcpServer: { command: mcpInstall.command, args: mcpInstall.args } } : {}),
1358
1360
  apiUrl: bridgeApiUrl(),
1359
1361
  ...(activityFile && hiveOpSettingsPath && sessionId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenrwa/marketplace-cli",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
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",