@wenrwa/marketplace-cli 0.7.0 → 0.7.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,194 @@
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
+ /**
37
+ * Terminal attach — `attach-file` saves an image next to the session's agent
38
+ * and answers `file-attached` with its absolute path. Both lanes: the local
39
+ * loopback server and the relay client (hosted container) share one handler,
40
+ * and both are exercised here end to end against a REAL temp folder so the
41
+ * bytes on disk are compared with the bytes sent.
42
+ */
43
+ const ws_1 = __importStar(require("ws"));
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
47
+ const server_1 = require("../bridge/server");
48
+ const relayClient_1 = require("../bridge/relayClient");
49
+ const sessionManager_1 = require("../bridge/sessionManager");
50
+ const attachmentWriter_1 = require("../bridge/attachmentWriter");
51
+ class FakePty {
52
+ written = [];
53
+ write(d) { this.written.push(d); }
54
+ resize() { }
55
+ kill() { }
56
+ onData() { }
57
+ onExit() { }
58
+ }
59
+ // A real (tiny) PNG: signature + IHDR + IDAT + IEND for a 1x1 pixel, plus
60
+ // every byte value so a lossy encoding would be caught.
61
+ const PNG_1PX = Buffer.from('89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d4944415478da63f8cfc0f01f0005000201a5f3b4d60000000049454e44ae426082', 'hex');
62
+ const PNG = Buffer.concat([PNG_1PX, Buffer.from(Array.from({ length: 256 }, (_, i) => i))]);
63
+ function frameQueue(ws) {
64
+ const frames = [];
65
+ const waiters = [];
66
+ ws.on('message', (raw) => {
67
+ const f = JSON.parse(String(raw));
68
+ const w = waiters.shift();
69
+ if (w)
70
+ w(f);
71
+ else
72
+ frames.push(f);
73
+ });
74
+ return { next: () => (frames.length ? Promise.resolve(frames.shift()) : new Promise((r) => waiters.push(r))) };
75
+ }
76
+ let tmp;
77
+ beforeEach(() => { tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'attach-'))); });
78
+ afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
79
+ describe('attachmentWriter — pure rules', () => {
80
+ it('sniffs the type from magic bytes, never the name', () => {
81
+ expect((0, attachmentWriter_1.sniffImageExt)(PNG)).toBe('png');
82
+ expect((0, attachmentWriter_1.sniffImageExt)(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0, 0]))).toBe('jpg');
83
+ expect((0, attachmentWriter_1.sniffImageExt)(Buffer.from('GIF89a......'))).toBe('gif');
84
+ expect((0, attachmentWriter_1.sniffImageExt)(Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBPVP8 ')]))).toBe('webp');
85
+ expect((0, attachmentWriter_1.sniffImageExt)(Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"/>'))).toBeNull();
86
+ expect((0, attachmentWriter_1.sniffImageExt)(Buffer.from('%PDF-1.7'))).toBeNull();
87
+ });
88
+ it('makes a stem that needs no escaping and cannot traverse', () => {
89
+ expect((0, attachmentWriter_1.safeAttachmentStem)('Screenshot 2026-10-04 at 9.41.12 AM.png')).toBe('Screenshot-2026-10-04-at-9.41.12-AM');
90
+ expect((0, attachmentWriter_1.safeAttachmentStem)('../../etc/passwd')).toBe('passwd');
91
+ expect((0, attachmentWriter_1.safeAttachmentStem)('..\\..\\x.png')).toBe('x');
92
+ expect((0, attachmentWriter_1.safeAttachmentStem)('.hidden.png')).toBe('hidden');
93
+ expect((0, attachmentWriter_1.safeAttachmentStem)('日本語.png')).toBe('image');
94
+ expect((0, attachmentWriter_1.safeAttachmentStem)('')).toBe('image');
95
+ expect((0, attachmentWriter_1.safeAttachmentStem)('a'.repeat(90) + '.png')).toHaveLength(40);
96
+ });
97
+ it('writes byte-identical bytes under .hiveop/attachments with a self-ignoring .gitignore', () => {
98
+ const r = (0, attachmentWriter_1.writeAttachment)({ root: tmp, name: 'shot one.png', dataBase64: PNG.toString('base64') });
99
+ expect(r.ok).toBe(true);
100
+ if (!r.ok)
101
+ return;
102
+ expect(path.dirname(r.path)).toBe(path.join(tmp, attachmentWriter_1.ATTACHMENTS_DIR));
103
+ expect(path.basename(r.path)).toMatch(/^\d{8}-\d{6}-[0-9a-f]{4}-shot-one\.png$/);
104
+ expect(fs.readFileSync(r.path).equals(PNG)).toBe(true);
105
+ expect(fs.readFileSync(path.join(tmp, attachmentWriter_1.ATTACHMENTS_DIR, '.gitignore'), 'utf8')).toBe('*\n');
106
+ // No temp files left behind.
107
+ expect(fs.readdirSync(path.join(tmp, attachmentWriter_1.ATTACHMENTS_DIR)).filter((f) => f.endsWith('.tmp'))).toEqual([]);
108
+ });
109
+ it('names the extension from the bytes even when the name lies', () => {
110
+ const r = (0, attachmentWriter_1.writeAttachment)({ root: tmp, name: 'photo.jpg', dataBase64: PNG.toString('base64') });
111
+ expect(r.ok && r.path.endsWith('-photo.png')).toBe(true);
112
+ });
113
+ it('refuses non-images and oversize files without writing anything', () => {
114
+ expect((0, attachmentWriter_1.writeAttachment)({ root: tmp, name: 'x.png', dataBase64: Buffer.from('not an image').toString('base64') }))
115
+ .toEqual({ ok: false, reason: 'not-image' });
116
+ const big = Buffer.concat([PNG_1PX, Buffer.alloc(attachmentWriter_1.MAX_ATTACHMENT_BYTES)]);
117
+ expect((0, attachmentWriter_1.writeAttachment)({ root: tmp, name: 'x.png', dataBase64: big.toString('base64') }))
118
+ .toEqual({ ok: false, reason: 'too-large' });
119
+ expect(fs.existsSync(path.join(tmp, '.hiveop'))).toBe(false);
120
+ });
121
+ it('refuses to follow a planted .hiveop symlink out of the project', () => {
122
+ const outside = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'outside-')));
123
+ try {
124
+ fs.mkdirSync(path.join(outside, 'attachments'));
125
+ fs.symlinkSync(outside, path.join(tmp, '.hiveop'));
126
+ expect((0, attachmentWriter_1.writeAttachment)({ root: tmp, name: 'x.png', dataBase64: PNG.toString('base64') }))
127
+ .toEqual({ ok: false, reason: 'error' });
128
+ expect(fs.readdirSync(path.join(outside, 'attachments'))).toEqual([]);
129
+ }
130
+ finally {
131
+ fs.rmSync(outside, { recursive: true, force: true });
132
+ }
133
+ });
134
+ it('refuses an unknown session (the session decides the folder, not the frame)', () => {
135
+ const sessions = { list: () => ['a'], cwdOf: () => tmp };
136
+ expect((0, attachmentWriter_1.handleAttachFrame)({ rid: 'r', sessionId: 'b', data: PNG.toString('base64') }, sessions))
137
+ .toEqual({ t: 'file-attached', rid: 'r', sessionId: 'b', ok: false, reason: 'no-session' });
138
+ });
139
+ });
140
+ describe('local bridge server — attach-file frame', () => {
141
+ let handle;
142
+ let sessions;
143
+ let ptys;
144
+ beforeEach(async () => {
145
+ ptys = [];
146
+ const factory = { spawn: () => { const p = new FakePty(); ptys.push(p); return p; } };
147
+ sessions = new sessionManager_1.SessionManager(factory);
148
+ handle = await (0, server_1.startBridgeServer)({ sessionToken: 'tok', allowedOrigin: 'https://app.hiveop.io', sessions });
149
+ });
150
+ afterEach(async () => { await handle.close(); });
151
+ it('writes next to the session and replies with the absolute path; nothing reaches the PTY', async () => {
152
+ const sessionId = sessions.open({ cols: 80, rows: 24, cwd: tmp, id: 'agent-1' });
153
+ const ws = new ws_1.default(`ws://127.0.0.1:${handle.port}`, { headers: { origin: 'https://app.hiveop.io' } });
154
+ await new Promise((r) => ws.on('open', r));
155
+ const q = frameQueue(ws);
156
+ ws.send(JSON.stringify({ v: 1, token: 'tok' }));
157
+ ws.send(JSON.stringify({ t: 'attach-file', rid: 'r1', sessionId, name: 'Screen Shot.png', data: PNG.toString('base64') }));
158
+ const reply = await q.next();
159
+ expect(reply).toMatchObject({ t: 'file-attached', rid: 'r1', sessionId, ok: true, bytes: PNG.length });
160
+ expect(reply.path.startsWith(path.join(tmp, attachmentWriter_1.ATTACHMENTS_DIR) + path.sep)).toBe(true);
161
+ expect(fs.readFileSync(reply.path).equals(PNG)).toBe(true);
162
+ expect(ptys[0].written).toEqual([]);
163
+ ws.close();
164
+ });
165
+ it('advertises the terminal-attach capability in /discovery', async () => {
166
+ const res = await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: 'https://app.hiveop.io' } });
167
+ const body = await res.json();
168
+ expect(body.capabilities).toContain('terminal-attach');
169
+ });
170
+ });
171
+ describe('relay client (hosted container) — attach-file frame', () => {
172
+ it('writes next to the relay session and echoes rid + sessionId for demux', async () => {
173
+ const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
174
+ await new Promise((r) => wss.on('listening', r));
175
+ const port = wss.address().port;
176
+ const conn = new Promise((r) => wss.on('connection', r));
177
+ const sessions = new sessionManager_1.SessionManager({ spawn: () => new FakePty() });
178
+ sessions.open({ cols: 80, rows: 24, cwd: tmp, id: 'agent-H' });
179
+ const client = (0, relayClient_1.startRelayClient)({ relayUrl: `ws://127.0.0.1:${port}`, relayToken: 'tok', sessions });
180
+ const ws = await conn;
181
+ const q = frameQueue(ws);
182
+ await q.next(); // auth
183
+ ws.send(JSON.stringify({ t: 'attach-file', rid: 'h1', sessionId: 'agent-H', name: 'x.png', data: PNG.toString('base64') }));
184
+ const reply = await q.next();
185
+ expect(reply).toMatchObject({ t: 'file-attached', rid: 'h1', sessionId: 'agent-H', ok: true });
186
+ expect(fs.readFileSync(reply.path).equals(PNG)).toBe(true);
187
+ ws.send(JSON.stringify({ t: 'attach-file', rid: 'h2', sessionId: 'agent-H', name: 'x.txt', data: Buffer.from('hello').toString('base64') }));
188
+ expect(await q.next()).toMatchObject({ t: 'file-attached', rid: 'h2', ok: false, reason: 'not-image' });
189
+ client.close();
190
+ for (const c of wss.clients)
191
+ c.terminate();
192
+ await new Promise((r) => wss.close(r));
193
+ });
194
+ });
@@ -0,0 +1,54 @@
1
+ export declare const ATTACHMENTS_DIR: string;
2
+ export declare const MAX_ATTACHMENT_BYTES: number;
3
+ export type ImageExt = 'png' | 'jpg' | 'gif' | 'webp';
4
+ export type AttachmentReason = 'not-image' | 'too-large' | 'no-session' | 'error';
5
+ export type AttachmentResult = {
6
+ ok: true;
7
+ path: string;
8
+ bytes: number;
9
+ } | {
10
+ ok: false;
11
+ reason: AttachmentReason;
12
+ };
13
+ /** The image type, read from the bytes themselves — never from the name. */
14
+ export declare function sniffImageExt(buf: Buffer): ImageExt | null;
15
+ /**
16
+ * A filename stem that is safe on every filesystem AND needs no shell
17
+ * escaping in a prompt: letters, digits, dot, dash, underscore only; no
18
+ * leading dot; at most 40 chars; never empty.
19
+ */
20
+ export declare function safeAttachmentStem(name: string): string;
21
+ export interface AttachmentDeps {
22
+ mkdirp: (dir: string) => void;
23
+ exists: (p: string) => boolean;
24
+ realpath: (p: string) => string;
25
+ /** Real impl: temp file + rename, so Claude never reads half an image. */
26
+ writeFileAtomic: (p: string, data: Buffer) => void;
27
+ now: () => Date;
28
+ random: () => string;
29
+ }
30
+ export declare const realAttachmentDeps: AttachmentDeps;
31
+ /** Write one image under `<root>/.hiveop/attachments/` and return its absolute path. */
32
+ export declare function writeAttachment(input: {
33
+ root: string;
34
+ name: string;
35
+ dataBase64: string;
36
+ }, deps?: AttachmentDeps): AttachmentResult;
37
+ /** Where a session's attachments go: its project folder, else the bridge user's home. */
38
+ export declare function attachmentRootFor(sessions: {
39
+ list(): string[];
40
+ cwdOf(id: string): string | undefined;
41
+ }, sessionId: unknown): string | null;
42
+ /**
43
+ * The whole `attach-file` frame → `file-attached` reply, shared verbatim by
44
+ * the local server and the relay client so the two lanes cannot drift.
45
+ */
46
+ export declare function handleAttachFrame(frame: {
47
+ rid?: unknown;
48
+ sessionId?: unknown;
49
+ name?: unknown;
50
+ data?: unknown;
51
+ }, sessions: {
52
+ list(): string[];
53
+ cwdOf(id: string): string | undefined;
54
+ }, deps?: AttachmentDeps): Record<string, unknown>;
@@ -0,0 +1,172 @@
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.realAttachmentDeps = exports.MAX_ATTACHMENT_BYTES = exports.ATTACHMENTS_DIR = void 0;
37
+ exports.sniffImageExt = sniffImageExt;
38
+ exports.safeAttachmentStem = safeAttachmentStem;
39
+ exports.writeAttachment = writeAttachment;
40
+ exports.attachmentRootFor = attachmentRootFor;
41
+ exports.handleAttachFrame = handleAttachFrame;
42
+ /**
43
+ * Terminal attach — save a screenshot NEXT TO the agent that will read it.
44
+ *
45
+ * Claude Code attaches an image when that image's file path lands in its
46
+ * prompt. So the browser ships the image bytes over the pane's own socket
47
+ * (`attach-file` frame — the local bridge's loopback WS, or the relay into the
48
+ * hosted container; same frame, same handler, both lanes) and this module
49
+ * writes them into the SESSION's project folder:
50
+ *
51
+ * <session cwd>/.hiveop/attachments/<stamp>-<rand>-<safe-name>.<ext>
52
+ *
53
+ * Why there:
54
+ * - `.hiveop/` is already the bridge-owned dir inside a checkout
55
+ * (`.hiveop/context.md`), excluded from the tree scanner/watcher and from
56
+ * the checkout inventory's "unsaved work" count — no new convention.
57
+ * - It is inside Claude's cwd, so a follow-up Read of the file needs no
58
+ * out-of-project permission prompt on either lane.
59
+ * - The attachments dir carries its OWN `.gitignore` (`*`), so an agent's
60
+ * `git add -A` never commits screenshots — without editing the user's
61
+ * .gitignore.
62
+ *
63
+ * The browser's name and type are hints only: the extension comes from the
64
+ * file's magic bytes, and anything that is not png/jpeg/gif/webp is refused.
65
+ * The session — never the frame — decides the folder, so a pane can only
66
+ * write next to a session it may already address.
67
+ */
68
+ const fs = __importStar(require("fs"));
69
+ const os = __importStar(require("os"));
70
+ const path = __importStar(require("path"));
71
+ const crypto = __importStar(require("crypto"));
72
+ exports.ATTACHMENTS_DIR = path.join('.hiveop', 'attachments');
73
+ exports.MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
74
+ /** The image type, read from the bytes themselves — never from the name. */
75
+ function sniffImageExt(buf) {
76
+ if (buf.length >= 8 && buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
77
+ return 'png';
78
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
79
+ return 'jpg';
80
+ if (buf.length >= 6 && (buf.subarray(0, 6).toString('latin1') === 'GIF87a' || buf.subarray(0, 6).toString('latin1') === 'GIF89a'))
81
+ return 'gif';
82
+ if (buf.length >= 12 && buf.subarray(0, 4).toString('latin1') === 'RIFF' && buf.subarray(8, 12).toString('latin1') === 'WEBP')
83
+ return 'webp';
84
+ return null;
85
+ }
86
+ /**
87
+ * A filename stem that is safe on every filesystem AND needs no shell
88
+ * escaping in a prompt: letters, digits, dot, dash, underscore only; no
89
+ * leading dot; at most 40 chars; never empty.
90
+ */
91
+ function safeAttachmentStem(name) {
92
+ const base = String(name ?? '').split(/[\\/]/).pop() ?? '';
93
+ const stem = base.replace(/\.[A-Za-z0-9]{1,5}$/, '');
94
+ const cleaned = stem
95
+ .replace(/[^A-Za-z0-9._-]+/g, '-')
96
+ .replace(/-{2,}/g, '-')
97
+ .replace(/^[-.]+|[-.]+$/g, '')
98
+ .slice(0, 40)
99
+ .replace(/[-.]+$/g, '');
100
+ return cleaned || 'image';
101
+ }
102
+ function stamp(now) {
103
+ const p = (n) => String(n).padStart(2, '0');
104
+ return `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
105
+ }
106
+ exports.realAttachmentDeps = {
107
+ mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
108
+ exists: (p) => fs.existsSync(p),
109
+ realpath: (p) => fs.realpathSync(p),
110
+ writeFileAtomic: (p, data) => {
111
+ const tmp = `${p}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
112
+ fs.writeFileSync(tmp, data);
113
+ fs.renameSync(tmp, p);
114
+ },
115
+ now: () => new Date(),
116
+ random: () => crypto.randomBytes(2).toString('hex'),
117
+ };
118
+ /** Write one image under `<root>/.hiveop/attachments/` and return its absolute path. */
119
+ function writeAttachment(input, deps = exports.realAttachmentDeps) {
120
+ const b64 = typeof input.dataBase64 === 'string' ? input.dataBase64 : '';
121
+ // Checked on the ENCODED length first, so an oversized frame is refused
122
+ // without allocating its decoded buffer.
123
+ if (b64.length > Math.ceil(exports.MAX_ATTACHMENT_BYTES / 3) * 4 + 4)
124
+ return { ok: false, reason: 'too-large' };
125
+ const data = Buffer.from(b64, 'base64');
126
+ if (data.length > exports.MAX_ATTACHMENT_BYTES)
127
+ return { ok: false, reason: 'too-large' };
128
+ const ext = sniffImageExt(data);
129
+ if (!ext)
130
+ return { ok: false, reason: 'not-image' };
131
+ try {
132
+ const root = deps.realpath(input.root);
133
+ const dir = path.join(root, exports.ATTACHMENTS_DIR);
134
+ deps.mkdirp(dir);
135
+ // A checkout's `.hiveop` could be a symlink planted by the repo — refuse
136
+ // to follow it anywhere outside the project.
137
+ const realDir = deps.realpath(dir);
138
+ if (realDir !== dir && !realDir.startsWith(root + path.sep))
139
+ return { ok: false, reason: 'error' };
140
+ const ignore = path.join(realDir, '.gitignore');
141
+ if (!deps.exists(ignore))
142
+ deps.writeFileAtomic(ignore, Buffer.from('*\n'));
143
+ const file = path.join(realDir, `${stamp(deps.now())}-${deps.random()}-${safeAttachmentStem(input.name)}.${ext}`);
144
+ deps.writeFileAtomic(file, data);
145
+ return { ok: true, path: file, bytes: data.length };
146
+ }
147
+ catch {
148
+ return { ok: false, reason: 'error' };
149
+ }
150
+ }
151
+ /** Where a session's attachments go: its project folder, else the bridge user's home. */
152
+ function attachmentRootFor(sessions, sessionId) {
153
+ if (typeof sessionId !== 'string' || !sessions.list().includes(sessionId))
154
+ return null;
155
+ return sessions.cwdOf(sessionId) ?? os.homedir();
156
+ }
157
+ /**
158
+ * The whole `attach-file` frame → `file-attached` reply, shared verbatim by
159
+ * the local server and the relay client so the two lanes cannot drift.
160
+ */
161
+ function handleAttachFrame(frame, sessions, deps = exports.realAttachmentDeps) {
162
+ const head = {
163
+ t: 'file-attached',
164
+ rid: frame.rid,
165
+ ...(typeof frame.sessionId === 'string' ? { sessionId: frame.sessionId } : {}),
166
+ };
167
+ const root = attachmentRootFor(sessions, frame.sessionId);
168
+ if (!root)
169
+ return { ...head, ok: false, reason: 'no-session' };
170
+ const result = writeAttachment({ root, name: typeof frame.name === 'string' ? frame.name : 'image', dataBase64: typeof frame.data === 'string' ? frame.data : '' }, deps);
171
+ return result.ok ? { ...head, ok: true, path: result.path, bytes: result.bytes } : { ...head, ok: false, reason: result.reason };
172
+ }
@@ -110,6 +110,7 @@ const diskPreflight_1 = require("./diskPreflight");
110
110
  // hcs-05's checkoutReclaim executor for the actual delete. See
111
111
  // checkoutParkAndRemove.ts's doc comment for the full safety design.
112
112
  const checkoutParkAndRemove_1 = require("./checkoutParkAndRemove");
113
+ const attachmentWriter_1 = require("./attachmentWriter");
113
114
  const PROTOCOL_VERSION = 1;
114
115
  /**
115
116
  * fcct-27 — typed refusal when `resolveLaunch` names a binary this machine does
@@ -133,6 +134,8 @@ exports.SESSION_NOT_RELAY_OWNED = 'SESSION_NOT_RELAY_OWNED';
133
134
  exports.SESSION_ADDRESSED_FRAMES = new Set([
134
135
  'attach', 'input', 'peek', 'resize', 'detach', 'close', 'read-handoff',
135
136
  'browser-input', 'browser-frame-ack', 'browser-resize', 'browser-sleep',
137
+ // Terminal attach writes next to a session — same consent boundary.
138
+ 'attach-file',
136
139
  ]);
137
140
  /**
138
141
  * pucc-14 — assemble the per-session PTY env for a workspace-scoped `open`.
@@ -1501,6 +1504,10 @@ function startRelayClient(opts) {
1501
1504
  });
1502
1505
  break;
1503
1506
  }
1507
+ case 'attach-file':
1508
+ // Terminal attach — shared verbatim with the local server.
1509
+ send(socket, (0, attachmentWriter_1.handleAttachFrame)(frame, opts.sessions));
1510
+ break;
1504
1511
  case 'read-transcript': {
1505
1512
  // wte-10 (docs/prd/work-this-epic.md D8): read ONE Claude Code
1506
1513
  // agent's session transcript, live. Server-initiated (no pane,
@@ -62,6 +62,7 @@ const sessionAnchor_1 = require("./sessionAnchor");
62
62
  // tchp-30 — per-session own-touch tracker (see its doc comment). Fed from
63
63
  // the SAME activity records fcct-24's tail already parses below.
64
64
  const sessionTouchTracker_1 = require("./sessionTouchTracker");
65
+ const attachmentWriter_1 = require("./attachmentWriter");
65
66
  const PROTOCOL_VERSION = 1;
66
67
  function send(ws, frame) {
67
68
  if (ws.readyState === ws_1.WebSocket.OPEN)
@@ -389,6 +390,8 @@ function startBridgeServer(opts) {
389
390
  // of failing at save time.
390
391
  capabilities: [
391
392
  ...(opts.writeLocalFile ? ['local-file-write'] : []),
393
+ // Terminal attach — this bridge answers `attach-file` frames.
394
+ 'terminal-attach',
392
395
  ],
393
396
  });
394
397
  return;
@@ -930,6 +933,11 @@ function startBridgeServer(opts) {
930
933
  });
931
934
  break;
932
935
  }
936
+ case 'attach-file':
937
+ // Terminal attach — a FILE write next to the session, never PTY
938
+ // input. Shared handler with relayClient.ts (both lanes).
939
+ send(ws, (0, attachmentWriter_1.handleAttachFrame)(frame, opts.sessions));
940
+ break;
933
941
  default:
934
942
  send(ws, { t: 'error', message: 'unknown frame type' });
935
943
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenrwa/marketplace-cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "CLI tool for the Wenrwa Agent Marketplace \u2014 monitor bounties, agents, workspaces, and treasury from your terminal",
5
5
  "author": "Wenrwa <dev@wenrwa.com>",
6
6
  "license": "MIT",