@hatchway/cli 0.50.68 → 0.50.70
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.
- package/dist/chunks/Banner-WeSdAE2V.js +115 -0
- package/dist/chunks/Banner-WeSdAE2V.js.map +1 -0
- package/dist/chunks/devtools-CPruVlOo.js.map +1 -1
- package/dist/chunks/index-CovlIWnu.js +119 -0
- package/dist/chunks/index-CovlIWnu.js.map +1 -0
- package/dist/chunks/init-DO24F24y.js +472 -0
- package/dist/chunks/init-DO24F24y.js.map +1 -0
- package/dist/chunks/init-tui-R_Z9ojhn.js +1167 -0
- package/dist/chunks/init-tui-R_Z9ojhn.js.map +1 -0
- package/dist/chunks/main-tui-ZMX-H8Pf.js +651 -0
- package/dist/chunks/main-tui-ZMX-H8Pf.js.map +1 -0
- package/dist/chunks/manager-0U0BIO9r.js +1254 -0
- package/dist/chunks/manager-0U0BIO9r.js.map +1 -0
- package/dist/chunks/port-allocator-DAjm7X-F.js +859 -0
- package/dist/chunks/port-allocator-DAjm7X-F.js.map +1 -0
- package/dist/chunks/run-jC7sIavB.js +696 -0
- package/dist/chunks/run-jC7sIavB.js.map +1 -0
- package/dist/chunks/start-B5Vi66EE.js +1713 -0
- package/dist/chunks/start-B5Vi66EE.js.map +1 -0
- package/dist/chunks/theme-CzLXk_6s.js +40256 -0
- package/dist/chunks/theme-CzLXk_6s.js.map +1 -0
- package/dist/chunks/use-app-Dw8M2HNg.js +10 -0
- package/dist/chunks/use-app-Dw8M2HNg.js.map +1 -0
- package/dist/chunks/useBuildState-c-9oir2y.js +330 -0
- package/dist/chunks/useBuildState-c-9oir2y.js.map +1 -0
- package/dist/cli/index.js +6 -31
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +292 -555
- package/dist/index.js.map +1 -1
- package/package.json +10 -11
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Hatchway CLI - Built with Rollup
|
|
2
|
-
import * as Sentry from '@sentry/node';
|
|
3
2
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
4
3
|
import { Codex } from '@openai/codex-sdk';
|
|
5
4
|
import { a as streamLog, f as fileLog, i as initRunnerLogger, s as setFileLoggerTuiMode, b as getLogger } from './chunks/runner-logger-instance-Dj_JMznn.js';
|
|
@@ -9,13 +8,13 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
8
|
import { existsSync, readdirSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
9
|
import { readFile } from 'fs/promises';
|
|
11
10
|
import * as path from 'path';
|
|
12
|
-
import { join as join$1 } from 'path';
|
|
11
|
+
import { join as join$1, resolve as resolve$1, relative as relative$1, isAbsolute as isAbsolute$1 } from 'path';
|
|
13
12
|
import WebSocket$1, { WebSocketServer, WebSocket } from 'ws';
|
|
14
13
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
15
14
|
import pg from 'pg';
|
|
16
15
|
import { pgTable, timestamp, boolean, text, uuid, index, uniqueIndex, integer, jsonb } from 'drizzle-orm/pg-core';
|
|
17
16
|
import { sql, eq, and, desc, isNull } from 'drizzle-orm';
|
|
18
|
-
import { randomUUID, createHash } from 'crypto';
|
|
17
|
+
import { randomUUID, timingSafeEqual, createHash } from 'crypto';
|
|
19
18
|
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
|
20
19
|
import { z } from 'zod';
|
|
21
20
|
import { spawn } from 'node:child_process';
|
|
@@ -28,8 +27,7 @@ import { readFile as readFile$1, rm, writeFile, readdir } from 'node:fs/promises
|
|
|
28
27
|
import { simpleGit } from 'simple-git';
|
|
29
28
|
import * as os from 'os';
|
|
30
29
|
import { existsSync as existsSync$1, mkdirSync as mkdirSync$1 } from 'fs';
|
|
31
|
-
import { tunnelManager } from './chunks/manager-
|
|
32
|
-
import { a as getPackageVersion } from './chunks/version-info-CDtU8Ta2.js';
|
|
30
|
+
import { tunnelManager } from './chunks/manager-0U0BIO9r.js';
|
|
33
31
|
import 'chalk';
|
|
34
32
|
import 'http';
|
|
35
33
|
import 'http-proxy';
|
|
@@ -183,14 +181,6 @@ function getPlatformPluginDir() {
|
|
|
183
181
|
if (process.env.SILENT_MODE !== '1') {
|
|
184
182
|
process.stderr.write('[skills] Platform plugin directory not found\n');
|
|
185
183
|
}
|
|
186
|
-
Sentry.logger.error('Platform plugin directory not found — agent will run without core skills', {
|
|
187
|
-
candidatePaths: [
|
|
188
|
-
join(__dirname$3, 'skills', 'platform-plugin'),
|
|
189
|
-
join(__dirname$3, 'lib', 'skills', 'platform-plugin'),
|
|
190
|
-
join(__dirname$3, '..', 'src', 'lib', 'skills', 'platform-plugin'),
|
|
191
|
-
].join(', '),
|
|
192
|
-
__dirname: __dirname$3,
|
|
193
|
-
});
|
|
194
184
|
}
|
|
195
185
|
}
|
|
196
186
|
return _pluginDir;
|
|
@@ -277,12 +267,12 @@ var init_tags$1 = __esm$1({
|
|
|
277
267
|
sdk: "agent"
|
|
278
268
|
},
|
|
279
269
|
{
|
|
280
|
-
value: "claude-opus-4-
|
|
281
|
-
label: "Claude Opus 4.
|
|
270
|
+
value: "claude-opus-4-8",
|
|
271
|
+
label: "Claude Opus 4.8",
|
|
282
272
|
description: "Anthropic Claude - Most capable for complex tasks",
|
|
283
273
|
logo: "/claude.png",
|
|
284
274
|
provider: "claude-code",
|
|
285
|
-
model: "claude-opus-4-
|
|
275
|
+
model: "claude-opus-4-8",
|
|
286
276
|
sdk: "agent"
|
|
287
277
|
},
|
|
288
278
|
{
|
|
@@ -306,10 +296,10 @@ var init_tags$1 = __esm$1({
|
|
|
306
296
|
{
|
|
307
297
|
value: "factory-droid-opus",
|
|
308
298
|
label: "Factory Droid (Opus)",
|
|
309
|
-
description: "Factory Droid SDK with Claude Opus 4.
|
|
299
|
+
description: "Factory Droid SDK with Claude Opus 4.8",
|
|
310
300
|
logo: "/factory.svg",
|
|
311
301
|
provider: "factory-droid",
|
|
312
|
-
model: "claude-opus-4-
|
|
302
|
+
model: "claude-opus-4-8",
|
|
313
303
|
sdk: "droid"
|
|
314
304
|
},
|
|
315
305
|
{
|
|
@@ -1539,7 +1529,10 @@ var DEFAULT_OPENCODE_MODEL_ID = "anthropic/claude-sonnet-4-6";
|
|
|
1539
1529
|
var LEGACY_MODEL_MAP = {
|
|
1540
1530
|
"claude-haiku-4-5": "anthropic/claude-haiku-4-5",
|
|
1541
1531
|
"claude-sonnet-4-6": "anthropic/claude-sonnet-4-6",
|
|
1542
|
-
"claude-opus-4-
|
|
1532
|
+
"claude-opus-4-8": "anthropic/claude-opus-4-8",
|
|
1533
|
+
// Legacy aliases — upgrade previously-saved selections to the current model
|
|
1534
|
+
"claude-opus-4-6": "anthropic/claude-opus-4-8",
|
|
1535
|
+
"claude-opus-4-6-20251101": "anthropic/claude-opus-4-8",
|
|
1543
1536
|
"gpt-5-codex": "openai/gpt-5.2-codex",
|
|
1544
1537
|
"gpt-5.2-codex": "openai/gpt-5.2-codex"
|
|
1545
1538
|
};
|
|
@@ -1568,8 +1561,8 @@ var CLAUDE_MODEL_METADATA = {
|
|
|
1568
1561
|
description: "Balanced performance and quality",
|
|
1569
1562
|
provider: "anthropic"
|
|
1570
1563
|
},
|
|
1571
|
-
"claude-opus-4-
|
|
1572
|
-
label: "Claude Opus 4.
|
|
1564
|
+
"claude-opus-4-8": {
|
|
1565
|
+
label: "Claude Opus 4.8",
|
|
1573
1566
|
description: "Most capable Claude model for complex tasks",
|
|
1574
1567
|
provider: "anthropic"
|
|
1575
1568
|
}
|
|
@@ -1585,8 +1578,8 @@ var MODEL_METADATA = {
|
|
|
1585
1578
|
description: "Balanced performance and quality",
|
|
1586
1579
|
provider: "anthropic"
|
|
1587
1580
|
},
|
|
1588
|
-
"anthropic/claude-opus-4-
|
|
1589
|
-
label: "Claude Opus 4.
|
|
1581
|
+
"anthropic/claude-opus-4-8": {
|
|
1582
|
+
label: "Claude Opus 4.8",
|
|
1590
1583
|
description: "Most capable for complex tasks",
|
|
1591
1584
|
provider: "anthropic"
|
|
1592
1585
|
},
|
|
@@ -1683,6 +1676,8 @@ async function resolveAgentStrategy$1(agentId) {
|
|
|
1683
1676
|
|
|
1684
1677
|
// src/index.ts
|
|
1685
1678
|
init_config_server$1();
|
|
1679
|
+
|
|
1680
|
+
// src/lib/logging/build-logger.ts
|
|
1686
1681
|
var BuildLogger$1 = class BuildLogger {
|
|
1687
1682
|
buildId = null;
|
|
1688
1683
|
projectId = null;
|
|
@@ -1733,23 +1728,6 @@ var BuildLogger$1 = class BuildLogger {
|
|
|
1733
1728
|
} else {
|
|
1734
1729
|
logFn(`${icon} ${prefix} ${message}`);
|
|
1735
1730
|
}
|
|
1736
|
-
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
1737
|
-
try {
|
|
1738
|
-
if (level === "warn" || level === "error") {
|
|
1739
|
-
Sentry.addBreadcrumb({
|
|
1740
|
-
category: `build-logger.${context}`,
|
|
1741
|
-
message,
|
|
1742
|
-
level: level === "error" ? "error" : "warning",
|
|
1743
|
-
data: {
|
|
1744
|
-
...data,
|
|
1745
|
-
buildId: this.buildId,
|
|
1746
|
-
projectId: this.projectId
|
|
1747
|
-
}
|
|
1748
|
-
});
|
|
1749
|
-
}
|
|
1750
|
-
} catch {
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
1731
|
}
|
|
1754
1732
|
/**
|
|
1755
1733
|
* Orchestrator-specific logging methods
|
|
@@ -2269,6 +2247,17 @@ var projects = pgTable("projects", {
|
|
|
2269
2247
|
// 'deploying' | 'success' | 'failed' | 'crashed'
|
|
2270
2248
|
railwayLastDeployedAt: timestamp("railway_last_deployed_at"),
|
|
2271
2249
|
// Last successful deployment
|
|
2250
|
+
// Execution mode: where builds run for this project
|
|
2251
|
+
executionMode: text("execution_mode").default("local"),
|
|
2252
|
+
// 'local' | 'sandbox'
|
|
2253
|
+
sandboxId: text("sandbox_id"),
|
|
2254
|
+
// Railway sandbox id when executionMode='sandbox' and currently running (warm)
|
|
2255
|
+
sandboxStatus: text("sandbox_status"),
|
|
2256
|
+
// 'provisioning' | 'running' | 'stopped' | 'failed'
|
|
2257
|
+
sandboxCheckpoint: text("sandbox_checkpoint"),
|
|
2258
|
+
// Railway checkpoint key for this project's saved workspace (restore point)
|
|
2259
|
+
sandboxSubdomain: text("sandbox_subdomain"),
|
|
2260
|
+
// Stable railgate subdomain so the preview URL persists across restarts
|
|
2272
2261
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
2273
2262
|
updatedAt: timestamp("updated_at").notNull().defaultNow()
|
|
2274
2263
|
}, (table) => ({
|
|
@@ -3087,8 +3076,18 @@ var RunnerCommandQueue = class {
|
|
|
3087
3076
|
}
|
|
3088
3077
|
};
|
|
3089
3078
|
var commandQueue = new RunnerCommandQueue();
|
|
3079
|
+
function timingSafeEqualString(a, b) {
|
|
3080
|
+
const bufA = Buffer.from(a);
|
|
3081
|
+
const bufB = Buffer.from(b);
|
|
3082
|
+
if (bufA.length !== bufB.length) {
|
|
3083
|
+
return false;
|
|
3084
|
+
}
|
|
3085
|
+
return timingSafeEqual(bufA, bufB);
|
|
3086
|
+
}
|
|
3090
3087
|
|
|
3091
3088
|
// src/lib/websocket/server.ts
|
|
3089
|
+
var isLocalMode = () => process.env.HATCHWAY_LOCAL_MODE === "true";
|
|
3090
|
+
var upgradeAuthContext = /* @__PURE__ */ new WeakMap();
|
|
3092
3091
|
var getSharedSecret = () => process.env.RUNNER_SHARED_SECRET;
|
|
3093
3092
|
function hashRunnerKey(key) {
|
|
3094
3093
|
return createHash("sha256").update(key).digest("hex");
|
|
@@ -3143,6 +3142,18 @@ var BuildWebSocketServer = class {
|
|
|
3143
3142
|
initialized = false;
|
|
3144
3143
|
// Callback for runner status changes (set by app layer)
|
|
3145
3144
|
onRunnerStatusChangeCallback = null;
|
|
3145
|
+
// Client auth hooks (set by app layer). Without these, client connections are
|
|
3146
|
+
// only accepted in local mode - default deny in hosted deployments.
|
|
3147
|
+
authenticateClient = null;
|
|
3148
|
+
authorizeProjectAccess = null;
|
|
3149
|
+
/**
|
|
3150
|
+
* Install authentication hooks for frontend client connections.
|
|
3151
|
+
* Must be called by the app layer before clients connect in hosted mode.
|
|
3152
|
+
*/
|
|
3153
|
+
setClientAuth(authenticate, authorize) {
|
|
3154
|
+
this.authenticateClient = authenticate;
|
|
3155
|
+
this.authorizeProjectAccess = authorize;
|
|
3156
|
+
}
|
|
3146
3157
|
constructor() {
|
|
3147
3158
|
buildLogger$2.websocket.serverCreated(this.instanceId);
|
|
3148
3159
|
}
|
|
@@ -3162,7 +3173,13 @@ var BuildWebSocketServer = class {
|
|
|
3162
3173
|
// Disable compression for lower latency
|
|
3163
3174
|
});
|
|
3164
3175
|
this.wss.on("connection", (ws, req) => {
|
|
3165
|
-
this.handleConnection(ws, req)
|
|
3176
|
+
this.handleConnection(ws, req).catch((error) => {
|
|
3177
|
+
buildLogger$2.websocket.error("Failed to handle client connection", error);
|
|
3178
|
+
try {
|
|
3179
|
+
ws.close(1011, "Internal error");
|
|
3180
|
+
} catch {
|
|
3181
|
+
}
|
|
3182
|
+
});
|
|
3166
3183
|
});
|
|
3167
3184
|
this.runnerWss = new WebSocketServer({
|
|
3168
3185
|
noServer: true,
|
|
@@ -3178,8 +3195,20 @@ var BuildWebSocketServer = class {
|
|
|
3178
3195
|
this.runnerWss.emit("connection", ws, request);
|
|
3179
3196
|
});
|
|
3180
3197
|
} else if (pathname === path || pathname === "/ws") {
|
|
3181
|
-
this.
|
|
3182
|
-
|
|
3198
|
+
this.resolveClientAuth(request).then((auth) => {
|
|
3199
|
+
if (!auth.allowed) {
|
|
3200
|
+
buildLogger$2.log("warn", "websocket", "Rejected unauthenticated client upgrade");
|
|
3201
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
3202
|
+
socket.destroy();
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
upgradeAuthContext.set(request, { userId: auth.userId });
|
|
3206
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
3207
|
+
this.wss.emit("connection", ws, request);
|
|
3208
|
+
});
|
|
3209
|
+
}).catch((error) => {
|
|
3210
|
+
buildLogger$2.websocket.error("Client upgrade auth failed", error);
|
|
3211
|
+
socket.destroy();
|
|
3183
3212
|
});
|
|
3184
3213
|
} else {
|
|
3185
3214
|
if (pathname && pathname !== "/") {
|
|
@@ -3205,19 +3234,58 @@ var BuildWebSocketServer = class {
|
|
|
3205
3234
|
// ============================================================
|
|
3206
3235
|
// FRONTEND CLIENT HANDLING
|
|
3207
3236
|
// ============================================================
|
|
3237
|
+
/**
|
|
3238
|
+
* Authenticate a client upgrade request.
|
|
3239
|
+
* Local mode: always allowed. Hosted mode: requires the app layer to have
|
|
3240
|
+
* installed an authenticator via setClientAuth() - otherwise deny.
|
|
3241
|
+
*/
|
|
3242
|
+
async resolveClientAuth(req) {
|
|
3243
|
+
if (isLocalMode()) {
|
|
3244
|
+
return { allowed: true };
|
|
3245
|
+
}
|
|
3246
|
+
if (!this.authenticateClient) {
|
|
3247
|
+
buildLogger$2.log("warn", "websocket", "No client authenticator configured - denying client connection");
|
|
3248
|
+
return { allowed: false };
|
|
3249
|
+
}
|
|
3250
|
+
const auth = await this.authenticateClient(req);
|
|
3251
|
+
if (!auth) {
|
|
3252
|
+
return { allowed: false };
|
|
3253
|
+
}
|
|
3254
|
+
return { allowed: true, userId: auth.userId };
|
|
3255
|
+
}
|
|
3256
|
+
/**
|
|
3257
|
+
* Check whether a user may access a project. Local mode always allows.
|
|
3258
|
+
*/
|
|
3259
|
+
async canAccessProject(userId, projectId) {
|
|
3260
|
+
if (isLocalMode()) return true;
|
|
3261
|
+
if (!this.authorizeProjectAccess || !userId) return false;
|
|
3262
|
+
try {
|
|
3263
|
+
return await this.authorizeProjectAccess(userId, projectId);
|
|
3264
|
+
} catch (error) {
|
|
3265
|
+
buildLogger$2.websocket.error("Project access check failed", error, { projectId });
|
|
3266
|
+
return false;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3208
3269
|
/**
|
|
3209
3270
|
* Handle new frontend WebSocket connection
|
|
3210
3271
|
*/
|
|
3211
|
-
handleConnection(ws, req) {
|
|
3272
|
+
async handleConnection(ws, req) {
|
|
3212
3273
|
const clientId = this.generateClientId();
|
|
3213
3274
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
3214
3275
|
const projectId = url.searchParams.get("projectId") || "";
|
|
3215
3276
|
const sessionId = url.searchParams.get("sessionId") || void 0;
|
|
3277
|
+
const userId = upgradeAuthContext.get(req)?.userId;
|
|
3278
|
+
if (projectId && !await this.canAccessProject(userId, projectId)) {
|
|
3279
|
+
buildLogger$2.log("warn", "websocket", `Client denied access to project ${projectId}`);
|
|
3280
|
+
ws.close(4403, "Forbidden");
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3216
3283
|
buildLogger$2.websocket.clientConnected(clientId, projectId, sessionId);
|
|
3217
3284
|
this.clients.set(clientId, {
|
|
3218
3285
|
ws,
|
|
3219
3286
|
projectId,
|
|
3220
3287
|
sessionId,
|
|
3288
|
+
userId,
|
|
3221
3289
|
lastHeartbeat: Date.now()
|
|
3222
3290
|
});
|
|
3223
3291
|
this.sendMessage(ws, {
|
|
@@ -3230,7 +3298,9 @@ var BuildWebSocketServer = class {
|
|
|
3230
3298
|
ws.on("message", (data) => {
|
|
3231
3299
|
try {
|
|
3232
3300
|
const message = JSON.parse(data.toString());
|
|
3233
|
-
this.handleClientMessage(clientId, message)
|
|
3301
|
+
this.handleClientMessage(clientId, message).catch((error) => {
|
|
3302
|
+
buildLogger$2.websocket.error("Failed to handle client message", error, { clientId });
|
|
3303
|
+
});
|
|
3234
3304
|
} catch (error) {
|
|
3235
3305
|
buildLogger$2.websocket.error("Failed to parse client message", error, { clientId });
|
|
3236
3306
|
}
|
|
@@ -3267,7 +3337,7 @@ var BuildWebSocketServer = class {
|
|
|
3267
3337
|
const result = await validateRunnerKey(token);
|
|
3268
3338
|
isAuthenticated = result.valid;
|
|
3269
3339
|
runnerUserId = result.userId;
|
|
3270
|
-
} else if (sharedSecret && token
|
|
3340
|
+
} else if (sharedSecret && timingSafeEqualString(token, sharedSecret)) {
|
|
3271
3341
|
isAuthenticated = true;
|
|
3272
3342
|
}
|
|
3273
3343
|
}
|
|
@@ -3283,6 +3353,19 @@ var BuildWebSocketServer = class {
|
|
|
3283
3353
|
}
|
|
3284
3354
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
3285
3355
|
const runnerId = url.searchParams.get("runnerId") ?? "default";
|
|
3356
|
+
const existing = this.runnerConnections.get(runnerId);
|
|
3357
|
+
if (existing) {
|
|
3358
|
+
if (existing.userId !== runnerUserId) {
|
|
3359
|
+
buildLogger$2.log("warn", "websocket", `Rejected runner connection: runnerId '${runnerId}' already claimed by another user`);
|
|
3360
|
+
ws.close(1008, "Runner ID already in use");
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
clearInterval(existing.pingInterval);
|
|
3364
|
+
try {
|
|
3365
|
+
existing.socket.close(1e3, "Replaced by new connection");
|
|
3366
|
+
} catch {
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3286
3369
|
buildLogger$2.websocket.runnerConnected(runnerId);
|
|
3287
3370
|
const pingInterval = setInterval(() => {
|
|
3288
3371
|
if (ws.readyState === WebSocket.OPEN) {
|
|
@@ -3296,11 +3379,6 @@ var BuildWebSocketServer = class {
|
|
|
3296
3379
|
pingInterval,
|
|
3297
3380
|
userId: runnerUserId
|
|
3298
3381
|
});
|
|
3299
|
-
Sentry.addBreadcrumb({
|
|
3300
|
-
category: "websocket",
|
|
3301
|
-
message: `Runner connected: ${runnerId}`,
|
|
3302
|
-
level: "info"
|
|
3303
|
-
});
|
|
3304
3382
|
const queueResult = commandQueue.processQueue(runnerId);
|
|
3305
3383
|
if (queueResult.sent > 0 || queueResult.failed > 0) {
|
|
3306
3384
|
buildLogger$2.log("info", "websocket", `Runner ${runnerId} reconnected - processed queued commands`, {
|
|
@@ -3330,16 +3408,15 @@ var BuildWebSocketServer = class {
|
|
|
3330
3408
|
}
|
|
3331
3409
|
} catch (error) {
|
|
3332
3410
|
this.runnerTotalErrors++;
|
|
3333
|
-
Sentry.captureException(error, {
|
|
3334
|
-
tags: { runnerId, source: "websocket_message" },
|
|
3335
|
-
level: "error"
|
|
3336
|
-
});
|
|
3337
3411
|
buildLogger$2.websocket.error("Failed to handle runner message", error, { runnerId });
|
|
3338
3412
|
}
|
|
3339
3413
|
});
|
|
3340
3414
|
ws.on("close", (code) => {
|
|
3341
|
-
buildLogger$2.websocket.runnerDisconnected(runnerId, code);
|
|
3342
3415
|
const conn = this.runnerConnections.get(runnerId);
|
|
3416
|
+
if (conn && conn.socket !== ws) {
|
|
3417
|
+
return;
|
|
3418
|
+
}
|
|
3419
|
+
buildLogger$2.websocket.runnerDisconnected(runnerId, code);
|
|
3343
3420
|
if (conn) {
|
|
3344
3421
|
clearInterval(conn.pingInterval);
|
|
3345
3422
|
}
|
|
@@ -3347,20 +3424,10 @@ var BuildWebSocketServer = class {
|
|
|
3347
3424
|
httpProxyManager.cancelRequestsForRunner(runnerId);
|
|
3348
3425
|
hmrProxyManager$1.disconnectRunner(runnerId);
|
|
3349
3426
|
this.cleanupRunnerProcesses(runnerId);
|
|
3350
|
-
Sentry.addBreadcrumb({
|
|
3351
|
-
category: "websocket",
|
|
3352
|
-
message: `Runner disconnected: ${runnerId}`,
|
|
3353
|
-
level: "info",
|
|
3354
|
-
data: { code }
|
|
3355
|
-
});
|
|
3356
3427
|
});
|
|
3357
3428
|
ws.on("error", (error) => {
|
|
3358
3429
|
buildLogger$2.websocket.error("Runner socket error", error, { runnerId });
|
|
3359
3430
|
this.runnerTotalErrors++;
|
|
3360
|
-
Sentry.captureException(error, {
|
|
3361
|
-
tags: { runnerId, source: "websocket_error" },
|
|
3362
|
-
level: "error"
|
|
3363
|
-
});
|
|
3364
3431
|
const conn = this.runnerConnections.get(runnerId);
|
|
3365
3432
|
if (conn) {
|
|
3366
3433
|
clearInterval(conn.pingInterval);
|
|
@@ -3398,30 +3465,12 @@ var BuildWebSocketServer = class {
|
|
|
3398
3465
|
return false;
|
|
3399
3466
|
}
|
|
3400
3467
|
try {
|
|
3401
|
-
|
|
3402
|
-
const hasTrace = !!activeSpan;
|
|
3403
|
-
if (activeSpan) {
|
|
3404
|
-
const traceData = Sentry.getTraceData();
|
|
3405
|
-
if (traceData["sentry-trace"]) {
|
|
3406
|
-
command._sentry = {
|
|
3407
|
-
trace: traceData["sentry-trace"],
|
|
3408
|
-
baggage: traceData.baggage
|
|
3409
|
-
};
|
|
3410
|
-
buildLogger$2.log("debug", "websocket", `Attaching trace to command ${command.type}`, {
|
|
3411
|
-
tracePreview: traceData["sentry-trace"].substring(0, 50)
|
|
3412
|
-
});
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
buildLogger$2.websocket.commandSent(runnerId, command.type, hasTrace);
|
|
3468
|
+
buildLogger$2.websocket.commandSent(runnerId, command.type, false);
|
|
3416
3469
|
connection.socket.send(JSON.stringify(command));
|
|
3417
3470
|
this.runnerTotalCommands++;
|
|
3418
3471
|
return true;
|
|
3419
3472
|
} catch (error) {
|
|
3420
3473
|
this.runnerTotalErrors++;
|
|
3421
|
-
Sentry.captureException(error, {
|
|
3422
|
-
tags: { runnerId, commandType: command.type },
|
|
3423
|
-
level: "error"
|
|
3424
|
-
});
|
|
3425
3474
|
buildLogger$2.websocket.error("Failed to send command to runner", error, { runnerId, commandType: command.type });
|
|
3426
3475
|
return false;
|
|
3427
3476
|
}
|
|
@@ -3484,12 +3533,6 @@ var BuildWebSocketServer = class {
|
|
|
3484
3533
|
for (const [runnerId, conn] of this.runnerConnections.entries()) {
|
|
3485
3534
|
if (now - conn.lastHeartbeat > this.RUNNER_HEARTBEAT_TIMEOUT) {
|
|
3486
3535
|
buildLogger$2.websocket.runnerStaleRemoved(runnerId);
|
|
3487
|
-
Sentry.addBreadcrumb({
|
|
3488
|
-
category: "websocket",
|
|
3489
|
-
message: `Stale runner connection removed: ${runnerId}`,
|
|
3490
|
-
level: "warning",
|
|
3491
|
-
data: { age: now - conn.lastHeartbeat }
|
|
3492
|
-
});
|
|
3493
3536
|
clearInterval(conn.pingInterval);
|
|
3494
3537
|
conn.socket.close(1e3, "Heartbeat timeout");
|
|
3495
3538
|
this.runnerConnections.delete(runnerId);
|
|
@@ -3527,12 +3570,6 @@ var BuildWebSocketServer = class {
|
|
|
3527
3570
|
runnerId,
|
|
3528
3571
|
projectIds
|
|
3529
3572
|
});
|
|
3530
|
-
Sentry.addBreadcrumb({
|
|
3531
|
-
category: "websocket",
|
|
3532
|
-
message: `Cleaned up processes for disconnected runner`,
|
|
3533
|
-
level: "info",
|
|
3534
|
-
data: { runnerId, processCount: projectIds.length }
|
|
3535
|
-
});
|
|
3536
3573
|
if (this.onRunnerStatusChangeCallback) {
|
|
3537
3574
|
try {
|
|
3538
3575
|
this.onRunnerStatusChangeCallback(runnerId, false, projectIds);
|
|
@@ -3542,10 +3579,6 @@ var BuildWebSocketServer = class {
|
|
|
3542
3579
|
}
|
|
3543
3580
|
} catch (error) {
|
|
3544
3581
|
buildLogger$2.websocket.error("Failed to cleanup runner processes", error, { runnerId });
|
|
3545
|
-
Sentry.captureException(error, {
|
|
3546
|
-
tags: { runnerId, source: "runner_cleanup" },
|
|
3547
|
-
level: "error"
|
|
3548
|
-
});
|
|
3549
3582
|
}
|
|
3550
3583
|
}
|
|
3551
3584
|
/**
|
|
@@ -3570,7 +3603,7 @@ var BuildWebSocketServer = class {
|
|
|
3570
3603
|
/**
|
|
3571
3604
|
* Handle messages from client (heartbeat, resubscribe, HMR, etc.)
|
|
3572
3605
|
*/
|
|
3573
|
-
handleClientMessage(clientId, message) {
|
|
3606
|
+
async handleClientMessage(clientId, message) {
|
|
3574
3607
|
const client = this.clients.get(clientId);
|
|
3575
3608
|
if (!client) return;
|
|
3576
3609
|
switch (message.type) {
|
|
@@ -3578,11 +3611,19 @@ var BuildWebSocketServer = class {
|
|
|
3578
3611
|
client.lastHeartbeat = Date.now();
|
|
3579
3612
|
this.sendMessage(client.ws, { type: "heartbeat-ack", timestamp: Date.now() });
|
|
3580
3613
|
break;
|
|
3581
|
-
case "subscribe":
|
|
3582
|
-
|
|
3614
|
+
case "subscribe": {
|
|
3615
|
+
const targetProjectId = typeof message.projectId === "string" ? message.projectId : "";
|
|
3616
|
+
const alreadyVerified = targetProjectId === client.projectId;
|
|
3617
|
+
if (targetProjectId && !alreadyVerified && !await this.canAccessProject(client.userId, targetProjectId)) {
|
|
3618
|
+
buildLogger$2.log("warn", "websocket", `Client ${clientId} denied subscribe to project ${targetProjectId}`);
|
|
3619
|
+
this.sendMessage(client.ws, { type: "error", error: "Forbidden", projectId: targetProjectId });
|
|
3620
|
+
break;
|
|
3621
|
+
}
|
|
3622
|
+
client.projectId = targetProjectId;
|
|
3583
3623
|
client.sessionId = message.sessionId;
|
|
3584
|
-
buildLogger$2.websocket.clientSubscribed(clientId,
|
|
3624
|
+
buildLogger$2.websocket.clientSubscribed(clientId, targetProjectId);
|
|
3585
3625
|
break;
|
|
3626
|
+
}
|
|
3586
3627
|
case "get-state":
|
|
3587
3628
|
this.sendCurrentState(client);
|
|
3588
3629
|
break;
|
|
@@ -3601,10 +3642,18 @@ var BuildWebSocketServer = class {
|
|
|
3601
3642
|
/**
|
|
3602
3643
|
* Handle HMR connect request from frontend
|
|
3603
3644
|
*/
|
|
3604
|
-
handleHmrConnect(clientId, client, message) {
|
|
3605
|
-
const { connectionId,
|
|
3606
|
-
|
|
3607
|
-
|
|
3645
|
+
async handleHmrConnect(clientId, client, message) {
|
|
3646
|
+
const { connectionId, protocol } = message;
|
|
3647
|
+
if (!client.projectId) {
|
|
3648
|
+
this.sendMessage(client.ws, {
|
|
3649
|
+
type: "hmr-error",
|
|
3650
|
+
connectionId,
|
|
3651
|
+
error: "Not subscribed to a project"
|
|
3652
|
+
});
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3655
|
+
const target = await this.getHmrTargetForProject(client.projectId);
|
|
3656
|
+
if (!target) {
|
|
3608
3657
|
this.sendMessage(client.ws, {
|
|
3609
3658
|
type: "hmr-error",
|
|
3610
3659
|
connectionId,
|
|
@@ -3612,6 +3661,7 @@ var BuildWebSocketServer = class {
|
|
|
3612
3661
|
});
|
|
3613
3662
|
return;
|
|
3614
3663
|
}
|
|
3664
|
+
const { runnerId: targetRunnerId, port } = target;
|
|
3615
3665
|
if (!client.hmrConnections) {
|
|
3616
3666
|
client.hmrConnections = /* @__PURE__ */ new Set();
|
|
3617
3667
|
}
|
|
@@ -3672,12 +3722,22 @@ var BuildWebSocketServer = class {
|
|
|
3672
3722
|
hmrProxyManager$1.disconnect(connectionId);
|
|
3673
3723
|
}
|
|
3674
3724
|
/**
|
|
3675
|
-
*
|
|
3676
|
-
*
|
|
3725
|
+
* Resolve the HMR tunnel target (runner + dev server port) for a project.
|
|
3726
|
+
* The project's assigned runner must be connected and its dev server port
|
|
3727
|
+
* recorded - no fallback to "any runner".
|
|
3677
3728
|
*/
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3729
|
+
async getHmrTargetForProject(projectId) {
|
|
3730
|
+
try {
|
|
3731
|
+
const rows = await db.select({ runnerId: projects.runnerId, devServerPort: projects.devServerPort }).from(projects).where(eq(projects.id, projectId)).limit(1);
|
|
3732
|
+
if (rows.length === 0) return null;
|
|
3733
|
+
const { runnerId, devServerPort } = rows[0];
|
|
3734
|
+
if (!runnerId || !devServerPort) return null;
|
|
3735
|
+
if (!this.runnerConnections.has(runnerId)) return null;
|
|
3736
|
+
return { runnerId, port: devServerPort };
|
|
3737
|
+
} catch (error) {
|
|
3738
|
+
buildLogger$2.websocket.error("Failed to resolve HMR target", error, { projectId });
|
|
3739
|
+
return null;
|
|
3740
|
+
}
|
|
3681
3741
|
}
|
|
3682
3742
|
/**
|
|
3683
3743
|
* @deprecated Use discrete event broadcasts instead (broadcastBuildStarted, broadcastTodosUpdate,
|
|
@@ -3693,11 +3753,7 @@ var BuildWebSocketServer = class {
|
|
|
3693
3753
|
updates: []
|
|
3694
3754
|
});
|
|
3695
3755
|
}
|
|
3696
|
-
const
|
|
3697
|
-
const traceContext = activeSpan ? {
|
|
3698
|
-
trace: Sentry.getTraceData()["sentry-trace"],
|
|
3699
|
-
baggage: Sentry.getTraceData().baggage
|
|
3700
|
-
} : void 0;
|
|
3756
|
+
const traceContext = void 0;
|
|
3701
3757
|
const batch = this.pendingUpdates.get(key);
|
|
3702
3758
|
batch.updates.push({
|
|
3703
3759
|
type: "state-update",
|
|
@@ -3728,11 +3784,7 @@ var BuildWebSocketServer = class {
|
|
|
3728
3784
|
updates: []
|
|
3729
3785
|
});
|
|
3730
3786
|
}
|
|
3731
|
-
const
|
|
3732
|
-
const traceContext = activeSpan ? {
|
|
3733
|
-
trace: Sentry.getTraceData()["sentry-trace"],
|
|
3734
|
-
baggage: Sentry.getTraceData().baggage
|
|
3735
|
-
} : void 0;
|
|
3787
|
+
const traceContext = void 0;
|
|
3736
3788
|
const batch = this.pendingUpdates.get(key);
|
|
3737
3789
|
batch.updates.push({
|
|
3738
3790
|
type: "tool-call",
|
|
@@ -4333,12 +4385,6 @@ function buildPromptWithImages(prompt, messageParts) {
|
|
|
4333
4385
|
*
|
|
4334
4386
|
* With:
|
|
4335
4387
|
* query() SDK function -> minimal transformation -> output
|
|
4336
|
-
*
|
|
4337
|
-
* Sentry Integration:
|
|
4338
|
-
* - Manual gen_ai.* spans for AI Agent Monitoring in Sentry
|
|
4339
|
-
* - gen_ai.invoke_agent wraps the full query lifecycle
|
|
4340
|
-
* - gen_ai.execute_tool spans are emitted per tool call
|
|
4341
|
-
* - Token usage and cost are captured from the SDK result message
|
|
4342
4388
|
*/
|
|
4343
4389
|
function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortController) {
|
|
4344
4390
|
return async function* nativeClaudeQuery(prompt, workingDirectory, systemPrompt, _agent, _codexThreadId, messageParts) {
|
|
@@ -4362,12 +4408,6 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4362
4408
|
const platformPlugins = platformPluginDir
|
|
4363
4409
|
? [{ type: 'local', path: platformPluginDir }]
|
|
4364
4410
|
: [];
|
|
4365
|
-
if (platformPlugins.length === 0) {
|
|
4366
|
-
Sentry.logger.warn('Agent starting without platform skills — degraded capabilities', {
|
|
4367
|
-
model: modelId,
|
|
4368
|
-
workingDirectory,
|
|
4369
|
-
});
|
|
4370
|
-
}
|
|
4371
4411
|
// Check for multi-modal content
|
|
4372
4412
|
const hasImages = messageParts?.some(p => p.type === 'image');
|
|
4373
4413
|
if (hasImages) {
|
|
@@ -4430,28 +4470,6 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4430
4470
|
let pendingRequestMessages = [
|
|
4431
4471
|
{ role: 'user', content: finalPrompt.substring(0, 1000) },
|
|
4432
4472
|
];
|
|
4433
|
-
// Create the gen_ai.invoke_agent span as a child of the current active span.
|
|
4434
|
-
//
|
|
4435
|
-
// We use startInactiveSpan because this is an async generator — we can't use
|
|
4436
|
-
// startSpan/startSpanManual (both require a callback, and yields can't cross
|
|
4437
|
-
// callback boundaries). startInactiveSpan creates a span that inherits the
|
|
4438
|
-
// parent from the current active span (build.runner, restored by engine.ts
|
|
4439
|
-
// via Sentry.withActiveSpan).
|
|
4440
|
-
//
|
|
4441
|
-
// For tool spans, we use Sentry.withActiveSpan(agentSpan, ...) to temporarily
|
|
4442
|
-
// make the agent span active so tool spans become its children.
|
|
4443
|
-
const agentSpan = Sentry.startInactiveSpan({
|
|
4444
|
-
op: 'gen_ai.invoke_agent',
|
|
4445
|
-
name: 'invoke_agent hatchway-builder',
|
|
4446
|
-
attributes: {
|
|
4447
|
-
'gen_ai.operation.name': 'invoke_agent',
|
|
4448
|
-
'gen_ai.agent.name': 'hatchway-builder',
|
|
4449
|
-
'gen_ai.request.model': modelId,
|
|
4450
|
-
'gen_ai.request.messages': JSON.stringify([{ role: 'user', content: finalPrompt.substring(0, 1000) }]),
|
|
4451
|
-
'gen_ai.request.available_tools': JSON.stringify(['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Task', 'TodoWrite', 'WebFetch']
|
|
4452
|
-
.map(name => ({ name, type: 'function' }))),
|
|
4453
|
-
},
|
|
4454
|
-
});
|
|
4455
4473
|
try {
|
|
4456
4474
|
// Stream messages directly from the SDK
|
|
4457
4475
|
for await (const sdkMessage of query({ prompt: finalPrompt, options })) {
|
|
@@ -4465,65 +4483,17 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4465
4483
|
// pendingRequestMessages) and the response text / tool calls.
|
|
4466
4484
|
if (transformed.type === 'assistant' && transformed.message?.content) {
|
|
4467
4485
|
turnCount++;
|
|
4468
|
-
// Collect response text and tool calls from this turn
|
|
4469
|
-
const responseTexts = [];
|
|
4470
|
-
const responseToolCalls = [];
|
|
4471
4486
|
for (const block of transformed.message.content) {
|
|
4472
4487
|
if (block.type === 'tool_use') {
|
|
4473
4488
|
toolCallCount++;
|
|
4474
4489
|
debugLog$4(`[runner] [native-sdk] 🔧 Tool call: ${block.name}\n`);
|
|
4475
|
-
responseToolCalls.push({
|
|
4476
|
-
name: block.name ?? 'unknown',
|
|
4477
|
-
type: 'function_call',
|
|
4478
|
-
arguments: JSON.stringify(block.input).substring(0, 500),
|
|
4479
|
-
});
|
|
4480
4490
|
}
|
|
4481
4491
|
else if (block.type === 'text' && block.text) {
|
|
4482
4492
|
textBlockCount++;
|
|
4483
|
-
responseTexts.push(block.text);
|
|
4484
4493
|
}
|
|
4485
4494
|
}
|
|
4486
|
-
// Emit gen_ai.request span as a child of the agent span
|
|
4487
|
-
Sentry.withActiveSpan(agentSpan, () => {
|
|
4488
|
-
Sentry.startSpan({
|
|
4489
|
-
op: 'gen_ai.request',
|
|
4490
|
-
name: `request ${modelId}`,
|
|
4491
|
-
attributes: {
|
|
4492
|
-
'gen_ai.request.model': modelId,
|
|
4493
|
-
'gen_ai.operation.name': 'request',
|
|
4494
|
-
'gen_ai.request.messages': JSON.stringify(pendingRequestMessages),
|
|
4495
|
-
...(responseTexts.length > 0
|
|
4496
|
-
? { 'gen_ai.response.text': JSON.stringify(responseTexts.map(t => t.substring(0, 500))) }
|
|
4497
|
-
: {}),
|
|
4498
|
-
...(responseToolCalls.length > 0
|
|
4499
|
-
? { 'gen_ai.response.tool_calls': JSON.stringify(responseToolCalls) }
|
|
4500
|
-
: {}),
|
|
4501
|
-
},
|
|
4502
|
-
}, () => {
|
|
4503
|
-
// Span marks the LLM request/response for this turn
|
|
4504
|
-
});
|
|
4505
|
-
});
|
|
4506
4495
|
// Reset pending messages — the next turn's input will be tool results
|
|
4507
4496
|
pendingRequestMessages = [];
|
|
4508
|
-
// Emit gen_ai.execute_tool spans for each tool call in this turn
|
|
4509
|
-
for (const block of transformed.message.content) {
|
|
4510
|
-
if (block.type === 'tool_use') {
|
|
4511
|
-
Sentry.withActiveSpan(agentSpan, () => {
|
|
4512
|
-
Sentry.startSpan({
|
|
4513
|
-
op: 'gen_ai.execute_tool',
|
|
4514
|
-
name: `execute_tool ${block.name}`,
|
|
4515
|
-
attributes: {
|
|
4516
|
-
'gen_ai.tool.name': block.name,
|
|
4517
|
-
'gen_ai.tool.call_id': block.id,
|
|
4518
|
-
'gen_ai.tool.input': JSON.stringify(block.input).substring(0, 1000),
|
|
4519
|
-
'gen_ai.request.model': modelId,
|
|
4520
|
-
},
|
|
4521
|
-
}, () => {
|
|
4522
|
-
// Span marks the tool invocation point
|
|
4523
|
-
});
|
|
4524
|
-
});
|
|
4525
|
-
}
|
|
4526
|
-
}
|
|
4527
4497
|
}
|
|
4528
4498
|
// --- Accumulate tool results for the next gen_ai.request span ---
|
|
4529
4499
|
// 'user' messages contain tool_result blocks that feed the next LLM turn.
|
|
@@ -4550,32 +4520,6 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4550
4520
|
process.stderr.write(`[native-sdk] SDK init — plugins: ${JSON.stringify(loadedPlugins)}\n`);
|
|
4551
4521
|
process.stderr.write(`[native-sdk] SDK init — tools: ${toolCount} loaded\n`);
|
|
4552
4522
|
}
|
|
4553
|
-
// Set discovered skills on the agent span
|
|
4554
|
-
if (agentSpan) {
|
|
4555
|
-
agentSpan.setAttribute('gen_ai.agent.skills', discoveredSkills.join(', '));
|
|
4556
|
-
agentSpan.setAttribute('gen_ai.agent.skill_count', discoveredSkills.length);
|
|
4557
|
-
}
|
|
4558
|
-
if (discoveredSkills.length > 0) {
|
|
4559
|
-
Sentry.logger.info('SDK initialized with skills', {
|
|
4560
|
-
skillCount: String(discoveredSkills.length),
|
|
4561
|
-
skills: discoveredSkills.join(', '),
|
|
4562
|
-
pluginCount: String(loadedPlugins.length),
|
|
4563
|
-
plugins: loadedPlugins.map(p => p.name).join(', '),
|
|
4564
|
-
toolCount: String(toolCount),
|
|
4565
|
-
model: initMsg.model ?? modelId,
|
|
4566
|
-
workingDirectory,
|
|
4567
|
-
});
|
|
4568
|
-
}
|
|
4569
|
-
else {
|
|
4570
|
-
Sentry.logger.warn('SDK initialized but no skills discovered', {
|
|
4571
|
-
pluginCount: String(loadedPlugins.length),
|
|
4572
|
-
plugins: JSON.stringify(loadedPlugins),
|
|
4573
|
-
toolCount: String(toolCount),
|
|
4574
|
-
model: initMsg.model ?? modelId,
|
|
4575
|
-
workingDirectory,
|
|
4576
|
-
platformPluginDir: platformPluginDir ?? 'null',
|
|
4577
|
-
});
|
|
4578
|
-
}
|
|
4579
4523
|
}
|
|
4580
4524
|
// Capture tool_use_summary messages — these indicate skill content loading
|
|
4581
4525
|
if (sdkMessage.type === 'tool_use_summary') {
|
|
@@ -4587,29 +4531,6 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4587
4531
|
// Capture result messages — record token usage and cost on the agent span
|
|
4588
4532
|
if (sdkMessage.type === 'result') {
|
|
4589
4533
|
const resultMsg = sdkMessage;
|
|
4590
|
-
if (agentSpan) {
|
|
4591
|
-
// Standard gen_ai token usage attributes (Sentry AI Agent Monitoring spec)
|
|
4592
|
-
agentSpan.setAttribute('gen_ai.usage.input_tokens', resultMsg.usage?.input_tokens ?? 0);
|
|
4593
|
-
agentSpan.setAttribute('gen_ai.usage.output_tokens', resultMsg.usage?.output_tokens ?? 0);
|
|
4594
|
-
agentSpan.setAttribute('gen_ai.usage.total_tokens', (resultMsg.usage?.input_tokens ?? 0) + (resultMsg.usage?.output_tokens ?? 0));
|
|
4595
|
-
if (resultMsg.usage?.cache_read_input_tokens) {
|
|
4596
|
-
agentSpan.setAttribute('gen_ai.usage.input_tokens.cached', resultMsg.usage.cache_read_input_tokens);
|
|
4597
|
-
}
|
|
4598
|
-
if (resultMsg.usage?.cache_creation_input_tokens) {
|
|
4599
|
-
agentSpan.setAttribute('gen_ai.usage.input_tokens.cache_write', resultMsg.usage.cache_creation_input_tokens);
|
|
4600
|
-
}
|
|
4601
|
-
// Response text (truncated for span safety)
|
|
4602
|
-
if (resultMsg.result) {
|
|
4603
|
-
agentSpan.setAttribute('gen_ai.response.text', JSON.stringify(resultMsg.result.substring(0, 1000)));
|
|
4604
|
-
}
|
|
4605
|
-
// Custom (non-spec) attributes for operational insight
|
|
4606
|
-
agentSpan.setAttribute('hatchway.cost_usd', resultMsg.total_cost_usd ?? 0);
|
|
4607
|
-
agentSpan.setAttribute('hatchway.num_turns', resultMsg.num_turns ?? 0);
|
|
4608
|
-
agentSpan.setAttribute('hatchway.num_tool_calls', toolCallCount);
|
|
4609
|
-
agentSpan.setAttribute('hatchway.result', resultMsg.subtype ?? 'unknown');
|
|
4610
|
-
agentSpan.setAttribute('hatchway.duration_ms', resultMsg.duration_ms ?? 0);
|
|
4611
|
-
agentSpan.setAttribute('hatchway.duration_api_ms', resultMsg.duration_api_ms ?? 0);
|
|
4612
|
-
}
|
|
4613
4534
|
if (resultMsg.subtype === 'success') {
|
|
4614
4535
|
debugLog$4(`[runner] [native-sdk] ✅ Query complete - ${resultMsg.num_turns} turns, $${resultMsg.total_cost_usd?.toFixed(4)} USD\n`);
|
|
4615
4536
|
}
|
|
@@ -4622,16 +4543,8 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
|
|
|
4622
4543
|
}
|
|
4623
4544
|
catch (error) {
|
|
4624
4545
|
debugLog$4(`[runner] [native-sdk] ❌ Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
4625
|
-
if (agentSpan) {
|
|
4626
|
-
agentSpan.setStatus({ code: 2, message: error instanceof Error ? error.message : String(error) });
|
|
4627
|
-
}
|
|
4628
|
-
Sentry.captureException(error);
|
|
4629
4546
|
throw error;
|
|
4630
4547
|
}
|
|
4631
|
-
finally {
|
|
4632
|
-
// End the agent span regardless of success/failure
|
|
4633
|
-
agentSpan?.end();
|
|
4634
|
-
}
|
|
4635
4548
|
};
|
|
4636
4549
|
}
|
|
4637
4550
|
|
|
@@ -4840,20 +4753,6 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
4840
4753
|
// Parse model ID
|
|
4841
4754
|
const { provider: providerID, model: modelID } = parseModelId(normalizeModelId(modelId));
|
|
4842
4755
|
let sessionId = null;
|
|
4843
|
-
// Start Sentry AI agent span for the entire OpenCode query
|
|
4844
|
-
// This provides visibility into AI operations in Sentry's trace view
|
|
4845
|
-
const aiSpan = Sentry.startInactiveSpan({
|
|
4846
|
-
name: 'opencode.query',
|
|
4847
|
-
op: 'ai.pipeline',
|
|
4848
|
-
attributes: {
|
|
4849
|
-
'ai.pipeline.name': 'opencode',
|
|
4850
|
-
'ai.model_id': `${providerID}/${modelID}`,
|
|
4851
|
-
'ai.provider': providerID,
|
|
4852
|
-
'ai.streaming': true,
|
|
4853
|
-
'gen_ai.system': 'opencode',
|
|
4854
|
-
'gen_ai.request.model': modelID,
|
|
4855
|
-
},
|
|
4856
|
-
});
|
|
4857
4756
|
try {
|
|
4858
4757
|
// Step 1: Create a session
|
|
4859
4758
|
debugLog$3('[runner] [opencode-sdk] Creating session...');
|
|
@@ -4871,10 +4770,6 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
4871
4770
|
const session = await sessionResponse.json();
|
|
4872
4771
|
sessionId = session.id;
|
|
4873
4772
|
debugLog$3(`[runner] [opencode-sdk] Session created: ${sessionId}`);
|
|
4874
|
-
// Update span with session info
|
|
4875
|
-
if (sessionId) {
|
|
4876
|
-
aiSpan?.setAttribute('opencode.session_id', sessionId);
|
|
4877
|
-
}
|
|
4878
4773
|
// Step 2: Subscribe to events
|
|
4879
4774
|
debugLog$3('[runner] [opencode-sdk] Subscribing to events...');
|
|
4880
4775
|
const eventResponse = await fetch(`${baseUrl}/event`, {
|
|
@@ -5000,16 +4895,9 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
5000
4895
|
};
|
|
5001
4896
|
}
|
|
5002
4897
|
debugLog$3('[runner] [opencode-sdk] Query complete');
|
|
5003
|
-
// Update span with final metrics
|
|
5004
|
-
aiSpan?.setAttribute('opencode.tool_calls', toolCallCount);
|
|
5005
|
-
aiSpan?.setAttribute('opencode.messages', messageCount);
|
|
5006
|
-
aiSpan?.setStatus({ code: 1 }); // OK status
|
|
5007
4898
|
}
|
|
5008
4899
|
catch (error) {
|
|
5009
4900
|
debugLog$3(`[runner] [opencode-sdk] Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
5010
|
-
Sentry.captureException(error);
|
|
5011
|
-
// Mark span as errored
|
|
5012
|
-
aiSpan?.setStatus({ code: 2, message: error instanceof Error ? error.message : String(error) });
|
|
5013
4901
|
// Yield error result
|
|
5014
4902
|
yield {
|
|
5015
4903
|
type: 'result',
|
|
@@ -5019,10 +4907,6 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
|
|
|
5019
4907
|
};
|
|
5020
4908
|
throw error;
|
|
5021
4909
|
}
|
|
5022
|
-
finally {
|
|
5023
|
-
// End the AI span
|
|
5024
|
-
aiSpan?.end();
|
|
5025
|
-
}
|
|
5026
4910
|
};
|
|
5027
4911
|
}
|
|
5028
4912
|
/**
|
|
@@ -5524,15 +5408,6 @@ function createDroidQuery(modelId) {
|
|
|
5524
5408
|
process.stderr.write(`[runner] [droid-sdk] Events received: ${messageCount}\n`);
|
|
5525
5409
|
process.stderr.write(`[runner] [droid-sdk] Tool calls: ${toolCallCount}\n`);
|
|
5526
5410
|
process.stderr.write(`[runner] [droid-sdk] Last event type: ${lastEventType}\n\n`);
|
|
5527
|
-
Sentry.captureException(error, {
|
|
5528
|
-
extra: {
|
|
5529
|
-
modelId,
|
|
5530
|
-
workingDirectory,
|
|
5531
|
-
promptLength: prompt.length,
|
|
5532
|
-
eventsReceived: messageCount,
|
|
5533
|
-
toolCalls: toolCallCount,
|
|
5534
|
-
}
|
|
5535
|
-
});
|
|
5536
5411
|
throw error;
|
|
5537
5412
|
}
|
|
5538
5413
|
finally {
|
|
@@ -5542,6 +5417,39 @@ function createDroidQuery(modelId) {
|
|
|
5542
5417
|
};
|
|
5543
5418
|
}
|
|
5544
5419
|
|
|
5420
|
+
// src/lib/path-safety.ts
|
|
5421
|
+
var SLUG_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
5422
|
+
var MAX_SLUG_LENGTH = 128;
|
|
5423
|
+
function isValidProjectSlug(slug) {
|
|
5424
|
+
return typeof slug === "string" && slug.length > 0 && slug.length <= MAX_SLUG_LENGTH && SLUG_PATTERN.test(slug) && !slug.includes("..");
|
|
5425
|
+
}
|
|
5426
|
+
function assertValidProjectSlug(slug) {
|
|
5427
|
+
if (!isValidProjectSlug(slug)) {
|
|
5428
|
+
throw new Error(`Invalid project slug: ${String(slug)}`);
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
function resolveProjectPath(workspaceRoot, slug) {
|
|
5432
|
+
assertValidProjectSlug(slug);
|
|
5433
|
+
const root = resolve$1(workspaceRoot);
|
|
5434
|
+
const projectPath = resolve$1(root, slug);
|
|
5435
|
+
const rel = relative$1(root, projectPath);
|
|
5436
|
+
if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) {
|
|
5437
|
+
throw new Error(`Invalid project slug: ${slug}`);
|
|
5438
|
+
}
|
|
5439
|
+
return projectPath;
|
|
5440
|
+
}
|
|
5441
|
+
function resolveWithinProject(projectPath, relPath) {
|
|
5442
|
+
if (typeof relPath !== "string" || relPath.length === 0 || relPath.includes("\0")) {
|
|
5443
|
+
throw new Error(`Invalid file path: ${String(relPath)}`);
|
|
5444
|
+
}
|
|
5445
|
+
const fullPath = resolve$1(projectPath, relPath);
|
|
5446
|
+
const rel = relative$1(projectPath, fullPath);
|
|
5447
|
+
if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) {
|
|
5448
|
+
throw new Error("Invalid file path - outside project directory");
|
|
5449
|
+
}
|
|
5450
|
+
return fullPath;
|
|
5451
|
+
}
|
|
5452
|
+
|
|
5545
5453
|
// src/lib/logging/build-logger.ts
|
|
5546
5454
|
var BuildLogger = class {
|
|
5547
5455
|
buildId = null;
|
|
@@ -5593,23 +5501,6 @@ var BuildLogger = class {
|
|
|
5593
5501
|
} else {
|
|
5594
5502
|
logFn(`${icon} ${prefix} ${message}`);
|
|
5595
5503
|
}
|
|
5596
|
-
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
5597
|
-
try {
|
|
5598
|
-
if (level === "warn" || level === "error") {
|
|
5599
|
-
Sentry.addBreadcrumb({
|
|
5600
|
-
category: `build-logger.${context}`,
|
|
5601
|
-
message,
|
|
5602
|
-
level: level === "error" ? "error" : "warning",
|
|
5603
|
-
data: {
|
|
5604
|
-
...data,
|
|
5605
|
-
buildId: this.buildId,
|
|
5606
|
-
projectId: this.projectId
|
|
5607
|
-
}
|
|
5608
|
-
});
|
|
5609
|
-
}
|
|
5610
|
-
} catch {
|
|
5611
|
-
}
|
|
5612
|
-
}
|
|
5613
5504
|
}
|
|
5614
5505
|
/**
|
|
5615
5506
|
* Orchestrator-specific logging methods
|
|
@@ -5986,12 +5877,12 @@ var init_tags = __esm({
|
|
|
5986
5877
|
sdk: "agent"
|
|
5987
5878
|
},
|
|
5988
5879
|
{
|
|
5989
|
-
value: "claude-opus-4-
|
|
5990
|
-
label: "Claude Opus 4.
|
|
5880
|
+
value: "claude-opus-4-8",
|
|
5881
|
+
label: "Claude Opus 4.8",
|
|
5991
5882
|
description: "Anthropic Claude - Most capable for complex tasks",
|
|
5992
5883
|
logo: "/claude.png",
|
|
5993
5884
|
provider: "claude-code",
|
|
5994
|
-
model: "claude-opus-4-
|
|
5885
|
+
model: "claude-opus-4-8",
|
|
5995
5886
|
sdk: "agent"
|
|
5996
5887
|
},
|
|
5997
5888
|
{
|
|
@@ -6015,10 +5906,10 @@ var init_tags = __esm({
|
|
|
6015
5906
|
{
|
|
6016
5907
|
value: "factory-droid-opus",
|
|
6017
5908
|
label: "Factory Droid (Opus)",
|
|
6018
|
-
description: "Factory Droid SDK with Claude Opus 4.
|
|
5909
|
+
description: "Factory Droid SDK with Claude Opus 4.8",
|
|
6019
5910
|
logo: "/factory.svg",
|
|
6020
5911
|
provider: "factory-droid",
|
|
6021
|
-
model: "claude-opus-4-
|
|
5912
|
+
model: "claude-opus-4-8",
|
|
6022
5913
|
sdk: "droid"
|
|
6023
5914
|
},
|
|
6024
5915
|
{
|
|
@@ -7257,12 +7148,6 @@ async function createBuildStream(options) {
|
|
|
7257
7148
|
debugLog$1();
|
|
7258
7149
|
const generator = query(fullPrompt, actualWorkingDir, systemPrompt, agent, options.codexThreadId, messageParts);
|
|
7259
7150
|
debugLog$1();
|
|
7260
|
-
// Capture the active Sentry span BEFORE creating the ReadableStream.
|
|
7261
|
-
// The ReadableStream.start() callback runs in a new async context where the
|
|
7262
|
-
// parent build.runner span is no longer active. We restore it with withActiveSpan()
|
|
7263
|
-
// so that gen_ai.invoke_agent spans created inside the query generator are
|
|
7264
|
-
// properly nested as children of the build.runner span.
|
|
7265
|
-
const parentSpan = Sentry.getActiveSpan();
|
|
7266
7151
|
// Create a ReadableStream from the AsyncGenerator
|
|
7267
7152
|
const stream = new ReadableStream({
|
|
7268
7153
|
async start(controller) {
|
|
@@ -7298,13 +7183,7 @@ async function createBuildStream(options) {
|
|
|
7298
7183
|
process.chdir(originalCwd);
|
|
7299
7184
|
}
|
|
7300
7185
|
};
|
|
7301
|
-
|
|
7302
|
-
if (parentSpan) {
|
|
7303
|
-
await Sentry.withActiveSpan(parentSpan, consume);
|
|
7304
|
-
}
|
|
7305
|
-
else {
|
|
7306
|
-
await consume();
|
|
7307
|
-
}
|
|
7186
|
+
await consume();
|
|
7308
7187
|
},
|
|
7309
7188
|
});
|
|
7310
7189
|
debugLog$1();
|
|
@@ -7935,6 +7814,12 @@ async function runHealthCheck(projectId, port) {
|
|
|
7935
7814
|
};
|
|
7936
7815
|
}
|
|
7937
7816
|
}
|
|
7817
|
+
/**
|
|
7818
|
+
* Get active process for a project
|
|
7819
|
+
*/
|
|
7820
|
+
function getDevServer(projectId) {
|
|
7821
|
+
return activeProcesses.get(projectId);
|
|
7822
|
+
}
|
|
7938
7823
|
/**
|
|
7939
7824
|
* Get all active project IDs
|
|
7940
7825
|
*/
|
|
@@ -7960,6 +7845,7 @@ var processManager = /*#__PURE__*/Object.freeze({
|
|
|
7960
7845
|
checkPortInUse: checkPortInUse,
|
|
7961
7846
|
findAvailablePort: findAvailablePort,
|
|
7962
7847
|
getAllActiveProjectIds: getAllActiveProjectIds,
|
|
7848
|
+
getDevServer: getDevServer,
|
|
7963
7849
|
runHealthCheck: runHealthCheck,
|
|
7964
7850
|
setSilentMode: setSilentMode$1,
|
|
7965
7851
|
startDevServer: startDevServer,
|
|
@@ -9876,12 +9762,12 @@ var TAG_DEFINITIONS = [
|
|
|
9876
9762
|
sdk: "agent"
|
|
9877
9763
|
},
|
|
9878
9764
|
{
|
|
9879
|
-
value: "claude-opus-4-
|
|
9880
|
-
label: "Claude Opus 4.
|
|
9765
|
+
value: "claude-opus-4-8",
|
|
9766
|
+
label: "Claude Opus 4.8",
|
|
9881
9767
|
description: "Anthropic Claude - Most capable for complex tasks",
|
|
9882
9768
|
logo: "/claude.png",
|
|
9883
9769
|
provider: "claude-code",
|
|
9884
|
-
model: "claude-opus-4-
|
|
9770
|
+
model: "claude-opus-4-8",
|
|
9885
9771
|
sdk: "agent"
|
|
9886
9772
|
},
|
|
9887
9773
|
{
|
|
@@ -9905,10 +9791,10 @@ var TAG_DEFINITIONS = [
|
|
|
9905
9791
|
{
|
|
9906
9792
|
value: "factory-droid-opus",
|
|
9907
9793
|
label: "Factory Droid (Opus)",
|
|
9908
|
-
description: "Factory Droid SDK with Claude Opus 4.
|
|
9794
|
+
description: "Factory Droid SDK with Claude Opus 4.8",
|
|
9909
9795
|
logo: "/factory.svg",
|
|
9910
9796
|
provider: "factory-droid",
|
|
9911
|
-
model: "claude-opus-4-
|
|
9797
|
+
model: "claude-opus-4-8",
|
|
9912
9798
|
sdk: "droid"
|
|
9913
9799
|
},
|
|
9914
9800
|
{
|
|
@@ -10127,7 +10013,7 @@ var TAG_DEFINITIONS = [
|
|
|
10127
10013
|
const MODEL_MAP = {
|
|
10128
10014
|
'claude-haiku-4-5': 'claude-sonnet-4-6', // Haiku 4.5 not yet available, use Sonnet
|
|
10129
10015
|
'claude-sonnet-4-6': 'claude-sonnet-4-6',
|
|
10130
|
-
'claude-opus-4-
|
|
10016
|
+
'claude-opus-4-8': 'claude-opus-4-8',
|
|
10131
10017
|
};
|
|
10132
10018
|
function resolveModelName(modelId) {
|
|
10133
10019
|
return MODEL_MAP[modelId] || 'claude-sonnet-4-6';
|
|
@@ -10195,7 +10081,6 @@ CRITICAL: Your response must START with { and END with }. Output only the JSON o
|
|
|
10195
10081
|
}
|
|
10196
10082
|
catch (error) {
|
|
10197
10083
|
console.error('[project-analyzer] SDK query failed:', error);
|
|
10198
|
-
Sentry.captureException(error);
|
|
10199
10084
|
throw error;
|
|
10200
10085
|
}
|
|
10201
10086
|
if (!responseText) {
|
|
@@ -10731,8 +10616,6 @@ class HmrProxyManager {
|
|
|
10731
10616
|
// Export singleton instance
|
|
10732
10617
|
const hmrProxyManager = new HmrProxyManager();
|
|
10733
10618
|
|
|
10734
|
-
// Sentry is initialized via --import flag (see package.json scripts)
|
|
10735
|
-
// This ensures instrumentation loads before any other modules
|
|
10736
10619
|
// AI SDK log warnings disabled (legacy - no longer using AI SDK)
|
|
10737
10620
|
/**
|
|
10738
10621
|
* Truncate strings for logging to prevent excessive output
|
|
@@ -10898,13 +10781,6 @@ function createCodexQuery() {
|
|
|
10898
10781
|
log(`🚀 [codex-query] Turn ${turnCount}: ${turnCount === 1 ? 'Initial request' : 'Continuing work'}...`);
|
|
10899
10782
|
// Log full prompt being sent to Codex
|
|
10900
10783
|
if (turnCount === 1) {
|
|
10901
|
-
Sentry.logger.info(Sentry.logger.fmt `Full Codex prompt (Turn 1) ${{
|
|
10902
|
-
prompt: turnPrompt,
|
|
10903
|
-
promptLength: turnPrompt.length,
|
|
10904
|
-
promptPreview: turnPrompt.substring(0, 200),
|
|
10905
|
-
operation: 'codex_query',
|
|
10906
|
-
turnCount: 1,
|
|
10907
|
-
}}`);
|
|
10908
10784
|
// Also log to file
|
|
10909
10785
|
fileLog.info('━━━ FULL CODEX PROMPT ━━━');
|
|
10910
10786
|
fileLog.info(turnPrompt);
|
|
@@ -11297,29 +11173,11 @@ async function startRunner(options = {}) {
|
|
|
11297
11173
|
throw lastError || new Error('Persistence failed after retries');
|
|
11298
11174
|
}
|
|
11299
11175
|
async function persistBuildEventDirect(context, event) {
|
|
11300
|
-
return
|
|
11301
|
-
name: `persist.build-event.${event.type}`,
|
|
11302
|
-
op: 'http.client',
|
|
11303
|
-
attributes: {
|
|
11304
|
-
'http.method': 'POST',
|
|
11305
|
-
'http.url': `${apiBaseUrl}/api/build-events`,
|
|
11306
|
-
'event.type': event.type,
|
|
11307
|
-
'event.tool_name': event.toolName || undefined,
|
|
11308
|
-
},
|
|
11309
|
-
}, async () => {
|
|
11310
|
-
// Get trace context for propagation
|
|
11311
|
-
const traceData = Sentry.getTraceData();
|
|
11176
|
+
return await (async () => {
|
|
11312
11177
|
const headers = {
|
|
11313
11178
|
'Authorization': `Bearer ${runnerSharedSecret}`,
|
|
11314
11179
|
'Content-Type': 'application/json',
|
|
11315
11180
|
};
|
|
11316
|
-
// Add Sentry trace headers for distributed tracing
|
|
11317
|
-
if (traceData['sentry-trace']) {
|
|
11318
|
-
headers['sentry-trace'] = traceData['sentry-trace'];
|
|
11319
|
-
}
|
|
11320
|
-
if (traceData.baggage) {
|
|
11321
|
-
headers['baggage'] = traceData.baggage;
|
|
11322
|
-
}
|
|
11323
11181
|
const response = await fetchWithRetry(`${apiBaseUrl}/api/build-events`, {
|
|
11324
11182
|
method: 'POST',
|
|
11325
11183
|
headers,
|
|
@@ -11337,7 +11195,7 @@ async function startRunner(options = {}) {
|
|
|
11337
11195
|
if (!response.ok) {
|
|
11338
11196
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
11339
11197
|
}
|
|
11340
|
-
});
|
|
11198
|
+
})();
|
|
11341
11199
|
}
|
|
11342
11200
|
/**
|
|
11343
11201
|
* Send a build event to the HTTP persistence endpoint.
|
|
@@ -11415,16 +11273,7 @@ async function startRunner(options = {}) {
|
|
|
11415
11273
|
* Send a runner lifecycle event to the HTTP persistence endpoint.
|
|
11416
11274
|
*/
|
|
11417
11275
|
async function persistRunnerEvent(event) {
|
|
11418
|
-
|
|
11419
|
-
return Sentry.startSpan({
|
|
11420
|
-
name: `persist.runner-event.${event.type}`,
|
|
11421
|
-
op: 'http.client',
|
|
11422
|
-
attributes: {
|
|
11423
|
-
'http.method': 'POST',
|
|
11424
|
-
'http.url': `${apiBaseUrl}/api/runner/events`,
|
|
11425
|
-
'event.type': event.type,
|
|
11426
|
-
},
|
|
11427
|
-
}, async () => {
|
|
11276
|
+
return await (async () => {
|
|
11428
11277
|
try {
|
|
11429
11278
|
const response = await fetch(`${apiBaseUrl}/api/runner/events`, {
|
|
11430
11279
|
method: 'POST',
|
|
@@ -11440,9 +11289,9 @@ async function startRunner(options = {}) {
|
|
|
11440
11289
|
}
|
|
11441
11290
|
catch (error) {
|
|
11442
11291
|
console.error('[runner] Error persisting runner event:', error);
|
|
11443
|
-
throw error;
|
|
11292
|
+
throw error;
|
|
11444
11293
|
}
|
|
11445
|
-
});
|
|
11294
|
+
})();
|
|
11446
11295
|
}
|
|
11447
11296
|
// Event types that trigger DB writes and should be persisted via HTTP
|
|
11448
11297
|
const DB_WORTHY_RUNNER_EVENTS = [
|
|
@@ -11468,16 +11317,6 @@ async function startRunner(options = {}) {
|
|
|
11468
11317
|
}
|
|
11469
11318
|
const sendOperation = () => {
|
|
11470
11319
|
try {
|
|
11471
|
-
// Attach trace context to ALL events for distributed tracing
|
|
11472
|
-
// This allows events to be linked back to the originating frontend request
|
|
11473
|
-
const span = Sentry.getActiveSpan();
|
|
11474
|
-
if (span) {
|
|
11475
|
-
const traceData = Sentry.getTraceData();
|
|
11476
|
-
event._sentry = {
|
|
11477
|
-
trace: traceData['sentry-trace'],
|
|
11478
|
-
baggage: traceData.baggage,
|
|
11479
|
-
};
|
|
11480
|
-
}
|
|
11481
11320
|
const eventJson = JSON.stringify(event);
|
|
11482
11321
|
// Only log important events
|
|
11483
11322
|
if (event.type === "error") {
|
|
@@ -11963,7 +11802,6 @@ async function startRunner(options = {}) {
|
|
|
11963
11802
|
break;
|
|
11964
11803
|
}
|
|
11965
11804
|
case "start-tunnel": {
|
|
11966
|
-
const tunnelStartTime = Date.now();
|
|
11967
11805
|
try {
|
|
11968
11806
|
const { port } = command.payload;
|
|
11969
11807
|
log(`🔗 Starting tunnel for port ${port}...`);
|
|
@@ -11978,15 +11816,6 @@ async function startRunner(options = {}) {
|
|
|
11978
11816
|
// Create tunnel
|
|
11979
11817
|
const tunnelUrl = await tunnelManager.createTunnel(port);
|
|
11980
11818
|
logger.tunnel({ port, url: tunnelUrl, status: 'created' });
|
|
11981
|
-
// Instrument tunnel startup timing
|
|
11982
|
-
const tunnelDuration = Date.now() - tunnelStartTime;
|
|
11983
|
-
Sentry.metrics.distribution('tunnel_startup_duration', tunnelDuration, {
|
|
11984
|
-
unit: 'millisecond',
|
|
11985
|
-
attributes: {
|
|
11986
|
-
port: port.toString(),
|
|
11987
|
-
success: 'true'
|
|
11988
|
-
}
|
|
11989
|
-
});
|
|
11990
11819
|
sendEvent({
|
|
11991
11820
|
type: "tunnel-created",
|
|
11992
11821
|
...buildEventBase(command.projectId, command.id),
|
|
@@ -11996,16 +11825,6 @@ async function startRunner(options = {}) {
|
|
|
11996
11825
|
}
|
|
11997
11826
|
catch (error) {
|
|
11998
11827
|
console.error("Failed to create tunnel:", error);
|
|
11999
|
-
// Instrument failed tunnel startup timing
|
|
12000
|
-
const tunnelDuration = Date.now() - tunnelStartTime;
|
|
12001
|
-
Sentry.metrics.distribution('tunnel_startup_duration', tunnelDuration, {
|
|
12002
|
-
unit: 'millisecond',
|
|
12003
|
-
attributes: {
|
|
12004
|
-
port: command.payload.port.toString(),
|
|
12005
|
-
success: 'false',
|
|
12006
|
-
error_type: error instanceof Error ? error.constructor.name : 'unknown'
|
|
12007
|
-
}
|
|
12008
|
-
});
|
|
12009
11828
|
sendEvent({
|
|
12010
11829
|
type: "error",
|
|
12011
11830
|
...buildEventBase(command.projectId, command.id),
|
|
@@ -12054,7 +11873,8 @@ async function startRunner(options = {}) {
|
|
|
12054
11873
|
case "delete-project-files": {
|
|
12055
11874
|
try {
|
|
12056
11875
|
const { slug } = command.payload;
|
|
12057
|
-
|
|
11876
|
+
// Validates the slug and guarantees the path stays inside the workspace
|
|
11877
|
+
const projectPath = resolveProjectPath(WORKSPACE_ROOT, slug);
|
|
12058
11878
|
console.log(`[runner] 🗑️ Deleting project files for slug: ${slug}`);
|
|
12059
11879
|
console.log(`[runner] Path: ${projectPath}`);
|
|
12060
11880
|
// First, stop any running dev server for this project to release file locks
|
|
@@ -12126,13 +11946,9 @@ async function startRunner(options = {}) {
|
|
|
12126
11946
|
case "read-file": {
|
|
12127
11947
|
try {
|
|
12128
11948
|
const { slug, filePath } = command.payload;
|
|
12129
|
-
const projectPath =
|
|
12130
|
-
const fullPath =
|
|
11949
|
+
const projectPath = resolveProjectPath(WORKSPACE_ROOT, slug);
|
|
11950
|
+
const fullPath = resolveWithinProject(projectPath, filePath);
|
|
12131
11951
|
console.log(`[runner] 📖 Reading file: ${filePath} from project: ${slug}`);
|
|
12132
|
-
// Security: Ensure path is within project directory
|
|
12133
|
-
if (!fullPath.startsWith(projectPath)) {
|
|
12134
|
-
throw new Error("Invalid file path - outside project directory");
|
|
12135
|
-
}
|
|
12136
11952
|
const { readFile, stat } = await import('node:fs/promises');
|
|
12137
11953
|
const stats = await stat(fullPath);
|
|
12138
11954
|
const content = await readFile(fullPath, "utf-8");
|
|
@@ -12160,13 +11976,9 @@ async function startRunner(options = {}) {
|
|
|
12160
11976
|
case "write-file": {
|
|
12161
11977
|
try {
|
|
12162
11978
|
const { slug, filePath, content } = command.payload;
|
|
12163
|
-
const projectPath =
|
|
12164
|
-
const fullPath =
|
|
11979
|
+
const projectPath = resolveProjectPath(WORKSPACE_ROOT, slug);
|
|
11980
|
+
const fullPath = resolveWithinProject(projectPath, filePath);
|
|
12165
11981
|
console.log(`[runner] 💾 Writing file: ${filePath} to project: ${slug}`);
|
|
12166
|
-
// Security: Ensure path is within project directory
|
|
12167
|
-
if (!fullPath.startsWith(projectPath)) {
|
|
12168
|
-
throw new Error("Invalid file path - outside project directory");
|
|
12169
|
-
}
|
|
12170
11982
|
const { writeFile } = await import('node:fs/promises');
|
|
12171
11983
|
await writeFile(fullPath, content, "utf-8");
|
|
12172
11984
|
console.log(`[runner] ✅ File written successfully (${content.length} bytes)`);
|
|
@@ -12191,14 +12003,12 @@ async function startRunner(options = {}) {
|
|
|
12191
12003
|
case "list-files": {
|
|
12192
12004
|
try {
|
|
12193
12005
|
const { slug, path: subPath } = command.payload;
|
|
12194
|
-
const projectPath =
|
|
12195
|
-
const targetPath = subPath
|
|
12006
|
+
const projectPath = resolveProjectPath(WORKSPACE_ROOT, slug);
|
|
12007
|
+
const targetPath = subPath
|
|
12008
|
+
? resolveWithinProject(projectPath, subPath)
|
|
12009
|
+
: projectPath;
|
|
12196
12010
|
console.log(`[runner] 📁 Listing files for project: ${slug}`);
|
|
12197
12011
|
console.log(`[runner] Path: ${targetPath}`);
|
|
12198
|
-
// Security: Ensure path is within project directory
|
|
12199
|
-
if (!targetPath.startsWith(projectPath)) {
|
|
12200
|
-
throw new Error("Invalid path - outside project directory");
|
|
12201
|
-
}
|
|
12202
12012
|
const { readdir, stat } = await import('node:fs/promises');
|
|
12203
12013
|
const entries = await readdir(targetPath);
|
|
12204
12014
|
const files = await Promise.all(entries.map(async (name) => {
|
|
@@ -12241,7 +12051,7 @@ async function startRunner(options = {}) {
|
|
|
12241
12051
|
const model = agent === 'claude-code' &&
|
|
12242
12052
|
(claudeModelFromPayload === 'claude-haiku-4-5' ||
|
|
12243
12053
|
claudeModelFromPayload === 'claude-sonnet-4-6' ||
|
|
12244
|
-
claudeModelFromPayload === 'claude-opus-4-
|
|
12054
|
+
claudeModelFromPayload === 'claude-opus-4-8')
|
|
12245
12055
|
? claudeModelFromPayload
|
|
12246
12056
|
: DEFAULT_CLAUDE_MODEL_ID;
|
|
12247
12057
|
logger.buildReceived({
|
|
@@ -12262,7 +12072,6 @@ async function startRunner(options = {}) {
|
|
|
12262
12072
|
fileLog.info("Agent:", command.payload?.agent);
|
|
12263
12073
|
fileLog.info("Template:", command.payload?.template);
|
|
12264
12074
|
// REMOVED: Manual Sentry span creation - rely on automatic instrumentation
|
|
12265
|
-
// await Sentry.startSpan({ name: "runner.build", op: "ai.build", ... }, async () => {
|
|
12266
12075
|
// Build operation (previously wrapped in Sentry span)
|
|
12267
12076
|
try {
|
|
12268
12077
|
loggedFirstChunk = false;
|
|
@@ -12270,9 +12079,11 @@ async function startRunner(options = {}) {
|
|
|
12270
12079
|
throw new Error("Invalid build payload");
|
|
12271
12080
|
}
|
|
12272
12081
|
// Calculate the project directory using slug
|
|
12082
|
+
// Slug may be LLM-derived from the user prompt - validate before
|
|
12083
|
+
// it touches the filesystem
|
|
12273
12084
|
const projectSlug = command.payload.projectSlug || command.projectId;
|
|
12274
12085
|
const projectName = command.payload.projectName || projectSlug;
|
|
12275
|
-
const projectDirectory =
|
|
12086
|
+
const projectDirectory = resolveProjectPath(WORKSPACE_ROOT, projectSlug);
|
|
12276
12087
|
log("project directory:", projectDirectory);
|
|
12277
12088
|
log("project slug:", projectSlug);
|
|
12278
12089
|
log("project name:", projectName);
|
|
@@ -12283,7 +12094,7 @@ async function startRunner(options = {}) {
|
|
|
12283
12094
|
const claudeModel = agent === "claude-code" &&
|
|
12284
12095
|
(command.payload.claudeModel === "claude-haiku-4-5" ||
|
|
12285
12096
|
command.payload.claudeModel === "claude-sonnet-4-6" ||
|
|
12286
|
-
command.payload.claudeModel === "claude-opus-4-
|
|
12097
|
+
command.payload.claudeModel === "claude-opus-4-8")
|
|
12287
12098
|
? command.payload.claudeModel
|
|
12288
12099
|
: DEFAULT_CLAUDE_MODEL_ID;
|
|
12289
12100
|
// For factory-droid, use the droidModel from payload
|
|
@@ -12708,7 +12519,7 @@ async function startRunner(options = {}) {
|
|
|
12708
12519
|
// Detect framework from generated files
|
|
12709
12520
|
let detectedFramework = null;
|
|
12710
12521
|
try {
|
|
12711
|
-
const { detectFrameworkFromFilesystem } = await import('./chunks/port-allocator-
|
|
12522
|
+
const { detectFrameworkFromFilesystem } = await import('./chunks/port-allocator-DAjm7X-F.js');
|
|
12712
12523
|
const framework = await detectFrameworkFromFilesystem(projectDirectory);
|
|
12713
12524
|
detectedFramework = framework;
|
|
12714
12525
|
if (framework) {
|
|
@@ -12810,10 +12621,6 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
|
|
|
12810
12621
|
catch (error) {
|
|
12811
12622
|
const errorMessage = error instanceof Error ? error.message : "Failed to run build";
|
|
12812
12623
|
logger.buildFailed(errorMessage);
|
|
12813
|
-
Sentry.getActiveSpan()?.setStatus({
|
|
12814
|
-
code: 2, // SPAN_STATUS_ERROR
|
|
12815
|
-
message: "Build failed",
|
|
12816
|
-
});
|
|
12817
12624
|
sendEvent({
|
|
12818
12625
|
type: "build-failed",
|
|
12819
12626
|
...buildEventBase(command.projectId, command.id),
|
|
@@ -12836,9 +12643,22 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
|
|
|
12836
12643
|
// HTTP proxy request - fetch from local dev server and return response
|
|
12837
12644
|
try {
|
|
12838
12645
|
const { requestId, method, path: reqPath, headers, body, port } = command.payload;
|
|
12839
|
-
|
|
12646
|
+
// SSRF guard: only proxy to the dev server this runner started for
|
|
12647
|
+
// this project - never to arbitrary localhost ports
|
|
12648
|
+
const devServer = getDevServer(command.projectId);
|
|
12649
|
+
if (!devServer?.port) {
|
|
12650
|
+
throw new Error(`No active dev server for project ${command.projectId}`);
|
|
12651
|
+
}
|
|
12652
|
+
if (port !== undefined && port !== devServer.port) {
|
|
12653
|
+
throw new Error(`Port ${port} does not match project dev server port ${devServer.port}`);
|
|
12654
|
+
}
|
|
12655
|
+
const targetPort = devServer.port;
|
|
12656
|
+
if (typeof reqPath !== 'string' || !reqPath.startsWith('/')) {
|
|
12657
|
+
throw new Error(`Invalid proxy path: ${String(reqPath)}`);
|
|
12658
|
+
}
|
|
12659
|
+
debugLog(`🔀 HTTP proxy request: ${method} ${reqPath} → localhost:${targetPort}`);
|
|
12840
12660
|
// Build the target URL
|
|
12841
|
-
const targetUrl = `http://localhost:${
|
|
12661
|
+
const targetUrl = `http://localhost:${targetPort}${reqPath}`;
|
|
12842
12662
|
// Decode body if present
|
|
12843
12663
|
const requestBody = body ? Buffer.from(body, 'base64') : undefined;
|
|
12844
12664
|
// Fetch from dev server
|
|
@@ -12847,7 +12667,7 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
|
|
|
12847
12667
|
headers: {
|
|
12848
12668
|
...headers,
|
|
12849
12669
|
// Remove hop-by-hop headers
|
|
12850
|
-
'host': `localhost:${
|
|
12670
|
+
'host': `localhost:${targetPort}`,
|
|
12851
12671
|
},
|
|
12852
12672
|
body: requestBody,
|
|
12853
12673
|
// Don't follow redirects - let the client handle them
|
|
@@ -12917,8 +12737,33 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
|
|
|
12917
12737
|
}
|
|
12918
12738
|
case "hmr-connect": {
|
|
12919
12739
|
// Connect to local HMR WebSocket server
|
|
12740
|
+
// SSRF guard: only connect to the dev server this runner started for
|
|
12741
|
+
// this project - never to an arbitrary server-supplied port
|
|
12920
12742
|
const { connectionId, port, protocol } = command.payload;
|
|
12921
|
-
|
|
12743
|
+
const hmrDevServer = getDevServer(command.projectId);
|
|
12744
|
+
// Surface a hmr-error (not a silent break) so the frontend's pending
|
|
12745
|
+
// connection fails fast instead of hanging until its timeout.
|
|
12746
|
+
if (!hmrDevServer?.port) {
|
|
12747
|
+
console.warn(`[runner] ⚠️ hmr-connect rejected: no active dev server for project ${command.projectId}`);
|
|
12748
|
+
sendEvent({
|
|
12749
|
+
type: "hmr-error",
|
|
12750
|
+
...buildEventBase(command.projectId, command.id),
|
|
12751
|
+
connectionId,
|
|
12752
|
+
error: "No active dev server for project",
|
|
12753
|
+
});
|
|
12754
|
+
break;
|
|
12755
|
+
}
|
|
12756
|
+
if (port !== undefined && port !== hmrDevServer.port) {
|
|
12757
|
+
console.warn(`[runner] ⚠️ hmr-connect rejected: port ${port} does not match dev server port ${hmrDevServer.port}`);
|
|
12758
|
+
sendEvent({
|
|
12759
|
+
type: "hmr-error",
|
|
12760
|
+
...buildEventBase(command.projectId, command.id),
|
|
12761
|
+
connectionId,
|
|
12762
|
+
error: "Requested port does not match the project's dev server",
|
|
12763
|
+
});
|
|
12764
|
+
break;
|
|
12765
|
+
}
|
|
12766
|
+
hmrProxyManager.connect(connectionId, hmrDevServer.port, command.projectId, protocol);
|
|
12922
12767
|
break;
|
|
12923
12768
|
}
|
|
12924
12769
|
case "hmr-message": {
|
|
@@ -13115,15 +12960,6 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
|
|
|
13115
12960
|
lastCommandReceived = Date.now(); // Reset command timer on new connection
|
|
13116
12961
|
// Update logger connection status
|
|
13117
12962
|
logger.setConnected(true);
|
|
13118
|
-
Sentry.logger.info('Runner connected to server', {
|
|
13119
|
-
runnerId: RUNNER_ID,
|
|
13120
|
-
version: getPackageVersion(),
|
|
13121
|
-
user: os$1.userInfo().username,
|
|
13122
|
-
hostname: os$1.hostname(),
|
|
13123
|
-
platform: os$1.platform(),
|
|
13124
|
-
serverUrl: WS_URL,
|
|
13125
|
-
workspace: WORKSPACE_ROOT,
|
|
13126
|
-
});
|
|
13127
12963
|
debugLog("Health check: ping/pong enabled, command timeout: 5 minutes");
|
|
13128
12964
|
publishStatus();
|
|
13129
12965
|
scheduleHeartbeat();
|
|
@@ -13164,107 +13000,10 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
|
|
|
13164
13000
|
const command = JSON.parse(String(data));
|
|
13165
13001
|
// BUG FIX: Update lastCommandReceived timestamp
|
|
13166
13002
|
lastCommandReceived = Date.now();
|
|
13167
|
-
|
|
13168
|
-
const projectIdForTelemetry = command.type === 'analyze-project'
|
|
13169
|
-
? 'pending-analysis'
|
|
13170
|
-
: command.projectId;
|
|
13171
|
-
// Continue trace from frontend - each build now starts its trace in the frontend
|
|
13172
|
-
// This creates a span within the continued trace for the runner's work
|
|
13173
|
-
if (command._sentry?.trace) {
|
|
13174
|
-
console.log("[runner] Continuing trace from frontend:", command._sentry.trace.substring(0, 50));
|
|
13175
|
-
await Sentry.continueTrace({
|
|
13176
|
-
sentryTrace: command._sentry.trace,
|
|
13177
|
-
baggage: command._sentry.baggage,
|
|
13178
|
-
}, async () => {
|
|
13179
|
-
// Create a span for this command execution within the continued trace
|
|
13180
|
-
await Sentry.startSpan({
|
|
13181
|
-
name: `runner.${command.type}`,
|
|
13182
|
-
op: command.type === 'start-build' ? 'build.runner' : `runner.${command.type}`,
|
|
13183
|
-
attributes: {
|
|
13184
|
-
'command.type': command.type,
|
|
13185
|
-
'command.id': command.id,
|
|
13186
|
-
'project.id': projectIdForTelemetry,
|
|
13187
|
-
'trace.continued': true,
|
|
13188
|
-
},
|
|
13189
|
-
}, async (span) => {
|
|
13190
|
-
try {
|
|
13191
|
-
Sentry.setTag("command_type", command.type);
|
|
13192
|
-
Sentry.setTag("project_id", projectIdForTelemetry);
|
|
13193
|
-
Sentry.setTag("command_id", command.id);
|
|
13194
|
-
// Capture build metrics for start-build commands
|
|
13195
|
-
if (command.type === 'start-build' && command.payload) {
|
|
13196
|
-
const agent = command.payload.agent ?? 'claude-code';
|
|
13197
|
-
const claudeModel = agent === 'claude-code' &&
|
|
13198
|
-
(command.payload.claudeModel === 'claude-haiku-4-5' ||
|
|
13199
|
-
command.payload.claudeModel === 'claude-sonnet-4-6' ||
|
|
13200
|
-
command.payload.claudeModel === 'claude-opus-4-6')
|
|
13201
|
-
? command.payload.claudeModel
|
|
13202
|
-
: 'claude-sonnet-4-6';
|
|
13203
|
-
Sentry.metrics.count('runner.build.started', 1, {
|
|
13204
|
-
attributes: {
|
|
13205
|
-
project_id: command.projectId,
|
|
13206
|
-
model: agent === 'claude-code' ? claudeModel : agent,
|
|
13207
|
-
framework: command.payload.template?.framework || 'unknown',
|
|
13208
|
-
operation_type: command.payload.operationType || 'initial-build',
|
|
13209
|
-
}
|
|
13210
|
-
});
|
|
13211
|
-
}
|
|
13212
|
-
await handleCommand(command);
|
|
13213
|
-
}
|
|
13214
|
-
catch (error) {
|
|
13215
|
-
span.setStatus({ code: 2, message: error instanceof Error ? error.message : 'Command failed' });
|
|
13216
|
-
throw error;
|
|
13217
|
-
}
|
|
13218
|
-
});
|
|
13219
|
-
});
|
|
13220
|
-
}
|
|
13221
|
-
else {
|
|
13222
|
-
console.log("[runner] No trace context - starting isolated span");
|
|
13223
|
-
// Create an isolated span when no trace context is provided
|
|
13224
|
-
await Sentry.startSpan({
|
|
13225
|
-
name: `runner.${command.type}`,
|
|
13226
|
-
op: command.type === 'start-build' ? 'build.runner' : `runner.${command.type}`,
|
|
13227
|
-
attributes: {
|
|
13228
|
-
'command.type': command.type,
|
|
13229
|
-
'command.id': command.id,
|
|
13230
|
-
'project.id': projectIdForTelemetry,
|
|
13231
|
-
'trace.continued': false,
|
|
13232
|
-
},
|
|
13233
|
-
}, async (span) => {
|
|
13234
|
-
try {
|
|
13235
|
-
Sentry.setTag("command_type", command.type);
|
|
13236
|
-
Sentry.setTag("project_id", projectIdForTelemetry);
|
|
13237
|
-
Sentry.setTag("command_id", command.id);
|
|
13238
|
-
// Capture build metrics for start-build commands
|
|
13239
|
-
if (command.type === 'start-build' && command.payload) {
|
|
13240
|
-
const agent = command.payload.agent ?? 'claude-code';
|
|
13241
|
-
const claudeModel = agent === 'claude-code' &&
|
|
13242
|
-
(command.payload.claudeModel === 'claude-haiku-4-5' ||
|
|
13243
|
-
command.payload.claudeModel === 'claude-sonnet-4-6' ||
|
|
13244
|
-
command.payload.claudeModel === 'claude-opus-4-6')
|
|
13245
|
-
? command.payload.claudeModel
|
|
13246
|
-
: 'claude-sonnet-4-6';
|
|
13247
|
-
Sentry.metrics.count('runner.build.started', 1, {
|
|
13248
|
-
attributes: {
|
|
13249
|
-
project_id: command.projectId,
|
|
13250
|
-
model: agent === 'claude-code' ? claudeModel : agent,
|
|
13251
|
-
framework: command.payload.template?.framework || 'unknown',
|
|
13252
|
-
operation_type: command.payload.operationType || 'initial-build',
|
|
13253
|
-
}
|
|
13254
|
-
});
|
|
13255
|
-
}
|
|
13256
|
-
await handleCommand(command);
|
|
13257
|
-
}
|
|
13258
|
-
catch (error) {
|
|
13259
|
-
span.setStatus({ code: 2, message: error instanceof Error ? error.message : 'Command failed' });
|
|
13260
|
-
throw error;
|
|
13261
|
-
}
|
|
13262
|
-
});
|
|
13263
|
-
}
|
|
13003
|
+
await handleCommand(command);
|
|
13264
13004
|
}
|
|
13265
13005
|
catch (error) {
|
|
13266
13006
|
console.error("Failed to parse command", error);
|
|
13267
|
-
Sentry.captureException(error);
|
|
13268
13007
|
sendEvent({
|
|
13269
13008
|
type: "error",
|
|
13270
13009
|
...buildEventBase(undefined, randomUUID$1()),
|
|
@@ -13355,8 +13094,6 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
|
|
|
13355
13094
|
await stopAllDevServers(tunnelManager);
|
|
13356
13095
|
// Final cleanup of any remaining tunnels
|
|
13357
13096
|
await tunnelManager.closeAll();
|
|
13358
|
-
// Flush Sentry events before exiting
|
|
13359
|
-
await Sentry.flush(2000);
|
|
13360
13097
|
log("shutdown complete");
|
|
13361
13098
|
};
|
|
13362
13099
|
process.on("SIGINT", async () => {
|