@wenrwa/marketplace-cli 0.7.7 → 0.7.8

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 @@
1
+ export {};
@@ -0,0 +1,250 @@
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
+ * Screenshot drop — `attach-screenshot {rid, sessionId, name}` (local bridge
41
+ * only). The browser cannot read a dropped macOS screenshot thumbnail, so the
42
+ * bridge reads `<screenshot folder>/<name>` once macOS has written it.
43
+ *
44
+ * Fake fs + fake clock: the folder is a Map, sleep advances the clock, so the
45
+ * ~6s poll runs instantly and deterministically. The attachment WRITE is real
46
+ * (temp dir), so the bytes on disk are compared with the bytes "dropped".
47
+ */
48
+ const ws_1 = __importDefault(require("ws"));
49
+ const fs = __importStar(require("fs"));
50
+ const os = __importStar(require("os"));
51
+ const path = __importStar(require("path"));
52
+ const server_1 = require("../bridge/server");
53
+ const relayClient_1 = require("../bridge/relayClient");
54
+ const sessionManager_1 = require("../bridge/sessionManager");
55
+ const attachmentWriter_1 = require("../bridge/attachmentWriter");
56
+ const screenshotAttach_1 = require("../bridge/screenshotAttach");
57
+ class FakePty {
58
+ written = [];
59
+ write(d) { this.written.push(d); }
60
+ resize() { }
61
+ kill() { }
62
+ onData() { }
63
+ onExit() { }
64
+ }
65
+ const PNG = Buffer.from('89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d4944415478da63f8cfc0f01f0005000201a5f3b4d60000000049454e44ae426082', 'hex');
66
+ const HOME = '/Users/me';
67
+ const NAME = 'Screenshot 2026-10-04 at 9.41.12 AM.png';
68
+ /** A fake screenshot folder: files appear at a given fake time. */
69
+ function fakeDeps(opts) {
70
+ let clock = 0;
71
+ const reads = [];
72
+ const deps = {
73
+ platform: opts.platform ?? 'darwin',
74
+ homedir: () => HOME,
75
+ readScreencaptureLocation: async () => (opts.location === undefined ? null : opts.location),
76
+ stat: (p) => {
77
+ const f = opts.files?.[p];
78
+ if (!f || clock < (f.at ?? 0))
79
+ return null;
80
+ return { isFile: f.isFile ?? true, size: f.data.length };
81
+ },
82
+ readFile: (p) => { reads.push(p); return opts.files[p].data; },
83
+ sleep: async (ms) => { clock += ms; },
84
+ now: () => clock,
85
+ attachment: attachmentWriter_1.realAttachmentDeps,
86
+ };
87
+ return { deps, reads, clock: () => clock };
88
+ }
89
+ let tmp;
90
+ const sessions = () => ({ list: () => ['agent-1'], cwdOf: () => tmp });
91
+ beforeEach(() => { tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'shot-'))); });
92
+ afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
93
+ describe('screenshot folder resolution', () => {
94
+ it('defaults to ~/Desktop, expands ~, ignores relative junk', () => {
95
+ expect((0, screenshotAttach_1.resolveScreenshotFolder)(null, HOME)).toBe('/Users/me/Desktop');
96
+ expect((0, screenshotAttach_1.resolveScreenshotFolder)(' \n', HOME)).toBe('/Users/me/Desktop');
97
+ expect((0, screenshotAttach_1.resolveScreenshotFolder)('~/Pictures/Screenshots\n', HOME)).toBe('/Users/me/Pictures/Screenshots');
98
+ expect((0, screenshotAttach_1.resolveScreenshotFolder)('~', HOME)).toBe('/Users/me');
99
+ expect((0, screenshotAttach_1.resolveScreenshotFolder)('/Volumes/Shots/', HOME)).toBe('/Volumes/Shots/');
100
+ expect((0, screenshotAttach_1.resolveScreenshotFolder)('Pictures', HOME)).toBe('/Users/me/Desktop');
101
+ });
102
+ it('accepts only a plain image basename', () => {
103
+ expect((0, screenshotAttach_1.isSafeScreenshotName)(NAME)).toBe(true);
104
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('shot.JPEG')).toBe(true);
105
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('shot.webp')).toBe(true);
106
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('../secret.png')).toBe(false);
107
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('a/b.png')).toBe(false);
108
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('a\\b.png')).toBe(false);
109
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('..png')).toBe(false);
110
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('.hidden.png')).toBe(false);
111
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('x.png\0.txt')).toBe(false);
112
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('shot.heic')).toBe(false);
113
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('notes.txt')).toBe(false);
114
+ expect((0, screenshotAttach_1.isSafeScreenshotName)('noext')).toBe(false);
115
+ expect((0, screenshotAttach_1.isSafeScreenshotName)(42)).toBe(false);
116
+ });
117
+ });
118
+ describe('handleScreenshotFrame', () => {
119
+ it('finds the file after macOS writes it (delay) and saves it via the attach writer', async () => {
120
+ const file = path.join('/Users/me/Pictures/Shots', NAME);
121
+ const f = fakeDeps({ location: '~/Pictures/Shots', files: { [file]: { data: PNG, at: 1500 } } });
122
+ const reply = await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r1', sessionId: 'agent-1', name: NAME }, sessions(), f.deps);
123
+ expect(reply).toMatchObject({ t: 'file-attached', rid: 'r1', sessionId: 'agent-1', ok: true, bytes: PNG.length });
124
+ expect(f.clock()).toBe(1500);
125
+ const saved = String(reply.path);
126
+ expect(path.dirname(saved)).toBe(path.join(tmp, attachmentWriter_1.ATTACHMENTS_DIR));
127
+ expect(saved.endsWith('-Screenshot-2026-10-04-at-9.41.12-AM.png')).toBe(true);
128
+ expect(fs.readFileSync(saved).equals(PNG)).toBe(true);
129
+ expect(f.reads).toEqual([file]);
130
+ });
131
+ it('defaults to ~/Desktop when no location is set', async () => {
132
+ const f = fakeDeps({ files: { [path.join('/Users/me/Desktop', NAME)]: { data: PNG } } });
133
+ const reply = await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r', sessionId: 'agent-1', name: NAME }, sessions(), f.deps);
134
+ expect(reply.ok).toBe(true);
135
+ expect(f.clock()).toBe(0);
136
+ });
137
+ it('gives up with not-found after ~6s', async () => {
138
+ const f = fakeDeps({});
139
+ expect(await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r', sessionId: 'agent-1', name: NAME }, sessions(), f.deps))
140
+ .toEqual({ t: 'file-attached', rid: 'r', sessionId: 'agent-1', ok: false, reason: 'not-found' });
141
+ expect(f.clock()).toBe(screenshotAttach_1.SCREENSHOT_POLL_MS);
142
+ expect(fs.existsSync(path.join(tmp, '.hiveop'))).toBe(false);
143
+ });
144
+ it('rejects path traversal without touching the disk', async () => {
145
+ const f = fakeDeps({ files: { '/Users/me/secret.png': { data: PNG } } });
146
+ for (const name of ['../secret.png', '/Users/me/secret.png', 'sub/../../secret.png']) {
147
+ expect(await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r', sessionId: 'agent-1', name }, sessions(), f.deps))
148
+ .toMatchObject({ ok: false, reason: 'bad-name' });
149
+ }
150
+ expect(f.reads).toEqual([]);
151
+ });
152
+ it('rejects non-image names and non-image bytes', async () => {
153
+ const file = path.join('/Users/me/Desktop', 'shot.png');
154
+ const f = fakeDeps({ files: { [file]: { data: Buffer.from('not an image at all') } } });
155
+ expect(await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r', sessionId: 'agent-1', name: 'notes.txt' }, sessions(), f.deps))
156
+ .toMatchObject({ ok: false, reason: 'bad-name' });
157
+ expect(await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r', sessionId: 'agent-1', name: 'shot.heic' }, sessions(), f.deps))
158
+ .toMatchObject({ ok: false, reason: 'bad-name' });
159
+ expect(await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r', sessionId: 'agent-1', name: 'shot.png' }, sessions(), f.deps))
160
+ .toMatchObject({ ok: false, reason: 'not-image' });
161
+ expect(fs.existsSync(path.join(tmp, '.hiveop'))).toBe(false);
162
+ });
163
+ it('refuses a symlink/dir by that name, an oversize file, an unknown session, and non-macOS', async () => {
164
+ const file = path.join('/Users/me/Desktop', 'shot.png');
165
+ const link = fakeDeps({ files: { [file]: { data: PNG, isFile: false } } });
166
+ expect(await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r', sessionId: 'agent-1', name: 'shot.png' }, sessions(), link.deps))
167
+ .toMatchObject({ ok: false, reason: 'not-image' });
168
+ expect(link.reads).toEqual([]);
169
+ const big = fakeDeps({ files: { [file]: { data: Buffer.alloc(attachmentWriter_1.MAX_ATTACHMENT_BYTES + 1) } } });
170
+ expect(await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r', sessionId: 'agent-1', name: 'shot.png' }, sessions(), big.deps))
171
+ .toMatchObject({ ok: false, reason: 'too-large' });
172
+ expect(big.reads).toEqual([]);
173
+ const ok = fakeDeps({ files: { [file]: { data: PNG } } });
174
+ expect(await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r', sessionId: 'nope', name: 'shot.png' }, sessions(), ok.deps))
175
+ .toMatchObject({ ok: false, reason: 'no-session' });
176
+ const linux = fakeDeps({ platform: 'linux', files: { [file]: { data: PNG } } });
177
+ expect(await (0, screenshotAttach_1.handleScreenshotFrame)({ rid: 'r', sessionId: 'agent-1', name: 'shot.png' }, sessions(), linux.deps))
178
+ .toMatchObject({ ok: false, reason: 'not-supported' });
179
+ });
180
+ });
181
+ function frameQueue(ws) {
182
+ const frames = [];
183
+ const waiters = [];
184
+ ws.on('message', (raw) => {
185
+ const f = JSON.parse(String(raw));
186
+ const w = waiters.shift();
187
+ if (w)
188
+ w(f);
189
+ else
190
+ frames.push(f);
191
+ });
192
+ return { next: () => (frames.length ? Promise.resolve(frames.shift()) : new Promise((r) => waiters.push(r))) };
193
+ }
194
+ describe('local bridge server — attach-screenshot frame', () => {
195
+ let handle;
196
+ afterEach(async () => { await handle?.close(); });
197
+ it('answers over the loopback socket and advertises attach-screenshot on macOS', async () => {
198
+ const pty = new FakePty();
199
+ const mgr = new sessionManager_1.SessionManager({ spawn: () => pty });
200
+ const f = fakeDeps({ files: { [path.join('/Users/me/Desktop', NAME)]: { data: PNG, at: 500 } } });
201
+ handle = await (0, server_1.startBridgeServer)({ sessionToken: 'tok', allowedOrigin: 'https://app.hiveop.io', sessions: mgr, screenshotDeps: f.deps });
202
+ const sessionId = mgr.open({ cols: 80, rows: 24, cwd: tmp, id: 'agent-1' });
203
+ const res = await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: 'https://app.hiveop.io' } });
204
+ expect((await res.json()).capabilities).toEqual(expect.arrayContaining(['terminal-attach', 'attach-screenshot']));
205
+ const ws = new ws_1.default(`ws://127.0.0.1:${handle.port}`, { headers: { origin: 'https://app.hiveop.io' } });
206
+ await new Promise((r) => ws.on('open', r));
207
+ const q = frameQueue(ws);
208
+ ws.send(JSON.stringify({ v: 1, token: 'tok' }));
209
+ ws.send(JSON.stringify({ t: 'attach-screenshot', rid: 's1', sessionId, name: NAME }));
210
+ const reply = await q.next();
211
+ expect(reply).toMatchObject({ t: 'file-attached', rid: 's1', sessionId, ok: true });
212
+ expect(fs.readFileSync(reply.path).equals(PNG)).toBe(true);
213
+ expect(pty.written).toEqual([]);
214
+ ws.close();
215
+ });
216
+ it('does not advertise attach-screenshot off macOS', async () => {
217
+ const mgr = new sessionManager_1.SessionManager({ spawn: () => new FakePty() });
218
+ handle = await (0, server_1.startBridgeServer)({
219
+ sessionToken: 'tok', allowedOrigin: 'https://app.hiveop.io', sessions: mgr,
220
+ screenshotDeps: fakeDeps({ platform: 'linux' }).deps,
221
+ });
222
+ const res = await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: 'https://app.hiveop.io' } });
223
+ const caps = (await res.json()).capabilities;
224
+ expect(caps).toContain('terminal-attach');
225
+ expect(caps).not.toContain('attach-screenshot');
226
+ });
227
+ });
228
+ describe('relay client (hosted) — never answers attach-screenshot', () => {
229
+ it('sends no file-attached for the frame', async () => {
230
+ const { WebSocketServer } = await Promise.resolve().then(() => __importStar(require('ws')));
231
+ const wss = new WebSocketServer({ port: 0, host: '127.0.0.1' });
232
+ await new Promise((r) => wss.on('listening', r));
233
+ const port = wss.address().port;
234
+ const conn = new Promise((r) => wss.on('connection', r));
235
+ const mgr = new sessionManager_1.SessionManager({ spawn: () => new FakePty() });
236
+ mgr.open({ cols: 80, rows: 24, cwd: tmp, id: 'agent-H' });
237
+ const client = (0, relayClient_1.startRelayClient)({ relayUrl: `ws://127.0.0.1:${port}`, relayToken: 'tok', sessions: mgr });
238
+ const ws = await conn;
239
+ const got = [];
240
+ ws.on('message', (raw) => got.push(JSON.parse(String(raw))));
241
+ await new Promise((r) => setTimeout(r, 50)); // auth frame
242
+ ws.send(JSON.stringify({ t: 'attach-screenshot', rid: 'h1', sessionId: 'agent-H', name: 'shot.png' }));
243
+ await new Promise((r) => setTimeout(r, 200));
244
+ expect(got.filter((f) => f.t === 'file-attached')).toEqual([]);
245
+ client.close();
246
+ for (const c of wss.clients)
247
+ c.terminate();
248
+ await new Promise((r) => wss.close(r));
249
+ });
250
+ });
@@ -0,0 +1,33 @@
1
+ import { AttachmentDeps } from './attachmentWriter';
2
+ export declare const SCREENSHOT_POLL_MS = 6000;
3
+ export declare const SCREENSHOT_POLL_INTERVAL_MS = 250;
4
+ export type ScreenshotReason = 'not-supported' | 'bad-name' | 'not-found' | 'not-image' | 'too-large' | 'no-session' | 'error';
5
+ export interface ScreenshotDeps {
6
+ platform: NodeJS.Platform;
7
+ homedir: () => string;
8
+ /** Raw output of `defaults read com.apple.screencapture location`, or null when unset/failed. */
9
+ readScreencaptureLocation: () => Promise<string | null>;
10
+ /** lstat-style: null when absent. Never follows a symlink. */
11
+ stat: (p: string) => {
12
+ isFile: boolean;
13
+ size: number;
14
+ } | null;
15
+ readFile: (p: string) => Buffer;
16
+ sleep: (ms: number) => Promise<void>;
17
+ now: () => number;
18
+ attachment: AttachmentDeps;
19
+ }
20
+ export declare const realScreenshotDeps: ScreenshotDeps;
21
+ /** `~` expansion + absolute check; anything odd falls back to ~/Desktop (macOS's own default). */
22
+ export declare function resolveScreenshotFolder(raw: string | null, home: string): string;
23
+ /** A plain image basename: no path separators, no traversal, no NUL, image extension. */
24
+ export declare function isSafeScreenshotName(name: unknown): name is string;
25
+ /** The `attach-screenshot` frame → `file-attached` reply. */
26
+ export declare function handleScreenshotFrame(frame: {
27
+ rid?: unknown;
28
+ sessionId?: unknown;
29
+ name?: unknown;
30
+ }, sessions: {
31
+ list(): string[];
32
+ cwdOf(id: string): string | undefined;
33
+ }, deps?: ScreenshotDeps): Promise<Record<string, unknown>>;
@@ -0,0 +1,171 @@
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 ? { ...head, ok: true, path: r.path, bytes: r.bytes } : fail(r.reason);
154
+ }
155
+ // Present but not (yet) an image — maybe still being written.
156
+ last = 'not-image';
157
+ }
158
+ }
159
+ else if (st && !st.isFile) {
160
+ // A directory or symlink by that name is never a screenshot.
161
+ return fail('not-image');
162
+ }
163
+ if (deps.now() >= deadline)
164
+ return fail(last);
165
+ await deps.sleep(exports.SCREENSHOT_POLL_INTERVAL_MS);
166
+ }
167
+ }
168
+ catch {
169
+ return fail('error');
170
+ }
171
+ }
@@ -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
@@ -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;
@@ -938,6 +941,13 @@ function startBridgeServer(opts) {
938
941
  // input. Shared handler with relayClient.ts (both lanes).
939
942
  send(ws, (0, attachmentWriter_1.handleAttachFrame)(frame, opts.sessions));
940
943
  break;
944
+ case 'attach-screenshot':
945
+ // Local lane ONLY (never relayClient.ts): reads the user's macOS
946
+ // screenshot folder by a validated basename, then the same writer.
947
+ void (0, screenshotAttach_1.handleScreenshotFrame)(frame, opts.sessions, opts.screenshotDeps ?? screenshotAttach_1.realScreenshotDeps)
948
+ .then((reply) => send(ws, reply))
949
+ .catch(() => send(ws, { t: 'file-attached', rid: frame.rid, sessionId: frame.sessionId, ok: false, reason: 'error' }));
950
+ break;
941
951
  default:
942
952
  send(ws, { t: 'error', message: 'unknown frame type' });
943
953
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenrwa/marketplace-cli",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
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",