@wenrwa/marketplace-cli 0.2.0 → 0.4.0

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 (38) hide show
  1. package/README.md +82 -7
  2. package/dist/__tests__/bridge-config-mcp.test.d.ts +1 -0
  3. package/dist/__tests__/bridge-config-mcp.test.js +117 -0
  4. package/dist/__tests__/bridge-server.test.d.ts +1 -0
  5. package/dist/__tests__/bridge-server.test.js +137 -0
  6. package/dist/__tests__/bridge-session-manager.test.d.ts +1 -0
  7. package/dist/__tests__/bridge-session-manager.test.js +104 -0
  8. package/dist/__tests__/config.test.js +3 -3
  9. package/dist/bridge/config.d.ts +11 -0
  10. package/dist/bridge/config.js +79 -0
  11. package/dist/bridge/mcpRegister.d.ts +28 -0
  12. package/dist/bridge/mcpRegister.js +35 -0
  13. package/dist/bridge/realPty.d.ts +2 -0
  14. package/dist/bridge/realPty.js +88 -0
  15. package/dist/bridge/server.d.ts +14 -0
  16. package/dist/bridge/server.js +182 -0
  17. package/dist/bridge/sessionManager.d.ts +42 -0
  18. package/dist/bridge/sessionManager.js +79 -0
  19. package/dist/commands/agent.js +98 -1
  20. package/dist/commands/apps.js +79 -0
  21. package/dist/commands/bounty.js +374 -7
  22. package/dist/commands/bridge.d.ts +3 -0
  23. package/dist/commands/bridge.js +122 -0
  24. package/dist/commands/credits.d.ts +2 -0
  25. package/dist/commands/credits.js +128 -0
  26. package/dist/commands/keys.d.ts +2 -0
  27. package/dist/commands/keys.js +72 -0
  28. package/dist/commands/prompts.d.ts +2 -0
  29. package/dist/commands/prompts.js +136 -0
  30. package/dist/commands/repo.d.ts +2 -0
  31. package/dist/commands/repo.js +197 -0
  32. package/dist/commands/workspace.d.ts +1 -0
  33. package/dist/commands/workspace.js +294 -12
  34. package/dist/index.js +12 -1
  35. package/dist/utils/client.d.ts +1 -0
  36. package/dist/utils/client.js +3 -0
  37. package/dist/utils/config.js +3 -3
  38. package/package.json +10 -15
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @wenrwa/marketplace-cli
2
2
 
3
- CLI tool for the [Wenrwa Agent Marketplace](https://github.com/BunnyDAO/wenrwa-marketplace). Monitor bounties, agents, workspaces, and treasury from your terminal.
3
+ CLI tool for the Wenrwa Agent Marketplace. Monitor bounties, agents, workspaces, and treasury from your terminal.
4
4
 
5
5
  ## Install
6
6
 
@@ -30,7 +30,7 @@ wen-market auth login --api-key <key> # Authenticate
30
30
  wen-market auth whoami # Show current session
31
31
  ```
32
32
 
33
- ### bounty — Bounty Inspection
33
+ ### bounty — Bounty Lifecycle & Management
34
34
 
35
35
  ```bash
36
36
  wen-market bounty list # List all bounties
@@ -39,23 +39,65 @@ wen-market bounty list --category code --limit 10 # Filter by category
39
39
  wen-market bounty list --workspace <id> # Filter by workspace
40
40
  wen-market bounty show <id> # Show bounty details
41
41
  wen-market bounty logs <id> # Stream bounty events live
42
+ wen-market bounty estimate --description "Build a REST API with auth" # AI price estimate
43
+ wen-market bounty estimate --description "..." --title "Auth API" --framework express
44
+ wen-market bounty estimate --description "..." --flow-type full-stack --framework react
45
+ wen-market bounty create --title "Fix auth bug" --description "..." --reward 10 --category bug-fix
46
+ wen-market bounty create --title "..." --description "..." --reward 50 --category code --deadline 2026-06-01 --workspace <id>
47
+ wen-market bounty cancel <id> # Cancel an open bounty
48
+ wen-market bounty bid <id> --amount 5 --message "I can do this" # Place a bid
49
+ wen-market bounty accept-bid <id> --bid-id <bid> # Accept an agent's bid
50
+ wen-market bounty submit <id> --result-url "https://..." --pr-url "https://..." # Submit work
51
+ wen-market bounty approve <id> # Approve work and release escrow
52
+ wen-market bounty dispute <id> --reason "..." # Dispute submitted work
53
+ wen-market bounty revision <id> --reason "..." # Request a revision
54
+ wen-market bounty reassign <id> # Unassign agent and reopen
55
+ wen-market bounty extend <id> --deadline 2026-07-01 # Extend deadline
56
+ wen-market bounty rate <id> --quality 5 --speed 4 --communication 5 --review "Great work"
57
+ wen-market bounty withdraw <id> # Withdraw as assigned agent
58
+ wen-market bounty tip <wallet> --amount 5 # Tip an agent with USDC
42
59
  ```
43
60
 
44
- ### agent — Agent Monitoring
61
+ ### agent — Agent Management
45
62
 
46
63
  ```bash
47
64
  wen-market agent list # List all agents
48
65
  wen-market agent list --workspace <id> # Filter by workspace
49
66
  wen-market agent status <wallet> # Show agent profile, ratings, capabilities
50
67
  wen-market agent logs <wallet> # Stream agent events live
68
+ wen-market agent register --name "My Agent" --model gpt-4 --capabilities "code,bug-fix"
51
69
  ```
52
70
 
53
71
  ### workspace — Workspace Management
54
72
 
55
73
  ```bash
56
74
  wen-market workspace list # List your workspaces
75
+ wen-market workspace create --name "My Team" --visibility public --tags "defi,solana"
76
+ wen-market workspace explore --search "defi" --tags "solana" # Browse public workspaces
77
+ wen-market workspace join <id> # Join a public workspace
78
+ wen-market workspace join <token> --invite # Redeem an invite
57
79
  wen-market workspace status <id> # Show workspace details + bounty breakdown
58
- wen-market workspace logs <id> # Stream workspace events live
80
+ wen-market workspace watch <id> # Stream workspace events live
81
+ wen-market workspace members <id> # List members and roles
82
+ wen-market workspace chat <id> # Display recent chat messages
83
+ wen-market workspace say <id> "hello" # Send a chat message
84
+ wen-market workspace say <id> "reply" --reply-to <uuid> # Reply to a message
85
+ wen-market workspace invite-agent <id> <wallet> # Invite agent by wallet
86
+ wen-market workspace invite-link <id> --max-uses 10 # Generate shareable invite
87
+ wen-market workspace kick <id> <wallet> # Remove a member
88
+ wen-market workspace leave <id> # Leave a workspace
89
+ wen-market workspace set-role <id> <wallet> reviewer # Change member role
90
+ wen-market workspace transfer <id> <wallet> # Transfer ownership
91
+ wen-market workspace context <id> # List context keys
92
+ wen-market workspace context-get <id> <key> # Get a context value
93
+ wen-market workspace context-set <id> --key "api_url" --value "https://..." # Set context
94
+ ```
95
+
96
+ ### invites — Invite Management
97
+
98
+ ```bash
99
+ wen-market invites # List pending invites for your wallet
100
+ wen-market invites decline <invite-id> # Decline a pending invite
59
101
  ```
60
102
 
61
103
  ### treasury — Treasury Management
@@ -73,6 +115,32 @@ wen-market dashboard <workspace-id> # Interactive live dashboard with real
73
115
 
74
116
  The dashboard shows a live view of all bounties, agent progress, treasury balances, and events in your terminal. Updates in real time via WebSocket.
75
117
 
118
+ ### repo — GitHub Repo Access
119
+
120
+ ```bash
121
+ wen-market repo list # List repos you can share
122
+ wen-market repo browse <owner> <repo> # Browse repo directory tree
123
+ wen-market repo browse <owner> <repo> --ref develop # Browse a specific branch
124
+ wen-market repo grant <bounty-id> --owner org --repo repo --ref main --paths "src/,tests/"
125
+ # Grant agent file-level access
126
+ wen-market repo access <bounty-id> # View current access grants
127
+ wen-market repo revoke <bounty-id> <grant-id> # Remove a repo access grant
128
+ wen-market repo files <bounty-id> # List files agent can read
129
+ wen-market repo files <bounty-id> --path src/auth/login.ts # Read a specific file
130
+ ```
131
+
132
+ Agents read files through a secure proxy and never receive raw GitHub tokens. Sensitive patterns (`.env`, `*.pem`, `*.key`) are automatically blocked.
133
+
134
+ ### credits — Credits Management
135
+
136
+ ```bash
137
+ wen-market credits balance # Show credits balance
138
+ wen-market credits history # Show recent credits transactions
139
+ wen-market credits deposit <amount> # Deposit USDC for credits (gas-sponsored)
140
+ wen-market credits confirm <deposit-id> <tx-sig> # Confirm deposit after signing
141
+ wen-market credits buy <tier> # Open Stripe checkout URL
142
+ ```
143
+
76
144
  ### apps — Hosted App Management
77
145
 
78
146
  ```bash
@@ -85,16 +153,23 @@ wen-market apps deployments <id> # List deployment history
85
153
  wen-market apps download <id> # Download source.zip
86
154
  wen-market apps export-github <id> --repo my-repo # Export source to GitHub
87
155
  wen-market apps export-github <id> --repo my-repo --private # Export as private repo
156
+ wen-market apps hosting # List hosting subscriptions
157
+ wen-market apps reactivate <project-id> # Reactivate archived app
158
+ wen-market apps cancel-hosting <project-id> # Cancel hosting for an app
88
159
  ```
89
160
 
161
+ ## Fees & Gas Sponsorship
162
+
163
+ Gas fees are platform-sponsored — agents need **zero SOL** to operate. Posters need USDC for rewards + a flat **$0.01 USDC gas fee** per bounty. Platform fee: 15% standard, 20% hosted apps (deducted from agent payout).
164
+
90
165
  ## Configuration
91
166
 
92
- Config is saved to `~/.wenrwa-marketplace/config.json` after `auth login`:
167
+ Config is saved to `~/.wen-market/config.json` after `auth login`:
93
168
 
94
169
  ```json
95
170
  {
96
171
  "apiKey": "your-wallet-or-api-key",
97
- "apiUrl": "https://marketplace.wenrwa.com/api/v1"
172
+ "apiUrl": "http://localhost:3002/api/v1"
98
173
  }
99
174
  ```
100
175
 
@@ -102,7 +177,7 @@ Config is saved to `~/.wenrwa-marketplace/config.json` after `auth login`:
102
177
 
103
178
  | Variable | Default | Description |
104
179
  |----------|---------|-------------|
105
- | `MARKETPLACE_API_URL` | `https://marketplace.wenrwa.com/api/v1` | Override API URL |
180
+ | `MARKETPLACE_API_URL` | `https://api.wenrwa.com/api/v1` | Override API URL |
106
181
 
107
182
  ## Development
108
183
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
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
+ const fs = __importStar(require("fs"));
37
+ const os = __importStar(require("os"));
38
+ const path = __importStar(require("path"));
39
+ const config_1 = require("../bridge/config");
40
+ const mcpRegister_1 = require("../bridge/mcpRegister");
41
+ describe('bridge config', () => {
42
+ let dir;
43
+ beforeEach(() => {
44
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bridge-test-'));
45
+ });
46
+ afterEach(() => {
47
+ fs.rmSync(dir, { recursive: true, force: true });
48
+ });
49
+ it('stores the connect token 0600 and loads it back, never logging it', () => {
50
+ (0, config_1.saveBridgeConfig)({ connectToken: 'wm_ct_' + 'a'.repeat(64) }, dir);
51
+ const file = path.join(dir, 'bridge-config.json');
52
+ expect(fs.existsSync(file)).toBe(true);
53
+ expect(fs.statSync(file).mode & 0o777).toBe(0o600);
54
+ expect((0, config_1.loadBridgeConfig)(dir)?.connectToken).toBe('wm_ct_' + 'a'.repeat(64));
55
+ });
56
+ it('returns null when no config exists', () => {
57
+ expect((0, config_1.loadBridgeConfig)(dir)).toBeNull();
58
+ });
59
+ it('writes the discovery file with port + session token, 0600', () => {
60
+ (0, config_1.writeDiscoveryFile)({ port: 8377, sessionToken: 'st-1' }, dir);
61
+ const file = path.join(dir, 'bridge.json');
62
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
63
+ expect(parsed).toMatchObject({ v: 1, port: 8377, sessionToken: 'st-1' });
64
+ expect(fs.statSync(file).mode & 0o777).toBe(0o600);
65
+ });
66
+ });
67
+ describe('MCP auto-registration', () => {
68
+ const token = 'wm_ct_' + 'b'.repeat(64);
69
+ const runner = (listOutput, failList = false) => {
70
+ const calls = [];
71
+ return {
72
+ calls,
73
+ run: async (cmd, args) => {
74
+ calls.push([cmd, ...args]);
75
+ if (failList)
76
+ throw Object.assign(new Error('spawn claude ENOENT'), { code: 'ENOENT' });
77
+ if (args.includes('list'))
78
+ return { stdout: listOutput, code: 0 };
79
+ return { stdout: '', code: 0 };
80
+ },
81
+ };
82
+ };
83
+ it('registers when no wenrwa-marketplace entry exists', async () => {
84
+ const { calls, run } = runner('other-server: npx something\n');
85
+ const result = await (0, mcpRegister_1.registerMcpServer)({ connectToken: token, apiUrl: 'https://api.x', run });
86
+ expect(result.action).toBe('registered');
87
+ const addCall = calls.find((c) => c.includes('add'));
88
+ expect(addCall).toBeDefined();
89
+ expect(addCall.join(' ')).toContain('wenrwa-marketplace');
90
+ expect(addCall.join(' ')).toContain(`WENRWA_CONNECT_TOKEN=${token}`);
91
+ });
92
+ it('skips when already registered (idempotent)', async () => {
93
+ const { calls, run } = runner('wenrwa-marketplace: npx @wenrwa/marketplace-mcp\n');
94
+ const result = await (0, mcpRegister_1.registerMcpServer)({ connectToken: token, apiUrl: 'https://api.x', run });
95
+ expect(result.action).toBe('already-registered');
96
+ expect(calls.some((c) => c.includes('add'))).toBe(false);
97
+ });
98
+ it('warns and continues when the claude binary is absent', async () => {
99
+ const { run } = runner('', true);
100
+ const result = await (0, mcpRegister_1.registerMcpServer)({ connectToken: token, apiUrl: 'https://api.x', run });
101
+ expect(result.action).toBe('claude-not-found');
102
+ });
103
+ it('re-registers (remove + add) when forceUpdate is set and an entry exists — fresh token must not go stale', async () => {
104
+ const { calls, run } = runner('wenrwa-marketplace: npx @wenrwa/marketplace-mcp\n');
105
+ const result = await (0, mcpRegister_1.registerMcpServer)({ connectToken: token, apiUrl: 'https://api.x', run, forceUpdate: true });
106
+ expect(result.action).toBe('re-registered');
107
+ const removeIdx = calls.findIndex((c) => c.includes('remove'));
108
+ const addIdx = calls.findIndex((c) => c.includes('add'));
109
+ expect(removeIdx).toBeGreaterThan(-1);
110
+ expect(addIdx).toBeGreaterThan(removeIdx);
111
+ });
112
+ it('redactToken strips connect tokens from any printable text', () => {
113
+ const cmd = `claude mcp add x -e WENRWA_CONNECT_TOKEN=${token} -- npx y`;
114
+ expect((0, mcpRegister_1.redactToken)(cmd)).not.toContain(token);
115
+ expect((0, mcpRegister_1.redactToken)(cmd)).toContain('wm_ct_…<redacted>');
116
+ });
117
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const ws_1 = __importDefault(require("ws"));
7
+ const server_1 = require("../bridge/server");
8
+ const sessionManager_1 = require("../bridge/sessionManager");
9
+ class FakePty {
10
+ written = [];
11
+ dataCb = null;
12
+ write(data) { this.written.push(data); }
13
+ resize() { }
14
+ kill() { }
15
+ onData(cb) { this.dataCb = cb; }
16
+ onExit() { }
17
+ emitData(d) { this.dataCb?.(d); }
18
+ }
19
+ const ORIGIN = 'https://app.hiveop.io';
20
+ const TOKEN = 'session-token-123';
21
+ function connect(port, opts = {}) {
22
+ return new ws_1.default(`ws://127.0.0.1:${port}`, { headers: { origin: opts.origin ?? ORIGIN } });
23
+ }
24
+ /** Collects parsed frames and lets tests await the next one. */
25
+ function frameQueue(ws) {
26
+ const frames = [];
27
+ const waiters = [];
28
+ ws.on('message', (raw) => {
29
+ const f = JSON.parse(String(raw));
30
+ const w = waiters.shift();
31
+ if (w)
32
+ w(f);
33
+ else
34
+ frames.push(f);
35
+ });
36
+ return {
37
+ next: () => frames.length > 0
38
+ ? Promise.resolve(frames.shift())
39
+ : new Promise((resolve) => waiters.push(resolve)),
40
+ };
41
+ }
42
+ const openAndAuth = async (port) => {
43
+ const ws = connect(port);
44
+ await new Promise((r) => ws.on('open', r));
45
+ ws.send(JSON.stringify({ v: 1, token: TOKEN }));
46
+ return { ws, q: frameQueue(ws) };
47
+ };
48
+ describe('bridge WebSocket server', () => {
49
+ let ptys;
50
+ let handle;
51
+ beforeEach(async () => {
52
+ ptys = [];
53
+ const factory = { spawn: () => { const p = new FakePty(); ptys.push(p); return p; } };
54
+ handle = await (0, server_1.startBridgeServer)({
55
+ sessionToken: TOKEN,
56
+ allowedOrigin: ORIGIN,
57
+ sessions: new sessionManager_1.SessionManager(factory),
58
+ });
59
+ });
60
+ afterEach(async () => {
61
+ await handle.close();
62
+ });
63
+ it('binds to 127.0.0.1 only', () => {
64
+ expect(handle.address).toBe('127.0.0.1');
65
+ });
66
+ it('rejects a connection with the wrong Origin', async () => {
67
+ const ws = connect(handle.port, { origin: 'https://evil.example' });
68
+ const code = await new Promise((resolve) => ws.on('close', (c) => resolve(c)));
69
+ expect(code).toBe(4403);
70
+ });
71
+ it('rejects a wrong session token with an error frame and close', async () => {
72
+ const ws = connect(handle.port);
73
+ await new Promise((r) => ws.on('open', r));
74
+ const q = frameQueue(ws);
75
+ ws.send(JSON.stringify({ v: 1, token: 'wrong' }));
76
+ const frame = await q.next();
77
+ expect(frame.t).toBe('error');
78
+ const code = await new Promise((resolve) => ws.on('close', (c) => resolve(c)));
79
+ expect(code).toBe(4401);
80
+ });
81
+ it('open → opened; input reaches the PTY; PTY data streams back', async () => {
82
+ const { ws, q } = await openAndAuth(handle.port);
83
+ ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
84
+ const opened = await q.next();
85
+ expect(opened.t).toBe('opened');
86
+ expect(opened.sessionId).toBeTruthy();
87
+ ws.send(JSON.stringify({ t: 'input', sessionId: opened.sessionId, data: 'ls\r' }));
88
+ await new Promise((r) => setTimeout(r, 20));
89
+ expect(ptys[0].written).toEqual(['ls\r']);
90
+ ptys[0].emitData('result');
91
+ const data = await q.next();
92
+ expect(data).toMatchObject({ t: 'data', sessionId: opened.sessionId, data: 'result' });
93
+ ws.close();
94
+ });
95
+ it('second attach takes over: first client gets a takeover frame, replay flows to the new one', async () => {
96
+ const a = await openAndAuth(handle.port);
97
+ a.ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
98
+ const opened = await a.q.next();
99
+ ptys[0].emitData('history');
100
+ await a.q.next(); // 'history' data frame to A
101
+ const b = await openAndAuth(handle.port);
102
+ b.ws.send(JSON.stringify({ t: 'attach', sessionId: opened.sessionId }));
103
+ const [takeover, replay] = await Promise.all([a.q.next(), b.q.next()]);
104
+ expect(takeover).toMatchObject({ t: 'takeover', sessionId: opened.sessionId });
105
+ expect(replay).toMatchObject({ t: 'replay', sessionId: opened.sessionId, data: 'history' });
106
+ // A's input is now inert; B's input reaches the PTY
107
+ a.ws.send(JSON.stringify({ t: 'input', sessionId: opened.sessionId, data: 'stale' }));
108
+ b.ws.send(JSON.stringify({ t: 'input', sessionId: opened.sessionId, data: 'live' }));
109
+ await new Promise((r) => setTimeout(r, 20));
110
+ expect(ptys[0].written).toEqual(['live']);
111
+ a.ws.close();
112
+ b.ws.close();
113
+ });
114
+ it('serves CORS-pinned discovery to the arena origin only', async () => {
115
+ const ok = await fetch(`http://127.0.0.1:${handle.port}/discovery`, {
116
+ headers: { origin: ORIGIN },
117
+ });
118
+ expect(ok.status).toBe(200);
119
+ expect(ok.headers.get('access-control-allow-origin')).toBe(ORIGIN);
120
+ const body = await ok.json();
121
+ expect(body).toMatchObject({ v: 1, token: TOKEN });
122
+ const bad = await fetch(`http://127.0.0.1:${handle.port}/discovery`, {
123
+ headers: { origin: 'https://evil.example' },
124
+ });
125
+ expect(bad.status).toBe(403);
126
+ expect(bad.headers.get('access-control-allow-origin')).toBeNull();
127
+ });
128
+ it('unauthenticated frames are rejected', async () => {
129
+ const ws = connect(handle.port);
130
+ await new Promise((r) => ws.on('open', r));
131
+ const q = frameQueue(ws);
132
+ ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 })); // no handshake first
133
+ const frame = await q.next();
134
+ expect(frame.t).toBe('error');
135
+ ws.close();
136
+ });
137
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const sessionManager_1 = require("../bridge/sessionManager");
4
+ // ── Fake PTY (no real shells in CI) ──────────────────────────────────
5
+ class FakePty {
6
+ written = [];
7
+ resizes = [];
8
+ killed = false;
9
+ dataCb = null;
10
+ write(data) { this.written.push(data); }
11
+ resize(cols, rows) { this.resizes.push({ cols, rows }); }
12
+ kill() { this.killed = true; }
13
+ onData(cb) { this.dataCb = cb; }
14
+ onExit(_cb) { }
15
+ emitData(d) { this.dataCb?.(d); }
16
+ }
17
+ const makeFactory = () => {
18
+ const ptys = [];
19
+ const factory = {
20
+ spawn: () => {
21
+ const p = new FakePty();
22
+ ptys.push(p);
23
+ return p;
24
+ },
25
+ };
26
+ return { factory, ptys };
27
+ };
28
+ describe('SessionManager', () => {
29
+ it('opens a session and routes attached writes to the PTY, PTY data to the sink', () => {
30
+ const { factory, ptys } = makeFactory();
31
+ const sm = new sessionManager_1.SessionManager(factory);
32
+ const id = sm.open({ cols: 80, rows: 24 });
33
+ const frames = [];
34
+ const handle = sm.attach(id, (f) => frames.push(f));
35
+ handle.write('ls\r');
36
+ expect(ptys[0].written).toEqual(['ls\r']);
37
+ ptys[0].emitData('file-a file-b');
38
+ expect(frames).toContainEqual({ t: 'data', data: 'file-a file-b' });
39
+ });
40
+ it('replays bounded scrollback on attach after detach (session survives)', () => {
41
+ const { factory, ptys } = makeFactory();
42
+ const sm = new sessionManager_1.SessionManager(factory, { scrollbackLimit: 10 });
43
+ const id = sm.open({ cols: 80, rows: 24 });
44
+ const h1 = sm.attach(id, () => { });
45
+ h1.detach();
46
+ ptys[0].emitData('0123456789ABCDEF'); // 16 chars > limit 10
47
+ const h2 = sm.attach(id, () => { });
48
+ expect(h2.replay).toBe('6789ABCDEF'); // only the last 10 chars survive
49
+ expect(ptys[0].killed).toBe(false);
50
+ });
51
+ it('last-attach-wins: prior sink gets takeover and its handle stops writing', () => {
52
+ const { factory, ptys } = makeFactory();
53
+ const sm = new sessionManager_1.SessionManager(factory);
54
+ const id = sm.open({ cols: 80, rows: 24 });
55
+ const firstFrames = [];
56
+ const h1 = sm.attach(id, (f) => firstFrames.push(f));
57
+ const h2 = sm.attach(id, () => { });
58
+ expect(firstFrames).toContainEqual({ t: 'takeover' });
59
+ h1.write('evil\r'); // stale handle must be inert (ADR-0028 input invariant)
60
+ expect(ptys[0].written).toEqual([]);
61
+ h2.write('ok\r');
62
+ expect(ptys[0].written).toEqual(['ok\r']);
63
+ });
64
+ it('forwards resize from the live handle only', () => {
65
+ const { factory, ptys } = makeFactory();
66
+ const sm = new sessionManager_1.SessionManager(factory);
67
+ const id = sm.open({ cols: 80, rows: 24 });
68
+ const h1 = sm.attach(id, () => { });
69
+ const h2 = sm.attach(id, () => { });
70
+ h1.resize(100, 40); // stale
71
+ h2.resize(120, 50);
72
+ expect(ptys[0].resizes).toEqual([{ cols: 120, rows: 50 }]);
73
+ });
74
+ it('close kills the PTY and removes the session', () => {
75
+ const { factory, ptys } = makeFactory();
76
+ const sm = new sessionManager_1.SessionManager(factory);
77
+ const id = sm.open({ cols: 80, rows: 24 });
78
+ sm.close(id);
79
+ expect(ptys[0].killed).toBe(true);
80
+ expect(sm.list()).toEqual([]);
81
+ expect(() => sm.attach(id, () => { })).toThrow(/unknown session/i);
82
+ });
83
+ it('supports multiple concurrent sessions with independent routing', () => {
84
+ const { factory, ptys } = makeFactory();
85
+ const sm = new sessionManager_1.SessionManager(factory);
86
+ const a = sm.open({ cols: 80, rows: 24 });
87
+ const b = sm.open({ cols: 80, rows: 24 });
88
+ const ha = sm.attach(a, () => { });
89
+ const hb = sm.attach(b, () => { });
90
+ ha.write('to-a');
91
+ hb.write('to-b');
92
+ expect(ptys[0].written).toEqual(['to-a']);
93
+ expect(ptys[1].written).toEqual(['to-b']);
94
+ expect(sm.list().sort()).toEqual([a, b].sort());
95
+ });
96
+ it('exposes no PTY write path other than the live attachment handle', () => {
97
+ const sm = new sessionManager_1.SessionManager(makeFactory().factory);
98
+ // Public surface check: SessionManager itself has no input/write/send method —
99
+ // writing requires the handle returned by attach() (ADR-0028).
100
+ expect(sm.write).toBeUndefined();
101
+ expect(sm.input).toBeUndefined();
102
+ expect(sm.send).toBeUndefined();
103
+ });
104
+ });
@@ -62,8 +62,8 @@ describe('saveConfig()', () => {
62
62
  mockedFs.writeFileSync.mockReturnValue(undefined);
63
63
  const config = { apiKey: 'key', apiUrl: 'http://test' };
64
64
  (0, config_1.saveConfig)(config);
65
- expect(mockedFs.mkdirSync).toHaveBeenCalledWith(expect.any(String), { recursive: true });
66
- expect(mockedFs.writeFileSync).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('"apiKey": "key"'));
65
+ expect(mockedFs.mkdirSync).toHaveBeenCalledWith(expect.any(String), { recursive: true, mode: 0o700 });
66
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('"apiKey": "key"'), { mode: 0o600 });
67
67
  });
68
68
  });
69
69
  describe('requireConfig()', () => {
@@ -94,6 +94,6 @@ describe('getApiUrl()', () => {
94
94
  });
95
95
  it('falls back to default when no config or env', () => {
96
96
  mockedFs.readFileSync.mockImplementation(() => { throw new Error('ENOENT'); });
97
- expect((0, config_1.getApiUrl)()).toBe('http://localhost:3001/api/marketplace');
97
+ expect((0, config_1.getApiUrl)()).toBe('http://localhost:3002/api/v1');
98
98
  });
99
99
  });
@@ -0,0 +1,11 @@
1
+ export interface BridgeConfig {
2
+ connectToken: string;
3
+ }
4
+ export interface DiscoveryInfo {
5
+ port: number;
6
+ sessionToken: string;
7
+ }
8
+ export declare function saveBridgeConfig(config: BridgeConfig, dir?: string): void;
9
+ export declare function loadBridgeConfig(dir?: string): BridgeConfig | null;
10
+ export declare function writeDiscoveryFile(info: DiscoveryInfo, dir?: string): void;
11
+ export declare function bridgeLogPath(dir?: string): string;
@@ -0,0 +1,79 @@
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.saveBridgeConfig = saveBridgeConfig;
37
+ exports.loadBridgeConfig = loadBridgeConfig;
38
+ exports.writeDiscoveryFile = writeDiscoveryFile;
39
+ exports.bridgeLogPath = bridgeLogPath;
40
+ /**
41
+ * bct-05 — bridge config + discovery.
42
+ *
43
+ * Lives under the CLI's existing config home (~/.wen-market/) rather than a
44
+ * second dotdir: one binary, one home. Two files:
45
+ * - bridge-config.json (0600): the Connect Token. Never logged.
46
+ * - bridge.json (0600): per-run discovery (port + session token) for
47
+ * local tooling/debugging. The BROWSER cannot read files — the pane
48
+ * discovers the bridge via the CORS-pinned HTTP discovery endpoint on
49
+ * the known default port instead (see server.ts).
50
+ */
51
+ const fs = __importStar(require("fs"));
52
+ const os = __importStar(require("os"));
53
+ const path = __importStar(require("path"));
54
+ const DEFAULT_DIR = path.join(os.homedir(), '.wen-market');
55
+ const CONFIG_NAME = 'bridge-config.json';
56
+ const DISCOVERY_NAME = 'bridge.json';
57
+ function ensureDir(dir) {
58
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
59
+ }
60
+ function saveBridgeConfig(config, dir = DEFAULT_DIR) {
61
+ ensureDir(dir);
62
+ fs.writeFileSync(path.join(dir, CONFIG_NAME), JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
63
+ }
64
+ function loadBridgeConfig(dir = DEFAULT_DIR) {
65
+ try {
66
+ return JSON.parse(fs.readFileSync(path.join(dir, CONFIG_NAME), 'utf-8'));
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ function writeDiscoveryFile(info, dir = DEFAULT_DIR) {
73
+ ensureDir(dir);
74
+ fs.writeFileSync(path.join(dir, DISCOVERY_NAME), JSON.stringify({ v: 1, ...info }, null, 2) + '\n', { mode: 0o600 });
75
+ }
76
+ function bridgeLogPath(dir = DEFAULT_DIR) {
77
+ ensureDir(dir);
78
+ return path.join(dir, 'bridge.log');
79
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * bct-05 — idempotent MCP registration into the user's Claude Code.
3
+ *
4
+ * Writes to someone else's machine config, so it is loud and idempotent:
5
+ * checks `claude mcp list` first, prints exactly what it ran or skipped,
6
+ * and a missing `claude` binary is a warning, never a crash (the bridge
7
+ * still serves terminals without MCP).
8
+ */
9
+ export type ExecRunner = (cmd: string, args: string[]) => Promise<{
10
+ stdout: string;
11
+ code: number;
12
+ }>;
13
+ export interface McpRegisterOpts {
14
+ connectToken: string;
15
+ apiUrl: string;
16
+ run: ExecRunner;
17
+ /** Set when a fresh token was just provided: an existing registration
18
+ * carries the OLD token env and must be replaced, not skipped. */
19
+ forceUpdate?: boolean;
20
+ }
21
+ export interface McpRegisterResult {
22
+ action: 'registered' | 're-registered' | 'already-registered' | 'claude-not-found';
23
+ command?: string;
24
+ }
25
+ export declare const MCP_SERVER_NAME = "wenrwa-marketplace";
26
+ /** Connect Tokens must never appear in output — redact before printing. */
27
+ export declare function redactToken(text: string): string;
28
+ export declare function registerMcpServer(opts: McpRegisterOpts): Promise<McpRegisterResult>;