@pinpatch/core 0.1.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1365 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ArtifactStore: () => ArtifactStore,
34
+ BoundingBoxSchema: () => BoundingBoxSchema,
35
+ CreateTaskRequestSchema: () => CreateTaskRequestSchema,
36
+ CreateTaskResponseSchema: () => CreateTaskResponseSchema,
37
+ DEFAULT_CONFIG: () => DEFAULT_CONFIG,
38
+ ElementDescriptorSchema: () => ElementDescriptorSchema,
39
+ PinStateSchema: () => PinStateSchema,
40
+ PinpatchConfigSchema: () => PinpatchConfigSchema,
41
+ ProviderErrorCodes: () => ProviderErrorCodes,
42
+ ProviderNameSchema: () => ProviderNameSchema,
43
+ ProviderProgressSchema: () => ProviderProgressSchema,
44
+ ProviderProgressStatusSchema: () => ProviderProgressStatusSchema,
45
+ ProviderResultSchema: () => ProviderResultSchema,
46
+ ProviderTerminalStatusSchema: () => ProviderTerminalStatusSchema,
47
+ RuntimeLogEventSchema: () => RuntimeLogEventSchema,
48
+ RuntimeLogLevelSchema: () => RuntimeLogLevelSchema,
49
+ SessionEventSchema: () => SessionEventSchema,
50
+ SessionRecordSchema: () => SessionRecordSchema,
51
+ SseEventSchema: () => SseEventSchema,
52
+ SseHeartbeatEventSchema: () => SseHeartbeatEventSchema,
53
+ SseProgressEventSchema: () => SseProgressEventSchema,
54
+ SseTerminalEventSchema: () => SseTerminalEventSchema,
55
+ SubmitTaskRequestSchema: () => SubmitTaskRequestSchema,
56
+ SubmitTaskResponseSchema: () => SubmitTaskResponseSchema,
57
+ TaskEventBus: () => TaskEventBus,
58
+ TaskPinSchema: () => TaskPinSchema,
59
+ TaskRecordSchema: () => TaskRecordSchema,
60
+ TaskRunner: () => TaskRunner,
61
+ TaskStatusSchema: () => TaskStatusSchema,
62
+ UiChangePacketSchema: () => UiChangePacketSchema,
63
+ ViewportSchema: () => ViewportSchema,
64
+ createBridgeServer: () => createBridgeServer,
65
+ createLogger: () => createLogger,
66
+ ensureConfigFile: () => ensureConfigFile,
67
+ generateSessionId: () => generateSessionId,
68
+ generateTaskId: () => generateTaskId,
69
+ getConfigPath: () => getConfigPath,
70
+ nowIso: () => nowIso,
71
+ readConfigFile: () => readConfigFile,
72
+ resolveConfig: () => resolveConfig
73
+ });
74
+ module.exports = __toCommonJS(index_exports);
75
+
76
+ // src/contracts/artifacts.ts
77
+ var import_zod2 = require("zod");
78
+
79
+ // src/contracts/provider.ts
80
+ var import_zod = require("zod");
81
+ var ProviderNameSchema = import_zod.z.enum(["codex", "claude", "cursor"]);
82
+ var ProviderProgressStatusSchema = import_zod.z.enum([
83
+ "queued",
84
+ "running",
85
+ "completed",
86
+ "error",
87
+ "cancelled",
88
+ "timeout"
89
+ ]);
90
+ var ProviderTerminalStatusSchema = import_zod.z.enum(["completed", "error", "cancelled", "timeout"]);
91
+ var ProviderProgressSchema = import_zod.z.object({
92
+ taskId: import_zod.z.string().min(1),
93
+ sessionId: import_zod.z.string().min(1),
94
+ status: ProviderProgressStatusSchema,
95
+ message: import_zod.z.string().min(1),
96
+ percent: import_zod.z.number().min(0).max(100).optional(),
97
+ timestamp: import_zod.z.string().datetime()
98
+ });
99
+ var ProviderResultSchema = import_zod.z.object({
100
+ taskId: import_zod.z.string().min(1),
101
+ sessionId: import_zod.z.string().min(1),
102
+ status: ProviderTerminalStatusSchema,
103
+ summary: import_zod.z.string().min(1),
104
+ changedFiles: import_zod.z.array(import_zod.z.string()),
105
+ errorCode: import_zod.z.string().optional(),
106
+ errorMessage: import_zod.z.string().optional()
107
+ });
108
+ var ProviderErrorCodes = {
109
+ ProviderUnavailable: "PROVIDER_UNAVAILABLE",
110
+ ProviderNotEnabled: "PROVIDER_NOT_ENABLED",
111
+ ProviderTimeout: "PROVIDER_TIMEOUT",
112
+ ProcessFailed: "PROVIDER_PROCESS_FAILED",
113
+ ValidationFailed: "PROVIDER_VALIDATION_FAILED",
114
+ Unknown: "UNKNOWN"
115
+ };
116
+
117
+ // src/contracts/artifacts.ts
118
+ var PinStateSchema = import_zod2.z.enum([
119
+ "idle",
120
+ "queued",
121
+ "running",
122
+ "completed",
123
+ "error",
124
+ "cancelled",
125
+ "timeout"
126
+ ]);
127
+ var ViewportSchema = import_zod2.z.object({
128
+ width: import_zod2.z.number().int().positive(),
129
+ height: import_zod2.z.number().int().positive()
130
+ });
131
+ var BoundingBoxSchema = import_zod2.z.object({
132
+ x: import_zod2.z.number(),
133
+ y: import_zod2.z.number(),
134
+ width: import_zod2.z.number(),
135
+ height: import_zod2.z.number()
136
+ });
137
+ var ElementDescriptorSchema = import_zod2.z.object({
138
+ tag: import_zod2.z.string().min(1),
139
+ role: import_zod2.z.string().nullable().optional(),
140
+ text: import_zod2.z.string().nullable().optional(),
141
+ attributes: import_zod2.z.record(import_zod2.z.union([import_zod2.z.string(), import_zod2.z.null()])),
142
+ boundingBox: BoundingBoxSchema
143
+ });
144
+ var UiChangePacketSchema = import_zod2.z.object({
145
+ id: import_zod2.z.string().min(1),
146
+ timestamp: import_zod2.z.string().datetime(),
147
+ url: import_zod2.z.string().min(1),
148
+ viewport: ViewportSchema,
149
+ element: ElementDescriptorSchema,
150
+ nearbyText: import_zod2.z.array(import_zod2.z.string()),
151
+ domSnippet: import_zod2.z.string(),
152
+ computedStyleSummary: import_zod2.z.record(import_zod2.z.string()),
153
+ screenshotPath: import_zod2.z.string().min(1),
154
+ userRequest: import_zod2.z.string().min(1)
155
+ });
156
+ var TaskStatusSchema = import_zod2.z.enum([
157
+ "created",
158
+ "queued",
159
+ "running",
160
+ "completed",
161
+ "error",
162
+ "cancelled",
163
+ "timeout"
164
+ ]);
165
+ var TaskPinSchema = import_zod2.z.object({
166
+ x: import_zod2.z.number(),
167
+ y: import_zod2.z.number(),
168
+ body: import_zod2.z.string().min(1)
169
+ });
170
+ var TaskRecordSchema = import_zod2.z.object({
171
+ taskId: import_zod2.z.string().min(1),
172
+ createdAt: import_zod2.z.string().datetime(),
173
+ updatedAt: import_zod2.z.string().datetime(),
174
+ status: TaskStatusSchema,
175
+ url: import_zod2.z.string().min(1),
176
+ viewport: ViewportSchema,
177
+ pin: TaskPinSchema,
178
+ uiChangePacket: UiChangePacketSchema,
179
+ screenshotPath: import_zod2.z.string().min(1),
180
+ provider: ProviderNameSchema.optional(),
181
+ model: import_zod2.z.string().optional(),
182
+ latestSessionId: import_zod2.z.string().optional(),
183
+ sessions: import_zod2.z.array(import_zod2.z.string()),
184
+ summary: import_zod2.z.string().optional(),
185
+ changedFiles: import_zod2.z.array(import_zod2.z.string()).default([]),
186
+ errorCode: import_zod2.z.string().optional(),
187
+ errorMessage: import_zod2.z.string().optional()
188
+ });
189
+ var SessionEventSchema = import_zod2.z.object({
190
+ status: ProviderProgressStatusSchema,
191
+ message: import_zod2.z.string().min(1),
192
+ percent: import_zod2.z.number().min(0).max(100).optional(),
193
+ timestamp: import_zod2.z.string().datetime()
194
+ });
195
+ var SessionRecordSchema = import_zod2.z.object({
196
+ sessionId: import_zod2.z.string().min(1),
197
+ taskId: import_zod2.z.string().min(1),
198
+ provider: ProviderNameSchema,
199
+ model: import_zod2.z.string().min(1),
200
+ status: ProviderProgressStatusSchema,
201
+ dryRun: import_zod2.z.boolean(),
202
+ startedAt: import_zod2.z.string().datetime(),
203
+ updatedAt: import_zod2.z.string().datetime(),
204
+ endedAt: import_zod2.z.string().datetime().optional(),
205
+ events: import_zod2.z.array(SessionEventSchema),
206
+ summary: import_zod2.z.string().optional(),
207
+ changedFiles: import_zod2.z.array(import_zod2.z.string()).default([]),
208
+ errorCode: import_zod2.z.string().optional(),
209
+ errorMessage: import_zod2.z.string().optional()
210
+ });
211
+ var RuntimeLogLevelSchema = import_zod2.z.enum(["debug", "info", "warn", "error"]);
212
+ var RuntimeLogEventSchema = import_zod2.z.object({
213
+ timestamp: import_zod2.z.string().datetime(),
214
+ level: RuntimeLogLevelSchema,
215
+ component: import_zod2.z.string().min(1),
216
+ taskId: import_zod2.z.string().optional(),
217
+ sessionId: import_zod2.z.string().optional(),
218
+ event: import_zod2.z.string().min(1),
219
+ message: import_zod2.z.string().min(1),
220
+ meta: import_zod2.z.record(import_zod2.z.unknown()).optional()
221
+ });
222
+ var PinpatchConfigSchema = import_zod2.z.object({
223
+ provider: ProviderNameSchema.default("codex"),
224
+ model: import_zod2.z.string().default("gpt-5.3-codex-spark"),
225
+ target: import_zod2.z.number().int().positive().default(3e3),
226
+ debug: import_zod2.z.boolean().default(false),
227
+ bridgePort: import_zod2.z.number().int().positive().default(7331),
228
+ proxyPort: import_zod2.z.number().int().positive().default(3030)
229
+ });
230
+
231
+ // src/contracts/bridge.ts
232
+ var import_zod3 = require("zod");
233
+ var CreateTaskRequestSchema = import_zod3.z.object({
234
+ sessionId: import_zod3.z.string().min(1),
235
+ url: import_zod3.z.string().min(1),
236
+ viewport: ViewportSchema,
237
+ pin: import_zod3.z.object({
238
+ x: import_zod3.z.number(),
239
+ y: import_zod3.z.number(),
240
+ body: import_zod3.z.string().min(1)
241
+ }),
242
+ uiChangePacket: UiChangePacketSchema,
243
+ screenshotPath: import_zod3.z.string().min(1),
244
+ screenshotDataUrl: import_zod3.z.string().startsWith("data:image/").optional(),
245
+ clientTaskId: import_zod3.z.string().min(1).optional()
246
+ });
247
+ var CreateTaskResponseSchema = import_zod3.z.object({
248
+ taskId: import_zod3.z.string(),
249
+ sessionId: import_zod3.z.string(),
250
+ status: import_zod3.z.literal("created"),
251
+ taskPath: import_zod3.z.string(),
252
+ eventsUrl: import_zod3.z.string()
253
+ });
254
+ var SubmitTaskRequestSchema = import_zod3.z.object({
255
+ sessionId: import_zod3.z.string().min(1),
256
+ provider: ProviderNameSchema,
257
+ model: import_zod3.z.string().min(1),
258
+ dryRun: import_zod3.z.boolean().default(false),
259
+ debug: import_zod3.z.boolean().default(false),
260
+ followUpBody: import_zod3.z.string().trim().min(1).optional()
261
+ });
262
+ var SubmitTaskResponseSchema = import_zod3.z.object({
263
+ taskId: import_zod3.z.string().min(1),
264
+ sessionId: import_zod3.z.string().min(1),
265
+ status: import_zod3.z.literal("queued"),
266
+ acceptedAt: import_zod3.z.string().datetime(),
267
+ eventsUrl: import_zod3.z.string().min(1)
268
+ });
269
+ var SseProgressEventSchema = import_zod3.z.object({
270
+ type: import_zod3.z.literal("progress"),
271
+ taskId: import_zod3.z.string().min(1),
272
+ sessionId: import_zod3.z.string().min(1),
273
+ status: import_zod3.z.enum([
274
+ "queued",
275
+ "running",
276
+ "completed",
277
+ "error",
278
+ "cancelled",
279
+ "timeout"
280
+ ]),
281
+ message: import_zod3.z.string().min(1),
282
+ percent: import_zod3.z.number().min(0).max(100).optional(),
283
+ timestamp: import_zod3.z.string().datetime()
284
+ });
285
+ var SseTerminalEventSchema = import_zod3.z.object({
286
+ type: import_zod3.z.literal("terminal"),
287
+ taskId: import_zod3.z.string().min(1),
288
+ sessionId: import_zod3.z.string().min(1),
289
+ status: import_zod3.z.enum(["completed", "error", "cancelled", "timeout"]),
290
+ summary: import_zod3.z.string().min(1),
291
+ changedFiles: import_zod3.z.array(import_zod3.z.string()),
292
+ errorCode: import_zod3.z.string().optional(),
293
+ errorMessage: import_zod3.z.string().optional(),
294
+ timestamp: import_zod3.z.string().datetime()
295
+ });
296
+ var SseHeartbeatEventSchema = import_zod3.z.object({
297
+ type: import_zod3.z.literal("heartbeat"),
298
+ timestamp: import_zod3.z.string().datetime()
299
+ });
300
+ var SseEventSchema = import_zod3.z.union([
301
+ SseProgressEventSchema,
302
+ SseTerminalEventSchema,
303
+ SseHeartbeatEventSchema
304
+ ]);
305
+
306
+ // src/config.ts
307
+ var import_node_path = __toESM(require("path"), 1);
308
+ var import_node_fs = require("fs");
309
+ var DEFAULT_CONFIG = {
310
+ provider: "codex",
311
+ model: "gpt-5.3-codex-spark",
312
+ target: 3e3,
313
+ debug: false,
314
+ bridgePort: 7331,
315
+ proxyPort: 3030
316
+ };
317
+ var DEFAULT_CLAUDE_MODEL = "sonnet";
318
+ var resolveConfigPath = (cwd) => import_node_path.default.join(cwd, ".pinpatch", "config.json");
319
+ var omitUndefined = (value) => {
320
+ return Object.fromEntries(Object.entries(value).filter(([, fieldValue]) => fieldValue !== void 0));
321
+ };
322
+ var readConfigFile = async (cwd) => {
323
+ const configPath = resolveConfigPath(cwd);
324
+ try {
325
+ const raw = await import_node_fs.promises.readFile(configPath, "utf8");
326
+ const parsed = JSON.parse(raw);
327
+ return PinpatchConfigSchema.partial().parse(parsed);
328
+ } catch (error) {
329
+ if (error.code === "ENOENT") {
330
+ return {};
331
+ }
332
+ return {};
333
+ }
334
+ };
335
+ var resolveConfig = async (cwd, overrides = {}) => {
336
+ const fileConfig = await readConfigFile(cwd);
337
+ const overrideConfig = omitUndefined(overrides);
338
+ const hasCliModelOverride = Object.prototype.hasOwnProperty.call(overrideConfig, "model");
339
+ const hasFileModel = Object.prototype.hasOwnProperty.call(fileConfig, "model");
340
+ const hasFileProvider = Object.prototype.hasOwnProperty.call(fileConfig, "provider");
341
+ const isBaselineFileModel = hasFileModel && hasFileProvider && fileConfig.model === DEFAULT_CONFIG.model && fileConfig.provider === DEFAULT_CONFIG.provider;
342
+ const merged = {
343
+ ...DEFAULT_CONFIG,
344
+ ...fileConfig,
345
+ ...overrideConfig
346
+ };
347
+ const shouldUseClaudeDefault = merged.provider === "claude" && merged.model === DEFAULT_CONFIG.model && !hasCliModelOverride && (!hasFileModel || isBaselineFileModel);
348
+ if (shouldUseClaudeDefault) {
349
+ merged.model = DEFAULT_CLAUDE_MODEL;
350
+ }
351
+ return PinpatchConfigSchema.parse(merged);
352
+ };
353
+ var ensureConfigFile = async (cwd) => {
354
+ const configPath = resolveConfigPath(cwd);
355
+ await import_node_fs.promises.mkdir(import_node_path.default.dirname(configPath), { recursive: true });
356
+ try {
357
+ const raw = await import_node_fs.promises.readFile(configPath, "utf8");
358
+ return PinpatchConfigSchema.parse(JSON.parse(raw));
359
+ } catch {
360
+ await import_node_fs.promises.writeFile(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
361
+ `, "utf8");
362
+ return DEFAULT_CONFIG;
363
+ }
364
+ };
365
+ var getConfigPath = resolveConfigPath;
366
+
367
+ // src/storage/artifact-store.ts
368
+ var import_node_path3 = __toESM(require("path"), 1);
369
+ var import_node_fs3 = require("fs");
370
+
371
+ // src/utils/fs.ts
372
+ var import_node_fs2 = require("fs");
373
+ var import_node_path2 = __toESM(require("path"), 1);
374
+ var import_node_crypto = __toESM(require("crypto"), 1);
375
+ var ensureDir = async (dirPath) => {
376
+ await import_node_fs2.promises.mkdir(dirPath, { recursive: true });
377
+ };
378
+ var writeJsonAtomic = async (filePath, payload) => {
379
+ const dir = import_node_path2.default.dirname(filePath);
380
+ await ensureDir(dir);
381
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.${import_node_crypto.default.randomUUID()}.tmp`;
382
+ await import_node_fs2.promises.writeFile(tempPath, `${JSON.stringify(payload, null, 2)}
383
+ `, "utf8");
384
+ await import_node_fs2.promises.rename(tempPath, filePath);
385
+ };
386
+ var readJsonIfExists = async (filePath) => {
387
+ try {
388
+ const raw = await import_node_fs2.promises.readFile(filePath, "utf8");
389
+ return JSON.parse(raw);
390
+ } catch (error) {
391
+ if (error.code === "ENOENT") {
392
+ return void 0;
393
+ }
394
+ throw error;
395
+ }
396
+ };
397
+ var listJsonFiles = async (dirPath) => {
398
+ try {
399
+ const entries = await import_node_fs2.promises.readdir(dirPath, { withFileTypes: true });
400
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => import_node_path2.default.join(dirPath, entry.name));
401
+ } catch (error) {
402
+ if (error.code === "ENOENT") {
403
+ return [];
404
+ }
405
+ throw error;
406
+ }
407
+ };
408
+
409
+ // src/storage/artifact-store.ts
410
+ var ArtifactStore = class {
411
+ cwd;
412
+ rootDir;
413
+ tasksDir;
414
+ sessionsDir;
415
+ screenshotsDir;
416
+ runtimeDir;
417
+ logsDir;
418
+ configPath;
419
+ constructor(cwd) {
420
+ this.cwd = cwd;
421
+ this.rootDir = import_node_path3.default.join(cwd, ".pinpatch");
422
+ this.tasksDir = import_node_path3.default.join(this.rootDir, "tasks");
423
+ this.sessionsDir = import_node_path3.default.join(this.rootDir, "sessions");
424
+ this.screenshotsDir = import_node_path3.default.join(this.rootDir, "screenshots");
425
+ this.runtimeDir = import_node_path3.default.join(this.rootDir, "runtime");
426
+ this.logsDir = import_node_path3.default.join(this.runtimeDir, "logs");
427
+ this.configPath = import_node_path3.default.join(this.rootDir, "config.json");
428
+ }
429
+ async ensureStructure() {
430
+ await ensureDir(this.tasksDir);
431
+ await ensureDir(this.sessionsDir);
432
+ await ensureDir(this.screenshotsDir);
433
+ await ensureDir(this.logsDir);
434
+ const existingConfig = await readJsonIfExists(this.configPath);
435
+ if (!existingConfig) {
436
+ await writeJsonAtomic(this.configPath, DEFAULT_CONFIG);
437
+ }
438
+ }
439
+ async ensureGitignoreEntry() {
440
+ const gitignorePath = import_node_path3.default.join(this.cwd, ".gitignore");
441
+ try {
442
+ const content = await import_node_fs3.promises.readFile(gitignorePath, "utf8");
443
+ if (!content.includes(".pinpatch/")) {
444
+ await import_node_fs3.promises.appendFile(gitignorePath, "\n.pinpatch/\n", "utf8");
445
+ }
446
+ } catch (error) {
447
+ if (error.code === "ENOENT") {
448
+ await import_node_fs3.promises.writeFile(gitignorePath, ".pinpatch/\n", "utf8");
449
+ return;
450
+ }
451
+ throw error;
452
+ }
453
+ }
454
+ getTaskPath(taskId) {
455
+ return import_node_path3.default.join(this.tasksDir, `${taskId}.json`);
456
+ }
457
+ getSessionPath(sessionId) {
458
+ return import_node_path3.default.join(this.sessionsDir, `${sessionId}.json`);
459
+ }
460
+ getRelativePath(absolutePath) {
461
+ return import_node_path3.default.relative(this.cwd, absolutePath);
462
+ }
463
+ async readConfig() {
464
+ const data = await readJsonIfExists(this.configPath);
465
+ return PinpatchConfigSchema.parse({ ...DEFAULT_CONFIG, ...data });
466
+ }
467
+ async writeConfig(config) {
468
+ const validated = PinpatchConfigSchema.parse(config);
469
+ await writeJsonAtomic(this.configPath, validated);
470
+ }
471
+ async createTask(task) {
472
+ const validated = TaskRecordSchema.parse(task);
473
+ await writeJsonAtomic(this.getTaskPath(validated.taskId), validated);
474
+ return validated;
475
+ }
476
+ async getTask(taskId) {
477
+ const raw = await readJsonIfExists(this.getTaskPath(taskId));
478
+ if (!raw) {
479
+ return void 0;
480
+ }
481
+ return TaskRecordSchema.parse(raw);
482
+ }
483
+ async updateTask(taskId, updater) {
484
+ const current = await this.getTask(taskId);
485
+ if (!current) {
486
+ throw new Error(`Task ${taskId} does not exist`);
487
+ }
488
+ const updated = TaskRecordSchema.parse(updater(current));
489
+ await writeJsonAtomic(this.getTaskPath(taskId), updated);
490
+ return updated;
491
+ }
492
+ async listTasks() {
493
+ const files = await listJsonFiles(this.tasksDir);
494
+ const rows = await Promise.all(
495
+ files.map(async (filePath) => {
496
+ const raw = await readJsonIfExists(filePath);
497
+ return raw ? TaskRecordSchema.parse(raw) : void 0;
498
+ })
499
+ );
500
+ return rows.filter((row) => row !== void 0);
501
+ }
502
+ async createSession(session) {
503
+ const validated = SessionRecordSchema.parse(session);
504
+ await writeJsonAtomic(this.getSessionPath(validated.sessionId), validated);
505
+ return validated;
506
+ }
507
+ async getSession(sessionId) {
508
+ const raw = await readJsonIfExists(this.getSessionPath(sessionId));
509
+ if (!raw) {
510
+ return void 0;
511
+ }
512
+ return SessionRecordSchema.parse(raw);
513
+ }
514
+ async updateSession(sessionId, updater) {
515
+ const current = await this.getSession(sessionId);
516
+ if (!current) {
517
+ throw new Error(`Session ${sessionId} does not exist`);
518
+ }
519
+ const updated = SessionRecordSchema.parse(updater(current));
520
+ await writeJsonAtomic(this.getSessionPath(sessionId), updated);
521
+ return updated;
522
+ }
523
+ async listSessions() {
524
+ const files = await listJsonFiles(this.sessionsDir);
525
+ const rows = await Promise.all(
526
+ files.map(async (filePath) => {
527
+ const raw = await readJsonIfExists(filePath);
528
+ return raw ? SessionRecordSchema.parse(raw) : void 0;
529
+ })
530
+ );
531
+ return rows.filter((row) => row !== void 0);
532
+ }
533
+ async writeScreenshot(taskId, screenshotDataUrl) {
534
+ const matches = screenshotDataUrl.match(/^data:image\/(png|jpeg|jpg);base64,(?<bytes>.+)$/);
535
+ if (!matches?.groups?.bytes) {
536
+ throw new Error("Invalid screenshot payload");
537
+ }
538
+ const filePath = import_node_path3.default.join(this.screenshotsDir, `${taskId}.png`);
539
+ const buffer = Buffer.from(matches.groups.bytes, "base64");
540
+ await import_node_fs3.promises.writeFile(filePath, buffer);
541
+ return this.getRelativePath(filePath);
542
+ }
543
+ async appendLog(logPath, event) {
544
+ const validated = RuntimeLogEventSchema.parse(event);
545
+ await import_node_fs3.promises.appendFile(logPath, `${JSON.stringify(validated)}
546
+ `, "utf8");
547
+ }
548
+ async prune(options) {
549
+ const logsOlderThanDays = options?.logsOlderThanDays ?? 14;
550
+ const orphanSessionAgeHours = options?.orphanSessionAgeHours ?? 24;
551
+ const logFiles = await import_node_fs3.promises.readdir(this.logsDir).catch(() => []);
552
+ const cutoffLogs = Date.now() - logsOlderThanDays * 24 * 60 * 60 * 1e3;
553
+ let removedLogs = 0;
554
+ for (const file of logFiles) {
555
+ const fullPath = import_node_path3.default.join(this.logsDir, file);
556
+ const stats = await import_node_fs3.promises.stat(fullPath).catch(() => void 0);
557
+ if (!stats) {
558
+ continue;
559
+ }
560
+ if (stats.mtimeMs < cutoffLogs) {
561
+ await import_node_fs3.promises.unlink(fullPath);
562
+ removedLogs += 1;
563
+ }
564
+ }
565
+ const tasks = await this.listTasks();
566
+ const taskIds = new Set(tasks.map((task) => task.taskId));
567
+ const sessions = await this.listSessions();
568
+ const cutoffSessions = Date.now() - orphanSessionAgeHours * 60 * 60 * 1e3;
569
+ let removedSessions = 0;
570
+ for (const session of sessions) {
571
+ const updatedAtMs = Date.parse(session.updatedAt);
572
+ const isOrphan = !taskIds.has(session.taskId);
573
+ if (isOrphan && updatedAtMs < cutoffSessions) {
574
+ await import_node_fs3.promises.unlink(this.getSessionPath(session.sessionId));
575
+ removedSessions += 1;
576
+ }
577
+ }
578
+ return {
579
+ removedLogs,
580
+ removedSessions
581
+ };
582
+ }
583
+ };
584
+
585
+ // src/runtime/ids.ts
586
+ var import_node_crypto2 = __toESM(require("crypto"), 1);
587
+ var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
588
+ var generateSessionId = () => import_node_crypto2.default.randomUUID();
589
+ var generateTaskId = () => {
590
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
591
+ const suffix = import_node_crypto2.default.randomBytes(3).toString("hex");
592
+ return `${date}-${suffix}`;
593
+ };
594
+
595
+ // src/runtime/task-runner.ts
596
+ var toMillis = (timestamp) => {
597
+ const value = Date.parse(timestamp);
598
+ return Number.isNaN(value) ? Date.now() : value;
599
+ };
600
+ var forceMonotonicTimestamp = (nextTs, lastTs) => {
601
+ const parsed = toMillis(nextTs);
602
+ if (parsed > lastTs) {
603
+ return { timestamp: new Date(parsed).toISOString(), millis: parsed };
604
+ }
605
+ const shifted = lastTs + 1;
606
+ return { timestamp: new Date(shifted).toISOString(), millis: shifted };
607
+ };
608
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
609
+ "completed",
610
+ "error",
611
+ "cancelled",
612
+ "timeout"
613
+ ]);
614
+ var isTerminalStatus = (status) => TERMINAL_STATUSES.has(status);
615
+ var TaskRunner = class {
616
+ cwd;
617
+ store;
618
+ logger;
619
+ eventBus;
620
+ getProviderAdapter;
621
+ defaultTimeoutMs;
622
+ dryRunTimeoutMs;
623
+ inFlight = /* @__PURE__ */ new Map();
624
+ constructor(options) {
625
+ this.cwd = options.cwd;
626
+ this.store = options.store;
627
+ this.logger = options.logger;
628
+ this.eventBus = options.eventBus;
629
+ this.getProviderAdapter = options.getProviderAdapter;
630
+ this.defaultTimeoutMs = options.defaultTimeoutMs ?? 10 * 60 * 1e3;
631
+ this.dryRunTimeoutMs = options.dryRunTimeoutMs ?? 2 * 60 * 1e3;
632
+ }
633
+ key(taskId, sessionId) {
634
+ return `${taskId}:${sessionId}`;
635
+ }
636
+ async cancelTask(taskId, sessionId) {
637
+ const key = this.key(taskId, sessionId);
638
+ const entry = this.inFlight.get(key);
639
+ if (!entry) {
640
+ return;
641
+ }
642
+ await entry.adapter.cancelTask(taskId, sessionId);
643
+ this.logger.info("Cancel request forwarded to provider", {
644
+ component: "runner",
645
+ taskId,
646
+ sessionId,
647
+ event: "task.cancel"
648
+ });
649
+ }
650
+ async runTask(input) {
651
+ const { taskId, sessionId, provider, model, dryRun, debug } = input;
652
+ const task = await this.store.getTask(taskId);
653
+ if (!task) {
654
+ throw new Error(`Task ${taskId} does not exist`);
655
+ }
656
+ const queuedTimestamp = nowIso();
657
+ await this.store.updateTask(taskId, (current) => ({
658
+ ...current,
659
+ status: "queued",
660
+ provider,
661
+ model,
662
+ latestSessionId: sessionId,
663
+ sessions: Array.from(/* @__PURE__ */ new Set([...current.sessions, sessionId])),
664
+ updatedAt: queuedTimestamp
665
+ }));
666
+ await this.store.createSession({
667
+ sessionId,
668
+ taskId,
669
+ provider,
670
+ model,
671
+ status: "queued",
672
+ dryRun,
673
+ startedAt: queuedTimestamp,
674
+ updatedAt: queuedTimestamp,
675
+ events: [
676
+ {
677
+ status: "queued",
678
+ message: "Task queued",
679
+ timestamp: queuedTimestamp
680
+ }
681
+ ],
682
+ changedFiles: []
683
+ });
684
+ const adapter = this.getProviderAdapter(provider);
685
+ if (!adapter) {
686
+ const unavailable = {
687
+ taskId,
688
+ sessionId,
689
+ status: "error",
690
+ summary: `Provider ${provider} is not available in this runtime`,
691
+ changedFiles: [],
692
+ errorCode: ProviderErrorCodes.ProviderUnavailable,
693
+ errorMessage: `Provider ${provider} was not registered`
694
+ };
695
+ await this.persistTerminalState(
696
+ task,
697
+ unavailable,
698
+ provider,
699
+ model,
700
+ dryRun
701
+ );
702
+ return unavailable;
703
+ }
704
+ const queuedEvent = {
705
+ type: "progress",
706
+ taskId,
707
+ sessionId,
708
+ status: "queued",
709
+ message: "Task queued",
710
+ timestamp: queuedTimestamp
711
+ };
712
+ this.eventBus.publish(taskId, sessionId, queuedEvent);
713
+ const key = this.key(taskId, sessionId);
714
+ this.inFlight.set(key, {
715
+ adapter,
716
+ startedAt: Date.now()
717
+ });
718
+ let lastProgressTime = toMillis(queuedTimestamp);
719
+ const timeoutMs = dryRun ? this.dryRunTimeoutMs : this.defaultTimeoutMs;
720
+ let terminalCommitted = false;
721
+ const handleProgress = async (event) => {
722
+ if (terminalCommitted) {
723
+ return;
724
+ }
725
+ const monotonic = forceMonotonicTimestamp(
726
+ event.timestamp,
727
+ lastProgressTime
728
+ );
729
+ lastProgressTime = monotonic.millis;
730
+ const normalizedEvent = {
731
+ ...event,
732
+ timestamp: monotonic.timestamp
733
+ };
734
+ await this.store.updateSession(sessionId, (session) => {
735
+ if (isTerminalStatus(session.status)) {
736
+ return session;
737
+ }
738
+ return {
739
+ ...session,
740
+ status: normalizedEvent.status,
741
+ updatedAt: normalizedEvent.timestamp,
742
+ events: [
743
+ ...session.events,
744
+ {
745
+ status: normalizedEvent.status,
746
+ message: normalizedEvent.message,
747
+ percent: normalizedEvent.percent,
748
+ timestamp: normalizedEvent.timestamp
749
+ }
750
+ ]
751
+ };
752
+ });
753
+ if (terminalCommitted) {
754
+ return;
755
+ }
756
+ if (normalizedEvent.status === "running" || normalizedEvent.status === "queued") {
757
+ await this.store.updateTask(taskId, (current) => {
758
+ if (isTerminalStatus(current.status)) {
759
+ return current;
760
+ }
761
+ return {
762
+ ...current,
763
+ status: normalizedEvent.status,
764
+ updatedAt: normalizedEvent.timestamp
765
+ };
766
+ });
767
+ }
768
+ if (terminalCommitted) {
769
+ return;
770
+ }
771
+ this.eventBus.publish(taskId, sessionId, {
772
+ type: "progress",
773
+ taskId,
774
+ sessionId,
775
+ status: normalizedEvent.status,
776
+ message: normalizedEvent.message,
777
+ percent: normalizedEvent.percent,
778
+ timestamp: normalizedEvent.timestamp
779
+ });
780
+ this.logger.debug(normalizedEvent.message, {
781
+ component: "runner",
782
+ taskId,
783
+ sessionId,
784
+ event: "task.progress",
785
+ meta: {
786
+ status: normalizedEvent.status,
787
+ percent: normalizedEvent.percent
788
+ }
789
+ });
790
+ };
791
+ try {
792
+ const providerTaskPromise = adapter.submitTask(
793
+ {
794
+ taskId,
795
+ sessionId,
796
+ task,
797
+ prompt: this.buildPrompt(task),
798
+ model,
799
+ dryRun,
800
+ debug,
801
+ cwd: this.cwd,
802
+ timeoutMs
803
+ },
804
+ (event) => {
805
+ void handleProgress(event);
806
+ }
807
+ );
808
+ let timeoutHandle;
809
+ const timeoutPromise = new Promise((resolve) => {
810
+ timeoutHandle = setTimeout(() => {
811
+ resolve({
812
+ taskId,
813
+ sessionId,
814
+ status: "timeout",
815
+ summary: `Task timed out after ${timeoutMs}ms`,
816
+ changedFiles: [],
817
+ errorCode: ProviderErrorCodes.ProviderTimeout,
818
+ errorMessage: `Provider timed out after ${timeoutMs}ms`
819
+ });
820
+ }, timeoutMs);
821
+ });
822
+ const result = await Promise.race([providerTaskPromise, timeoutPromise]);
823
+ const normalizedResult = {
824
+ ...result,
825
+ taskId,
826
+ sessionId
827
+ };
828
+ terminalCommitted = true;
829
+ if (timeoutHandle) {
830
+ clearTimeout(timeoutHandle);
831
+ }
832
+ await this.persistTerminalState(
833
+ task,
834
+ normalizedResult,
835
+ provider,
836
+ model,
837
+ dryRun
838
+ );
839
+ this.inFlight.delete(key);
840
+ return normalizedResult;
841
+ } catch (error) {
842
+ const message = error instanceof Error ? error.message : "Unknown provider error";
843
+ const failedResult = {
844
+ taskId,
845
+ sessionId,
846
+ status: "error",
847
+ summary: "Provider execution failed",
848
+ changedFiles: [],
849
+ errorCode: ProviderErrorCodes.ProcessFailed,
850
+ errorMessage: message
851
+ };
852
+ terminalCommitted = true;
853
+ await this.persistTerminalState(
854
+ task,
855
+ failedResult,
856
+ provider,
857
+ model,
858
+ dryRun
859
+ );
860
+ this.inFlight.delete(key);
861
+ return failedResult;
862
+ }
863
+ }
864
+ buildPrompt(task) {
865
+ const selectedAttributes = JSON.stringify(
866
+ task.uiChangePacket.element.attributes
867
+ );
868
+ return [
869
+ "You are implementing a UI change request from Pinpatch.",
870
+ "Scope guardrails (must follow):",
871
+ "- Implement only the requested UI change.",
872
+ "- Treat the selected element as the primary edit target by default.",
873
+ "- Do not move the change to ancestors, siblings, or page/global wrappers unless the request explicitly asks for a page-level or app-wide change or the selected element cannot satisfy the request without broader changes.",
874
+ "- If the request is ambiguous, prefer changing the selected element directly.",
875
+ "- Do not edit, reformat, or reorganize unrelated files.",
876
+ "- Never revert, overwrite, or clean up unrelated repo changes (other agents may be editing in parallel).",
877
+ "- If unrelated files are modified or dirty, leave them untouched.",
878
+ "- If you cannot complete the request without touching unrelated areas, stop and report the exact blocker.",
879
+ `User request: ${task.pin.body}`,
880
+ `Page URL: ${task.url}`,
881
+ `Element: <${task.uiChangePacket.element.tag}> text="${task.uiChangePacket.element.text ?? ""}"`,
882
+ `Element attributes: ${selectedAttributes}`,
883
+ `Bounding box: ${JSON.stringify(task.uiChangePacket.element.boundingBox)}`,
884
+ `Nearby text: ${task.uiChangePacket.nearbyText.join(" | ")}`,
885
+ `DOM snippet: ${task.uiChangePacket.domSnippet}`,
886
+ `Computed style summary: ${JSON.stringify(task.uiChangePacket.computedStyleSummary)}`,
887
+ `Screenshot path: ${task.screenshotPath}`,
888
+ "Apply the change in local files.",
889
+ "Output format (must follow):",
890
+ "- For each modified file, print a line exactly as: CHANGED: <path>",
891
+ "- Final line must be exactly one sentence summarizing the changes made."
892
+ ].join("\n");
893
+ }
894
+ async persistTerminalState(task, result, provider, model, dryRun) {
895
+ const finishedAt = nowIso();
896
+ await this.store.updateSession(result.sessionId, (session) => ({
897
+ ...session,
898
+ provider,
899
+ model,
900
+ dryRun,
901
+ status: result.status,
902
+ summary: result.summary,
903
+ changedFiles: result.changedFiles,
904
+ errorCode: result.errorCode,
905
+ errorMessage: result.errorMessage,
906
+ endedAt: finishedAt,
907
+ updatedAt: finishedAt,
908
+ events: [
909
+ ...session.events,
910
+ {
911
+ status: result.status,
912
+ message: result.summary,
913
+ timestamp: finishedAt
914
+ }
915
+ ]
916
+ }));
917
+ await this.store.updateTask(task.taskId, (current) => ({
918
+ ...current,
919
+ status: result.status,
920
+ updatedAt: finishedAt,
921
+ provider,
922
+ model,
923
+ summary: result.summary,
924
+ changedFiles: result.changedFiles,
925
+ errorCode: result.errorCode,
926
+ errorMessage: result.errorMessage
927
+ }));
928
+ this.eventBus.publish(task.taskId, result.sessionId, {
929
+ type: "terminal",
930
+ taskId: task.taskId,
931
+ sessionId: result.sessionId,
932
+ status: result.status,
933
+ summary: result.summary,
934
+ changedFiles: result.changedFiles,
935
+ errorCode: result.errorCode,
936
+ errorMessage: result.errorMessage,
937
+ timestamp: finishedAt
938
+ });
939
+ this.logger.info(result.summary, {
940
+ component: "runner",
941
+ taskId: task.taskId,
942
+ sessionId: result.sessionId,
943
+ event: "task.terminal",
944
+ meta: {
945
+ status: result.status,
946
+ changedFiles: result.changedFiles,
947
+ errorCode: result.errorCode
948
+ }
949
+ });
950
+ }
951
+ };
952
+
953
+ // src/bridge/event-bus.ts
954
+ var import_node_events = require("events");
955
+ var keyFor = (taskId, sessionId) => `${taskId}:${sessionId}`;
956
+ var TaskEventBus = class {
957
+ emitter = new import_node_events.EventEmitter();
958
+ subscribe(taskId, sessionId, listener) {
959
+ const key = keyFor(taskId, sessionId);
960
+ this.emitter.on(key, listener);
961
+ return () => {
962
+ this.emitter.off(key, listener);
963
+ };
964
+ }
965
+ publish(taskId, sessionId, event) {
966
+ this.emitter.emit(keyFor(taskId, sessionId), event);
967
+ }
968
+ };
969
+
970
+ // src/bridge/server.ts
971
+ var import_node_http = require("http");
972
+ var import_node_path4 = __toESM(require("path"), 1);
973
+ var import_node_fs4 = require("fs");
974
+ var import_express = __toESM(require("express"), 1);
975
+ var import_cors = __toESM(require("cors"), 1);
976
+ var sanitizeTaskId = (candidate) => candidate.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 64);
977
+ var serializeSse = (eventName, payload) => {
978
+ return `event: ${eventName}
979
+ data: ${JSON.stringify(payload)}
980
+
981
+ `;
982
+ };
983
+ var fallbackOverlayScript = `
984
+ (function(){
985
+ if (window.__PINPATCH_OVERLAY_FALLBACK__) return;
986
+ window.__PINPATCH_OVERLAY_FALLBACK__ = true;
987
+ console.warn('[pinpatch] overlay bundle is missing. Build apps/overlay first.');
988
+ })();
989
+ `;
990
+ var resolveAvailableTaskId = async (store, initialTaskId) => {
991
+ if (initialTaskId) {
992
+ const candidate = sanitizeTaskId(initialTaskId);
993
+ const existing = await store.getTask(candidate);
994
+ if (!existing) {
995
+ return candidate;
996
+ }
997
+ }
998
+ let tries = 0;
999
+ while (tries < 10) {
1000
+ const candidate = generateTaskId();
1001
+ const existing = await store.getTask(candidate);
1002
+ if (!existing) {
1003
+ return candidate;
1004
+ }
1005
+ tries += 1;
1006
+ }
1007
+ throw new Error("Failed to allocate a unique task id");
1008
+ };
1009
+ var createBridgeServer = (options) => {
1010
+ const app = (0, import_express.default)();
1011
+ const eventBus = new TaskEventBus();
1012
+ const taskRunner = new TaskRunner({
1013
+ cwd: options.cwd,
1014
+ store: options.store,
1015
+ logger: options.logger,
1016
+ eventBus,
1017
+ getProviderAdapter: options.getProviderAdapter
1018
+ });
1019
+ app.use((0, import_cors.default)());
1020
+ app.use(import_express.default.json({ limit: "25mb" }));
1021
+ app.get("/health", (_req, res) => {
1022
+ res.status(200).json({ ok: true });
1023
+ });
1024
+ app.get("/overlay.js", async (_req, res) => {
1025
+ const requestedPath = options.overlayScriptPath;
1026
+ if (requestedPath) {
1027
+ try {
1028
+ const script = await import_node_fs4.promises.readFile(import_node_path4.default.resolve(requestedPath), "utf8");
1029
+ res.setHeader("content-type", "application/javascript; charset=utf-8");
1030
+ res.status(200).send(script);
1031
+ return;
1032
+ } catch {
1033
+ options.logger.warn(
1034
+ "Overlay bundle not found, serving fallback overlay script",
1035
+ {
1036
+ component: "bridge",
1037
+ event: "overlay.fallback",
1038
+ meta: {
1039
+ overlayScriptPath: requestedPath
1040
+ }
1041
+ }
1042
+ );
1043
+ }
1044
+ }
1045
+ res.setHeader("content-type", "application/javascript; charset=utf-8");
1046
+ res.status(200).send(fallbackOverlayScript);
1047
+ });
1048
+ app.post("/api/tasks", async (req, res) => {
1049
+ const parsed = CreateTaskRequestSchema.safeParse(req.body);
1050
+ if (!parsed.success) {
1051
+ res.status(400).json({
1052
+ error: "Invalid request",
1053
+ details: parsed.error.flatten()
1054
+ });
1055
+ return;
1056
+ }
1057
+ const payload = parsed.data;
1058
+ const taskId = await resolveAvailableTaskId(
1059
+ options.store,
1060
+ payload.clientTaskId
1061
+ );
1062
+ let screenshotPath = payload.screenshotPath;
1063
+ if (payload.screenshotDataUrl) {
1064
+ screenshotPath = await options.store.writeScreenshot(
1065
+ taskId,
1066
+ payload.screenshotDataUrl
1067
+ );
1068
+ }
1069
+ const createdAt = nowIso();
1070
+ const taskRecord = {
1071
+ taskId,
1072
+ createdAt,
1073
+ updatedAt: createdAt,
1074
+ status: "created",
1075
+ url: payload.url,
1076
+ viewport: payload.viewport,
1077
+ pin: payload.pin,
1078
+ uiChangePacket: {
1079
+ ...payload.uiChangePacket,
1080
+ screenshotPath,
1081
+ userRequest: payload.pin.body
1082
+ },
1083
+ screenshotPath,
1084
+ sessions: [payload.sessionId],
1085
+ latestSessionId: payload.sessionId,
1086
+ changedFiles: []
1087
+ };
1088
+ await options.store.createTask(taskRecord);
1089
+ options.logger.info("Task created", {
1090
+ component: "bridge",
1091
+ taskId,
1092
+ sessionId: payload.sessionId,
1093
+ event: "task.created",
1094
+ meta: {
1095
+ screenshotPath
1096
+ }
1097
+ });
1098
+ res.status(201).json({
1099
+ taskId,
1100
+ sessionId: payload.sessionId,
1101
+ status: "created",
1102
+ taskPath: `.pinpatch/tasks/${taskId}.json`,
1103
+ eventsUrl: `/api/tasks/${taskId}/events?sessionId=${payload.sessionId}`
1104
+ });
1105
+ });
1106
+ app.post("/api/tasks/:taskId/submit", async (req, res) => {
1107
+ const taskId = String(req.params.taskId ?? "");
1108
+ if (!taskId) {
1109
+ res.status(400).json({ error: "taskId is required" });
1110
+ return;
1111
+ }
1112
+ const task = await options.store.getTask(taskId);
1113
+ if (!task) {
1114
+ res.status(404).json({ error: "Task not found" });
1115
+ return;
1116
+ }
1117
+ const parsed = SubmitTaskRequestSchema.safeParse(req.body);
1118
+ if (!parsed.success) {
1119
+ res.status(400).json({
1120
+ error: "Invalid request",
1121
+ details: parsed.error.flatten()
1122
+ });
1123
+ return;
1124
+ }
1125
+ const payload = parsed.data;
1126
+ if (payload.followUpBody) {
1127
+ const updatedAt = nowIso();
1128
+ const followUpBody = payload.followUpBody;
1129
+ await options.store.updateTask(taskId, (current) => ({
1130
+ ...current,
1131
+ updatedAt,
1132
+ pin: {
1133
+ ...current.pin,
1134
+ body: followUpBody
1135
+ },
1136
+ uiChangePacket: {
1137
+ ...current.uiChangePacket,
1138
+ userRequest: followUpBody
1139
+ }
1140
+ }));
1141
+ }
1142
+ void taskRunner.runTask({
1143
+ taskId,
1144
+ sessionId: payload.sessionId,
1145
+ provider: payload.provider,
1146
+ model: payload.model,
1147
+ dryRun: payload.dryRun,
1148
+ debug: payload.debug
1149
+ }).catch((error) => {
1150
+ options.logger.error("Provider task execution failed", {
1151
+ component: "bridge",
1152
+ taskId,
1153
+ sessionId: payload.sessionId,
1154
+ event: "task.run.error",
1155
+ meta: {
1156
+ error: error instanceof Error ? error.message : String(error)
1157
+ }
1158
+ });
1159
+ });
1160
+ const acceptedAt = nowIso();
1161
+ res.status(202).json({
1162
+ taskId,
1163
+ sessionId: payload.sessionId,
1164
+ status: "queued",
1165
+ acceptedAt,
1166
+ eventsUrl: `/api/tasks/${taskId}/events?sessionId=${payload.sessionId}`
1167
+ });
1168
+ });
1169
+ app.post("/api/tasks/:taskId/cancel", async (req, res) => {
1170
+ const taskId = String(req.params.taskId ?? "");
1171
+ const sessionId = String(req.body?.sessionId ?? "");
1172
+ if (!taskId || !sessionId) {
1173
+ res.status(400).json({ error: "taskId and sessionId are required" });
1174
+ return;
1175
+ }
1176
+ await taskRunner.cancelTask(taskId, sessionId);
1177
+ res.status(202).json({ taskId, sessionId, status: "cancelled" });
1178
+ });
1179
+ app.get("/api/tasks/:taskId/events", async (req, res) => {
1180
+ const taskId = String(req.params.taskId ?? "");
1181
+ const sessionId = String(req.query.sessionId ?? "");
1182
+ if (!taskId || !sessionId) {
1183
+ res.status(400).json({ error: "taskId and sessionId query params are required" });
1184
+ return;
1185
+ }
1186
+ res.setHeader("Content-Type", "text/event-stream");
1187
+ res.setHeader("Cache-Control", "no-cache");
1188
+ res.setHeader("Connection", "keep-alive");
1189
+ res.flushHeaders();
1190
+ const push = (event) => {
1191
+ const type = event.type === "terminal" ? "terminal" : event.type === "heartbeat" ? "heartbeat" : "progress";
1192
+ res.write(serializeSse(type, event));
1193
+ };
1194
+ push({
1195
+ type: "heartbeat",
1196
+ timestamp: nowIso()
1197
+ });
1198
+ const unsubscribe = eventBus.subscribe(taskId, sessionId, push);
1199
+ const heartbeat = setInterval(() => {
1200
+ push({
1201
+ type: "heartbeat",
1202
+ timestamp: nowIso()
1203
+ });
1204
+ }, 15e3);
1205
+ req.on("close", () => {
1206
+ clearInterval(heartbeat);
1207
+ unsubscribe();
1208
+ res.end();
1209
+ });
1210
+ });
1211
+ const server = (0, import_node_http.createServer)(app);
1212
+ return {
1213
+ app,
1214
+ server,
1215
+ eventBus,
1216
+ taskRunner,
1217
+ async start() {
1218
+ await new Promise((resolve, reject) => {
1219
+ server.once("error", reject);
1220
+ server.listen(options.port, () => {
1221
+ server.off("error", reject);
1222
+ resolve();
1223
+ });
1224
+ });
1225
+ options.logger.info(
1226
+ `Bridge listening on http://localhost:${options.port}`,
1227
+ {
1228
+ component: "bridge",
1229
+ event: "bridge.started"
1230
+ }
1231
+ );
1232
+ },
1233
+ async stop() {
1234
+ await new Promise((resolve) => {
1235
+ server.close(() => resolve());
1236
+ });
1237
+ options.logger.info("Bridge stopped", {
1238
+ component: "bridge",
1239
+ event: "bridge.stopped"
1240
+ });
1241
+ }
1242
+ };
1243
+ };
1244
+
1245
+ // src/logging/logger.ts
1246
+ var import_node_os = __toESM(require("os"), 1);
1247
+ var import_node_path5 = __toESM(require("path"), 1);
1248
+ var import_node_fs5 = require("fs");
1249
+ var redactValue = (value) => {
1250
+ if (typeof value === "string") {
1251
+ const home = import_node_os.default.homedir();
1252
+ return value.replaceAll(home, "~").replace(/(Bearer\s+)[A-Za-z0-9._-]+/gi, "$1[REDACTED]").replace(/(token=)[^\s&]+/gi, "$1[REDACTED]");
1253
+ }
1254
+ if (Array.isArray(value)) {
1255
+ return value.map((entry) => redactValue(entry));
1256
+ }
1257
+ if (value && typeof value === "object") {
1258
+ const output = {};
1259
+ for (const [key, fieldValue] of Object.entries(value)) {
1260
+ if (["token", "authorization", "auth", "apiKey", "apikey"].includes(key.toLowerCase())) {
1261
+ output[key] = "[REDACTED]";
1262
+ } else {
1263
+ output[key] = redactValue(fieldValue);
1264
+ }
1265
+ }
1266
+ return output;
1267
+ }
1268
+ return value;
1269
+ };
1270
+ var getDateKey = (dateIso) => dateIso.slice(0, 10);
1271
+ var resolveLogPath = (logsDir, timestamp, maxFileSizeBytes) => {
1272
+ const dateKey = getDateKey(timestamp);
1273
+ let index = 0;
1274
+ while (true) {
1275
+ const candidateName = index === 0 ? `${dateKey}.jsonl` : `${dateKey}-${index}.jsonl`;
1276
+ const candidatePath = import_node_path5.default.join(logsDir, candidateName);
1277
+ if (!(0, import_node_fs5.existsSync)(candidatePath)) {
1278
+ return candidatePath;
1279
+ }
1280
+ const size = (0, import_node_fs5.statSync)(candidatePath).size;
1281
+ if (size < maxFileSizeBytes) {
1282
+ return candidatePath;
1283
+ }
1284
+ index += 1;
1285
+ }
1286
+ };
1287
+ var createLogger = (options) => {
1288
+ const { store, debugEnabled, component, maxFileSizeBytes = 2 * 1024 * 1024 } = options;
1289
+ const emit = (level, message, details) => {
1290
+ if (level === "debug" && !debugEnabled) {
1291
+ return;
1292
+ }
1293
+ const timestamp = nowIso();
1294
+ const payload = RuntimeLogEventSchema.parse({
1295
+ timestamp,
1296
+ level,
1297
+ component: details?.component ?? component,
1298
+ taskId: details?.taskId,
1299
+ sessionId: details?.sessionId,
1300
+ event: details?.event ?? "log",
1301
+ message,
1302
+ meta: redactValue(details?.meta ?? {})
1303
+ });
1304
+ const line = `[${payload.timestamp}] ${payload.level.toUpperCase()} ${payload.component}: ${payload.message}`;
1305
+ if (level === "error") {
1306
+ console.error(line);
1307
+ } else if (level === "warn") {
1308
+ console.warn(line);
1309
+ } else {
1310
+ console.log(line);
1311
+ }
1312
+ const logPath = resolveLogPath(store.logsDir, timestamp, maxFileSizeBytes);
1313
+ void store.appendLog(logPath, payload);
1314
+ };
1315
+ return {
1316
+ debug: (message, details) => emit("debug", message, details),
1317
+ info: (message, details) => emit("info", message, details),
1318
+ warn: (message, details) => emit("warn", message, details),
1319
+ error: (message, details) => emit("error", message, details)
1320
+ };
1321
+ };
1322
+ // Annotate the CommonJS export names for ESM import in node:
1323
+ 0 && (module.exports = {
1324
+ ArtifactStore,
1325
+ BoundingBoxSchema,
1326
+ CreateTaskRequestSchema,
1327
+ CreateTaskResponseSchema,
1328
+ DEFAULT_CONFIG,
1329
+ ElementDescriptorSchema,
1330
+ PinStateSchema,
1331
+ PinpatchConfigSchema,
1332
+ ProviderErrorCodes,
1333
+ ProviderNameSchema,
1334
+ ProviderProgressSchema,
1335
+ ProviderProgressStatusSchema,
1336
+ ProviderResultSchema,
1337
+ ProviderTerminalStatusSchema,
1338
+ RuntimeLogEventSchema,
1339
+ RuntimeLogLevelSchema,
1340
+ SessionEventSchema,
1341
+ SessionRecordSchema,
1342
+ SseEventSchema,
1343
+ SseHeartbeatEventSchema,
1344
+ SseProgressEventSchema,
1345
+ SseTerminalEventSchema,
1346
+ SubmitTaskRequestSchema,
1347
+ SubmitTaskResponseSchema,
1348
+ TaskEventBus,
1349
+ TaskPinSchema,
1350
+ TaskRecordSchema,
1351
+ TaskRunner,
1352
+ TaskStatusSchema,
1353
+ UiChangePacketSchema,
1354
+ ViewportSchema,
1355
+ createBridgeServer,
1356
+ createLogger,
1357
+ ensureConfigFile,
1358
+ generateSessionId,
1359
+ generateTaskId,
1360
+ getConfigPath,
1361
+ nowIso,
1362
+ readConfigFile,
1363
+ resolveConfig
1364
+ });
1365
+ //# sourceMappingURL=index.cjs.map