@vibe-interviewing/core 0.1.0 → 0.2.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.
@@ -0,0 +1,309 @@
1
+ import {
2
+ InvalidSessionCodeError,
3
+ VibeError,
4
+ decodeSessionCode,
5
+ encodeSessionCode,
6
+ isCloudSessionCode
7
+ } from "../chunk-5PTYUK4Z.js";
8
+
9
+ // src/network/server.ts
10
+ import { createServer } from "http";
11
+ import { execSync } from "child_process";
12
+ import { readFile } from "fs/promises";
13
+ import { join } from "path";
14
+ import { networkInterfaces } from "os";
15
+ import { EventEmitter } from "events";
16
+ var SessionServer = class extends EventEmitter {
17
+ server = null;
18
+ /** Start serving the session workspace */
19
+ async start(session, config, port) {
20
+ const tarball = join(session.workdir, "..", `${session.id}.tar.gz`);
21
+ execSync(`tar czf "${tarball}" -C "${session.workdir}" .`, { stdio: "pipe" });
22
+ const systemPrompt = await readFile(session.systemPromptPath, "utf-8");
23
+ const metadata = {
24
+ scenarioName: config.name,
25
+ type: config.type,
26
+ difficulty: config.difficulty,
27
+ estimatedTime: config.estimated_time,
28
+ briefing: config.briefing,
29
+ setupCommands: config.setup
30
+ };
31
+ this.server = createServer((req, res) => {
32
+ if (req.method !== "GET") {
33
+ res.writeHead(405);
34
+ res.end();
35
+ return;
36
+ }
37
+ switch (req.url) {
38
+ case "/metadata":
39
+ res.writeHead(200, { "Content-Type": "application/json" });
40
+ res.end(JSON.stringify(metadata));
41
+ break;
42
+ case "/system-prompt":
43
+ this.emit("candidate-connected");
44
+ res.writeHead(200, { "Content-Type": "text/plain" });
45
+ res.end(systemPrompt);
46
+ break;
47
+ case "/workspace":
48
+ readFile(tarball).then((data) => {
49
+ res.writeHead(200, {
50
+ "Content-Type": "application/gzip",
51
+ "Content-Length": data.length
52
+ });
53
+ res.end(data);
54
+ this.emit("download-complete");
55
+ }).catch(() => {
56
+ res.writeHead(500);
57
+ res.end("Failed to read workspace tarball");
58
+ });
59
+ break;
60
+ default:
61
+ res.writeHead(404);
62
+ res.end();
63
+ }
64
+ });
65
+ const listenPort = port ?? 0;
66
+ return new Promise((resolve, reject) => {
67
+ this.server.listen(listenPort, () => {
68
+ const addr = this.server.address();
69
+ if (!addr || typeof addr === "string") {
70
+ reject(new Error("Failed to get server address"));
71
+ return;
72
+ }
73
+ const host = getLanIp();
74
+ const code = encodeSessionCode(host, addr.port);
75
+ resolve({ code, port: addr.port, host });
76
+ });
77
+ this.server.on("error", reject);
78
+ });
79
+ }
80
+ /** Stop the server and clean up */
81
+ async stop() {
82
+ return new Promise((resolve) => {
83
+ if (this.server) {
84
+ this.server.close(() => resolve());
85
+ } else {
86
+ resolve();
87
+ }
88
+ });
89
+ }
90
+ };
91
+ function getLanIp() {
92
+ const interfaces = networkInterfaces();
93
+ for (const entries of Object.values(interfaces)) {
94
+ if (!entries) continue;
95
+ for (const entry of entries) {
96
+ if (entry.family === "IPv4" && !entry.internal) {
97
+ return entry.address;
98
+ }
99
+ }
100
+ }
101
+ return "127.0.0.1";
102
+ }
103
+
104
+ // src/network/client.ts
105
+ import { get } from "http";
106
+ import { writeFile, mkdir } from "fs/promises";
107
+ import { join as join2 } from "path";
108
+ import { homedir } from "os";
109
+ import { execSync as execSync2 } from "child_process";
110
+ import { randomBytes } from "crypto";
111
+ var NetworkError = class extends VibeError {
112
+ constructor(message) {
113
+ super(message, "NETWORK_ERROR", "Check the session code and ensure the host is running");
114
+ }
115
+ };
116
+ function fetchJson(host, port, path) {
117
+ return new Promise((resolve, reject) => {
118
+ const req = get({ hostname: host, port, path, timeout: 1e4 }, (res) => {
119
+ if (res.statusCode !== 200) {
120
+ reject(new NetworkError(`Server returned ${res.statusCode} for ${path}`));
121
+ return;
122
+ }
123
+ let data = "";
124
+ res.on("data", (chunk) => {
125
+ data += chunk.toString();
126
+ });
127
+ res.on("end", () => {
128
+ try {
129
+ resolve(JSON.parse(data));
130
+ } catch {
131
+ reject(new NetworkError(`Invalid response from server for ${path}`));
132
+ }
133
+ });
134
+ });
135
+ req.on("error", (err) => reject(new NetworkError(`Connection failed: ${err.message}`)));
136
+ req.on("timeout", () => {
137
+ req.destroy();
138
+ reject(new NetworkError("Connection timed out"));
139
+ });
140
+ });
141
+ }
142
+ function fetchText(host, port, path) {
143
+ return new Promise((resolve, reject) => {
144
+ const req = get({ hostname: host, port, path, timeout: 1e4 }, (res) => {
145
+ if (res.statusCode !== 200) {
146
+ reject(new NetworkError(`Server returned ${res.statusCode} for ${path}`));
147
+ return;
148
+ }
149
+ let data = "";
150
+ res.on("data", (chunk) => {
151
+ data += chunk.toString();
152
+ });
153
+ res.on("end", () => resolve(data));
154
+ });
155
+ req.on("error", (err) => reject(new NetworkError(`Connection failed: ${err.message}`)));
156
+ req.on("timeout", () => {
157
+ req.destroy();
158
+ reject(new NetworkError("Connection timed out"));
159
+ });
160
+ });
161
+ }
162
+ function fetchBinary(host, port, path) {
163
+ return new Promise((resolve, reject) => {
164
+ const req = get({ hostname: host, port, path, timeout: 6e4 }, (res) => {
165
+ if (res.statusCode !== 200) {
166
+ reject(new NetworkError(`Server returned ${res.statusCode} for ${path}`));
167
+ return;
168
+ }
169
+ const chunks = [];
170
+ res.on("data", (chunk) => chunks.push(chunk));
171
+ res.on("end", () => resolve(Buffer.concat(chunks)));
172
+ });
173
+ req.on("error", (err) => reject(new NetworkError(`Download failed: ${err.message}`)));
174
+ req.on("timeout", () => {
175
+ req.destroy();
176
+ reject(new NetworkError("Download timed out"));
177
+ });
178
+ });
179
+ }
180
+ async function downloadSession(host, port, targetDir, onProgress) {
181
+ const id = randomBytes(4).toString("hex");
182
+ onProgress?.("Connecting to host...");
183
+ const metadata = await fetchJson(host, port, "/metadata");
184
+ onProgress?.("Downloading scenario...");
185
+ const systemPrompt = await fetchText(host, port, "/system-prompt");
186
+ onProgress?.("Downloading workspace...");
187
+ const tarball = await fetchBinary(host, port, "/workspace");
188
+ onProgress?.("Extracting workspace...");
189
+ const workdir = targetDir ?? join2(homedir(), "vibe-sessions", `${metadata.scenarioName}-${id}`);
190
+ await mkdir(workdir, { recursive: true });
191
+ const tarballPath = join2(workdir, "..", `${id}-download.tar.gz`);
192
+ await writeFile(tarballPath, tarball);
193
+ execSync2(`tar xzf "${tarballPath}" -C "${workdir}"`, { stdio: "pipe" });
194
+ const promptDir = join2(homedir(), ".vibe-interviewing", "prompts");
195
+ await mkdir(promptDir, { recursive: true });
196
+ const systemPromptPath = join2(promptDir, `${id}.md`);
197
+ await writeFile(systemPromptPath, systemPrompt);
198
+ return { workdir, systemPromptPath, metadata, id };
199
+ }
200
+
201
+ // src/network/cloud-client.ts
202
+ import { writeFile as writeFile2, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
203
+ import { join as join3 } from "path";
204
+ import { homedir as homedir2 } from "os";
205
+ import { execSync as execSync3 } from "child_process";
206
+ import { randomBytes as randomBytes2 } from "crypto";
207
+ var DEFAULT_WORKER_URL = "https://api.vibe-interviewing.iar.dev";
208
+ function getWorkerUrl() {
209
+ return process.env["VIBE_WORKER_URL"] || DEFAULT_WORKER_URL;
210
+ }
211
+ var CloudError = class extends VibeError {
212
+ constructor(message) {
213
+ super(message, "CLOUD_ERROR", "Check your network connection and try again");
214
+ }
215
+ };
216
+ async function uploadSession(workerUrl, metadata, systemPrompt, tarballPath, onProgress) {
217
+ onProgress?.("Reading workspace tarball...");
218
+ const tarballData = await readFile2(tarballPath);
219
+ onProgress?.("Uploading to cloud...");
220
+ const formData = new FormData();
221
+ formData.set("metadata", JSON.stringify(metadata));
222
+ formData.set("systemPrompt", systemPrompt);
223
+ formData.set(
224
+ "workspace",
225
+ new Blob([tarballData], { type: "application/gzip" }),
226
+ "workspace.tar.gz"
227
+ );
228
+ let response;
229
+ try {
230
+ response = await fetch(`${workerUrl}/sessions`, {
231
+ method: "POST",
232
+ body: formData
233
+ });
234
+ } catch (err) {
235
+ throw new CloudError(
236
+ `Failed to connect to cloud relay at ${workerUrl}: ${err instanceof Error ? err.message : String(err)}`
237
+ );
238
+ }
239
+ if (!response.ok) {
240
+ const body = await response.text().catch(() => "unknown error");
241
+ throw new CloudError(`Cloud upload failed (${response.status}): ${body}`);
242
+ }
243
+ const result = await response.json();
244
+ return result;
245
+ }
246
+ async function downloadSessionFromCloud(workerUrl, code, targetDir, onProgress) {
247
+ const id = randomBytes2(4).toString("hex");
248
+ let rawCode = code.trim().toUpperCase();
249
+ if (rawCode.startsWith("VIBE-")) {
250
+ rawCode = rawCode.slice(5);
251
+ }
252
+ rawCode = rawCode.toLowerCase();
253
+ onProgress?.("Connecting to cloud relay...");
254
+ const metadataRes = await cloudFetch(`${workerUrl}/sessions/${rawCode}/metadata`);
255
+ const metadata = await metadataRes.json();
256
+ onProgress?.("Downloading scenario...");
257
+ const promptRes = await cloudFetch(`${workerUrl}/sessions/${rawCode}/system-prompt`);
258
+ const systemPrompt = await promptRes.text();
259
+ onProgress?.("Downloading workspace...");
260
+ const workspaceRes = await cloudFetch(`${workerUrl}/sessions/${rawCode}/workspace`);
261
+ const tarball = Buffer.from(await workspaceRes.arrayBuffer());
262
+ onProgress?.("Extracting workspace...");
263
+ const workdir = targetDir ?? join3(homedir2(), "vibe-sessions", `${metadata.scenarioName}-${id}`);
264
+ await mkdir2(workdir, { recursive: true });
265
+ const tarballPath = join3(workdir, "..", `${id}-download.tar.gz`);
266
+ await writeFile2(tarballPath, tarball);
267
+ execSync3(`tar xzf "${tarballPath}" -C "${workdir}"`, { stdio: "pipe" });
268
+ const promptDir = join3(homedir2(), ".vibe-interviewing", "prompts");
269
+ await mkdir2(promptDir, { recursive: true });
270
+ const systemPromptPath = join3(promptDir, `${id}.md`);
271
+ await writeFile2(systemPromptPath, systemPrompt);
272
+ return { workdir, systemPromptPath, metadata, id };
273
+ }
274
+ async function cloudFetch(url) {
275
+ let response;
276
+ try {
277
+ response = await fetch(url, { signal: AbortSignal.timeout(6e4) });
278
+ } catch (err) {
279
+ if (err instanceof DOMException && err.name === "TimeoutError") {
280
+ throw new CloudError("Cloud download timed out");
281
+ }
282
+ throw new CloudError(
283
+ `Failed to connect to cloud relay: ${err instanceof Error ? err.message : String(err)}`
284
+ );
285
+ }
286
+ if (response.status === 404) {
287
+ throw new CloudError("Session not found or expired");
288
+ }
289
+ if (!response.ok) {
290
+ const body = await response.text().catch(() => "unknown error");
291
+ throw new CloudError(`Cloud request failed (${response.status}): ${body}`);
292
+ }
293
+ return response;
294
+ }
295
+ export {
296
+ CloudError,
297
+ DEFAULT_WORKER_URL,
298
+ InvalidSessionCodeError,
299
+ NetworkError,
300
+ SessionServer,
301
+ decodeSessionCode,
302
+ downloadSession,
303
+ downloadSessionFromCloud,
304
+ encodeSessionCode,
305
+ getWorkerUrl,
306
+ isCloudSessionCode,
307
+ uploadSession
308
+ };
309
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/network/server.ts","../../src/network/client.ts","../../src/network/cloud-client.ts"],"sourcesContent":["import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http'\nimport { execSync } from 'node:child_process'\nimport { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { networkInterfaces } from 'node:os'\nimport { EventEmitter } from 'node:events'\nimport type { Session } from '../session/types.js'\nimport type { ScenarioConfig } from '../scenario/types.js'\nimport { encodeSessionCode } from './session-code.js'\n\n/** Metadata served to candidates */\nexport interface SessionMetadata {\n scenarioName: string\n type: string\n difficulty: string\n estimatedTime: string\n briefing: string\n setupCommands: string[]\n}\n\n/** Events emitted by the session server */\nexport interface SessionServerEvents {\n 'candidate-connected': []\n 'download-complete': []\n}\n\n/** HTTP server that serves a prepared workspace to a remote candidate */\nexport class SessionServer extends EventEmitter<SessionServerEvents> {\n private server: Server | null = null\n\n /** Start serving the session workspace */\n async start(\n session: Session,\n config: ScenarioConfig,\n port?: number,\n ): Promise<{ code: string; port: number; host: string }> {\n // Create tarball of the workspace\n const tarball = join(session.workdir, '..', `${session.id}.tar.gz`)\n execSync(`tar czf \"${tarball}\" -C \"${session.workdir}\" .`, { stdio: 'pipe' })\n\n const systemPrompt = await readFile(session.systemPromptPath, 'utf-8')\n\n const metadata: SessionMetadata = {\n scenarioName: config.name,\n type: config.type,\n difficulty: config.difficulty,\n estimatedTime: config.estimated_time,\n briefing: config.briefing,\n setupCommands: config.setup,\n }\n\n this.server = createServer((req: IncomingMessage, res: ServerResponse) => {\n if (req.method !== 'GET') {\n res.writeHead(405)\n res.end()\n return\n }\n\n switch (req.url) {\n case '/metadata':\n res.writeHead(200, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify(metadata))\n break\n\n case '/system-prompt':\n this.emit('candidate-connected')\n res.writeHead(200, { 'Content-Type': 'text/plain' })\n res.end(systemPrompt)\n break\n\n case '/workspace':\n readFile(tarball)\n .then((data) => {\n res.writeHead(200, {\n 'Content-Type': 'application/gzip',\n 'Content-Length': data.length,\n })\n res.end(data)\n this.emit('download-complete')\n })\n .catch(() => {\n res.writeHead(500)\n res.end('Failed to read workspace tarball')\n })\n break\n\n default:\n res.writeHead(404)\n res.end()\n }\n })\n\n const listenPort = port ?? 0\n\n return new Promise((resolve, reject) => {\n this.server!.listen(listenPort, () => {\n const addr = this.server!.address()\n if (!addr || typeof addr === 'string') {\n reject(new Error('Failed to get server address'))\n return\n }\n\n const host = getLanIp()\n const code = encodeSessionCode(host, addr.port)\n\n resolve({ code, port: addr.port, host })\n })\n\n this.server!.on('error', reject)\n })\n }\n\n /** Stop the server and clean up */\n async stop(): Promise<void> {\n return new Promise((resolve) => {\n if (this.server) {\n this.server.close(() => resolve())\n } else {\n resolve()\n }\n })\n }\n}\n\n/** Get the first non-internal IPv4 address */\nfunction getLanIp(): string {\n const interfaces = networkInterfaces()\n for (const entries of Object.values(interfaces)) {\n if (!entries) continue\n for (const entry of entries) {\n if (entry.family === 'IPv4' && !entry.internal) {\n return entry.address\n }\n }\n }\n return '127.0.0.1'\n}\n","import { get } from 'node:http'\nimport { writeFile, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { execSync } from 'node:child_process'\nimport { randomBytes } from 'node:crypto'\nimport { VibeError } from '../errors.js'\nimport type { SessionMetadata } from './server.js'\nimport type { ProgressCallback } from '../session/manager.js'\n\n/** Result of downloading a session from a host */\nexport interface DownloadedSession {\n /** Path to the extracted workspace */\n workdir: string\n /** Path to the system prompt file */\n systemPromptPath: string\n /** Session metadata from the host */\n metadata: SessionMetadata\n /** Generated session ID */\n id: string\n}\n\n/** Error for network/connection failures */\nexport class NetworkError extends VibeError {\n constructor(message: string) {\n super(message, 'NETWORK_ERROR', 'Check the session code and ensure the host is running')\n }\n}\n\n/** Fetch JSON from an HTTP endpoint */\nfunction fetchJson<T>(host: string, port: number, path: string): Promise<T> {\n return new Promise((resolve, reject) => {\n const req = get({ hostname: host, port, path, timeout: 10000 }, (res) => {\n if (res.statusCode !== 200) {\n reject(new NetworkError(`Server returned ${res.statusCode} for ${path}`))\n return\n }\n let data = ''\n res.on('data', (chunk: Buffer) => {\n data += chunk.toString()\n })\n res.on('end', () => {\n try {\n resolve(JSON.parse(data) as T)\n } catch {\n reject(new NetworkError(`Invalid response from server for ${path}`))\n }\n })\n })\n req.on('error', (err) => reject(new NetworkError(`Connection failed: ${err.message}`)))\n req.on('timeout', () => {\n req.destroy()\n reject(new NetworkError('Connection timed out'))\n })\n })\n}\n\n/** Fetch text from an HTTP endpoint */\nfunction fetchText(host: string, port: number, path: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const req = get({ hostname: host, port, path, timeout: 10000 }, (res) => {\n if (res.statusCode !== 200) {\n reject(new NetworkError(`Server returned ${res.statusCode} for ${path}`))\n return\n }\n let data = ''\n res.on('data', (chunk: Buffer) => {\n data += chunk.toString()\n })\n res.on('end', () => resolve(data))\n })\n req.on('error', (err) => reject(new NetworkError(`Connection failed: ${err.message}`)))\n req.on('timeout', () => {\n req.destroy()\n reject(new NetworkError('Connection timed out'))\n })\n })\n}\n\n/** Download a binary file from an HTTP endpoint */\nfunction fetchBinary(host: string, port: number, path: string): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const req = get({ hostname: host, port, path, timeout: 60000 }, (res) => {\n if (res.statusCode !== 200) {\n reject(new NetworkError(`Server returned ${res.statusCode} for ${path}`))\n return\n }\n const chunks: Buffer[] = []\n res.on('data', (chunk: Buffer) => chunks.push(chunk))\n res.on('end', () => resolve(Buffer.concat(chunks)))\n })\n req.on('error', (err) => reject(new NetworkError(`Download failed: ${err.message}`)))\n req.on('timeout', () => {\n req.destroy()\n reject(new NetworkError('Download timed out'))\n })\n })\n}\n\n/**\n * Download a session from a host.\n *\n * Fetches metadata, system prompt, and workspace tarball, then\n * extracts everything to a local directory.\n */\nexport async function downloadSession(\n host: string,\n port: number,\n targetDir?: string,\n onProgress?: ProgressCallback,\n): Promise<DownloadedSession> {\n const id = randomBytes(4).toString('hex')\n\n // 1. Fetch metadata\n onProgress?.('Connecting to host...')\n const metadata = await fetchJson<SessionMetadata>(host, port, '/metadata')\n\n // 2. Fetch system prompt\n onProgress?.('Downloading scenario...')\n const systemPrompt = await fetchText(host, port, '/system-prompt')\n\n // 3. Download workspace tarball\n onProgress?.('Downloading workspace...')\n const tarball = await fetchBinary(host, port, '/workspace')\n\n // 4. Extract workspace\n onProgress?.('Extracting workspace...')\n const workdir = targetDir ?? join(homedir(), 'vibe-sessions', `${metadata.scenarioName}-${id}`)\n await mkdir(workdir, { recursive: true })\n\n // Write tarball to temp file, then extract\n const tarballPath = join(workdir, '..', `${id}-download.tar.gz`)\n await writeFile(tarballPath, tarball)\n execSync(`tar xzf \"${tarballPath}\" -C \"${workdir}\"`, { stdio: 'pipe' })\n\n // 5. Write system prompt outside workspace\n const promptDir = join(homedir(), '.vibe-interviewing', 'prompts')\n await mkdir(promptDir, { recursive: true })\n const systemPromptPath = join(promptDir, `${id}.md`)\n await writeFile(systemPromptPath, systemPrompt)\n\n return { workdir, systemPromptPath, metadata, id }\n}\n","import { writeFile, mkdir, readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { execSync } from 'node:child_process'\nimport { randomBytes } from 'node:crypto'\nimport { VibeError } from '../errors.js'\nimport type { SessionMetadata } from './server.js'\nimport type { ProgressCallback } from '../session/manager.js'\nimport type { DownloadedSession } from './client.js'\n\n/** Default Cloudflare Worker URL */\nexport const DEFAULT_WORKER_URL = 'https://api.vibe-interviewing.iar.dev'\n\n/** Get the configured worker URL (env override or default) */\nexport function getWorkerUrl(): string {\n return process.env['VIBE_WORKER_URL'] || DEFAULT_WORKER_URL\n}\n\n/** Error for cloud upload/download failures */\nexport class CloudError extends VibeError {\n constructor(message: string) {\n super(message, 'CLOUD_ERROR', 'Check your network connection and try again')\n }\n}\n\n/**\n * Upload a session to the cloud relay.\n *\n * Sends metadata, system prompt, and workspace tarball to the Cloudflare Worker.\n * Returns a session code the candidate can use to download.\n */\nexport async function uploadSession(\n workerUrl: string,\n metadata: SessionMetadata,\n systemPrompt: string,\n tarballPath: string,\n onProgress?: ProgressCallback,\n): Promise<{ code: string; expiresAt: string }> {\n onProgress?.('Reading workspace tarball...')\n const tarballData = await readFile(tarballPath)\n\n onProgress?.('Uploading to cloud...')\n const formData = new FormData()\n formData.set('metadata', JSON.stringify(metadata))\n formData.set('systemPrompt', systemPrompt)\n formData.set(\n 'workspace',\n new Blob([tarballData], { type: 'application/gzip' }),\n 'workspace.tar.gz',\n )\n\n let response: Response\n try {\n response = await fetch(`${workerUrl}/sessions`, {\n method: 'POST',\n body: formData,\n })\n } catch (err) {\n throw new CloudError(\n `Failed to connect to cloud relay at ${workerUrl}: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n\n if (!response.ok) {\n const body = await response.text().catch(() => 'unknown error')\n throw new CloudError(`Cloud upload failed (${response.status}): ${body}`)\n }\n\n const result = (await response.json()) as { code: string; expiresAt: string }\n return result\n}\n\n/**\n * Download a session from the cloud relay.\n *\n * Fetches metadata, system prompt, and workspace tarball from the Cloudflare Worker.\n */\nexport async function downloadSessionFromCloud(\n workerUrl: string,\n code: string,\n targetDir?: string,\n onProgress?: ProgressCallback,\n): Promise<DownloadedSession> {\n const id = randomBytes(4).toString('hex')\n\n // Strip VIBE- prefix and lowercase for the API\n let rawCode = code.trim().toUpperCase()\n if (rawCode.startsWith('VIBE-')) {\n rawCode = rawCode.slice(5)\n }\n rawCode = rawCode.toLowerCase()\n\n // 1. Fetch metadata\n onProgress?.('Connecting to cloud relay...')\n const metadataRes = await cloudFetch(`${workerUrl}/sessions/${rawCode}/metadata`)\n const metadata = (await metadataRes.json()) as SessionMetadata\n\n // 2. Fetch system prompt\n onProgress?.('Downloading scenario...')\n const promptRes = await cloudFetch(`${workerUrl}/sessions/${rawCode}/system-prompt`)\n const systemPrompt = await promptRes.text()\n\n // 3. Download workspace tarball\n onProgress?.('Downloading workspace...')\n const workspaceRes = await cloudFetch(`${workerUrl}/sessions/${rawCode}/workspace`)\n const tarball = Buffer.from(await workspaceRes.arrayBuffer())\n\n // 4. Extract workspace\n onProgress?.('Extracting workspace...')\n const workdir = targetDir ?? join(homedir(), 'vibe-sessions', `${metadata.scenarioName}-${id}`)\n await mkdir(workdir, { recursive: true })\n\n const tarballPath = join(workdir, '..', `${id}-download.tar.gz`)\n await writeFile(tarballPath, tarball)\n execSync(`tar xzf \"${tarballPath}\" -C \"${workdir}\"`, { stdio: 'pipe' })\n\n // 5. Write system prompt outside workspace\n const promptDir = join(homedir(), '.vibe-interviewing', 'prompts')\n await mkdir(promptDir, { recursive: true })\n const systemPromptPath = join(promptDir, `${id}.md`)\n await writeFile(systemPromptPath, systemPrompt)\n\n return { workdir, systemPromptPath, metadata, id }\n}\n\n/** Fetch from the cloud worker with error handling */\nasync function cloudFetch(url: string): Promise<Response> {\n let response: Response\n try {\n response = await fetch(url, { signal: AbortSignal.timeout(60000) })\n } catch (err) {\n if (err instanceof DOMException && err.name === 'TimeoutError') {\n throw new CloudError('Cloud download timed out')\n }\n throw new CloudError(\n `Failed to connect to cloud relay: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n\n if (response.status === 404) {\n throw new CloudError('Session not found or expired')\n }\n\n if (!response.ok) {\n const body = await response.text().catch(() => 'unknown error')\n throw new CloudError(`Cloud request failed (${response.status}): ${body}`)\n }\n\n return response\n}\n"],"mappings":"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","names":["join","execSync","join","execSync","writeFile","mkdir","readFile","join","homedir","execSync","randomBytes","readFile","randomBytes","join","homedir","mkdir","writeFile","execSync"]}