@tstax/coding-tab 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -7
- package/dist/browser.js +81 -39
- package/dist/browser.js.map +1 -1
- package/dist/server.cjs +642 -125
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +150 -22
- package/dist/server.d.ts +150 -22
- package/dist/server.js +641 -123
- package/dist/server.js.map +1 -1
- package/dist/style.css +310 -17
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -162,7 +162,11 @@ function makeAuthRouter(opts) {
|
|
|
162
162
|
|
|
163
163
|
// src/server/agentRoutes.ts
|
|
164
164
|
import { Router as Router2 } from "express";
|
|
165
|
-
import {
|
|
165
|
+
import {
|
|
166
|
+
Agent,
|
|
167
|
+
CursorAgentError
|
|
168
|
+
} from "@cursor/sdk";
|
|
169
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
166
170
|
|
|
167
171
|
// src/server/models.ts
|
|
168
172
|
import { Cursor } from "@cursor/sdk";
|
|
@@ -203,7 +207,6 @@ async function resolveModel(apiKey, choice) {
|
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
// src/server/sessions.ts
|
|
206
|
-
import { randomUUID } from "crypto";
|
|
207
210
|
var SESSION_IDLE_TTL_MS = 30 * 60 * 1e3;
|
|
208
211
|
var sessions = /* @__PURE__ */ new Map();
|
|
209
212
|
var sweeperStarted = false;
|
|
@@ -214,35 +217,41 @@ function startSweeper() {
|
|
|
214
217
|
const now = Date.now();
|
|
215
218
|
for (const [id, s] of sessions) {
|
|
216
219
|
if (now - s.lastUsedAt > SESSION_IDLE_TTL_MS) {
|
|
217
|
-
|
|
220
|
+
disposeSessionsForChat(id).catch(
|
|
221
|
+
(e) => console.error("[coding-tab] session sweep dispose failed", e)
|
|
222
|
+
);
|
|
218
223
|
}
|
|
219
224
|
}
|
|
220
225
|
}, 5 * 60 * 1e3).unref?.();
|
|
221
226
|
}
|
|
222
|
-
function
|
|
227
|
+
function registerLiveSession(opts) {
|
|
223
228
|
startSweeper();
|
|
224
|
-
const
|
|
229
|
+
const prev = sessions.get(opts.chatId);
|
|
230
|
+
if (prev) {
|
|
231
|
+
sessions.delete(opts.chatId);
|
|
232
|
+
prev.agent[Symbol.asyncDispose]().catch(() => {
|
|
233
|
+
});
|
|
234
|
+
}
|
|
225
235
|
const session = {
|
|
226
|
-
|
|
236
|
+
chatId: opts.chatId,
|
|
227
237
|
githubLogin: opts.githubLogin,
|
|
228
238
|
agent: opts.agent,
|
|
229
|
-
|
|
230
|
-
startingRef: opts.startingRef,
|
|
239
|
+
agentId: opts.agentId,
|
|
231
240
|
createdAt: Date.now(),
|
|
232
241
|
lastUsedAt: Date.now()
|
|
233
242
|
};
|
|
234
|
-
sessions.set(
|
|
243
|
+
sessions.set(opts.chatId, session);
|
|
235
244
|
return session;
|
|
236
245
|
}
|
|
237
|
-
function
|
|
238
|
-
const s = sessions.get(
|
|
246
|
+
function getLiveSession(chatId) {
|
|
247
|
+
const s = sessions.get(chatId);
|
|
239
248
|
if (s) s.lastUsedAt = Date.now();
|
|
240
249
|
return s;
|
|
241
250
|
}
|
|
242
|
-
async function
|
|
243
|
-
const s = sessions.get(
|
|
251
|
+
async function disposeSessionsForChat(chatId) {
|
|
252
|
+
const s = sessions.get(chatId);
|
|
244
253
|
if (!s) return;
|
|
245
|
-
sessions.delete(
|
|
254
|
+
sessions.delete(chatId);
|
|
246
255
|
try {
|
|
247
256
|
await s.agent[Symbol.asyncDispose]();
|
|
248
257
|
} catch (err) {
|
|
@@ -250,6 +259,235 @@ async function disposeSession(id) {
|
|
|
250
259
|
}
|
|
251
260
|
}
|
|
252
261
|
|
|
262
|
+
// src/server/storage.ts
|
|
263
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
264
|
+
import { dirname, join, resolve } from "path";
|
|
265
|
+
import { randomUUID } from "crypto";
|
|
266
|
+
function safeId(s) {
|
|
267
|
+
return s.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
268
|
+
}
|
|
269
|
+
async function ensureDir(p) {
|
|
270
|
+
await mkdir(p, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
async function readJson(path) {
|
|
273
|
+
try {
|
|
274
|
+
const raw = await readFile(path, "utf8");
|
|
275
|
+
return JSON.parse(raw);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (err.code === "ENOENT") return null;
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function writeJsonAtomic(path, data) {
|
|
282
|
+
await ensureDir(dirname(path));
|
|
283
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
284
|
+
await writeFile(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
285
|
+
await rename(tmp, path);
|
|
286
|
+
}
|
|
287
|
+
var MAX_BLOB_BYTES = 32 * 1024;
|
|
288
|
+
function truncateBlob(value) {
|
|
289
|
+
if (value === void 0 || value === null) return value;
|
|
290
|
+
try {
|
|
291
|
+
const str = typeof value === "string" ? value : JSON.stringify(value);
|
|
292
|
+
if (str.length <= MAX_BLOB_BYTES) return value;
|
|
293
|
+
const head = str.slice(0, MAX_BLOB_BYTES);
|
|
294
|
+
return `${head}
|
|
295
|
+
|
|
296
|
+
\u2026[truncated ${str.length - MAX_BLOB_BYTES} chars]`;
|
|
297
|
+
} catch {
|
|
298
|
+
return "[unserializable]";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function sanitizeEvent(evt) {
|
|
302
|
+
if (evt.kind === "tool") {
|
|
303
|
+
return {
|
|
304
|
+
...evt,
|
|
305
|
+
args: truncateBlob(evt.args),
|
|
306
|
+
result: truncateBlob(evt.result)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return evt;
|
|
310
|
+
}
|
|
311
|
+
var FileChatStorage = class {
|
|
312
|
+
dataDir;
|
|
313
|
+
/** Per-chat write mutex chain to keep concurrent appends consistent. */
|
|
314
|
+
chains = /* @__PURE__ */ new Map();
|
|
315
|
+
constructor(opts) {
|
|
316
|
+
this.dataDir = resolve(opts.dataDir);
|
|
317
|
+
}
|
|
318
|
+
chatPath(id) {
|
|
319
|
+
return join(this.dataDir, "chats", `${safeId(id)}.json`);
|
|
320
|
+
}
|
|
321
|
+
indexPath(login) {
|
|
322
|
+
return join(this.dataDir, "index", `${safeId(login.toLowerCase())}.json`);
|
|
323
|
+
}
|
|
324
|
+
/** Serialize all reads/writes for a single chat through a chained promise. */
|
|
325
|
+
withChat(id, fn) {
|
|
326
|
+
const prev = this.chains.get(id) ?? Promise.resolve();
|
|
327
|
+
const next = prev.then(fn, fn);
|
|
328
|
+
this.chains.set(
|
|
329
|
+
id,
|
|
330
|
+
next.finally(() => {
|
|
331
|
+
if (this.chains.get(id) === next) this.chains.delete(id);
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
return next;
|
|
335
|
+
}
|
|
336
|
+
async readChat(id) {
|
|
337
|
+
return readJson(this.chatPath(id));
|
|
338
|
+
}
|
|
339
|
+
async writeChat(file) {
|
|
340
|
+
await writeJsonAtomic(this.chatPath(file.chat.id), file);
|
|
341
|
+
}
|
|
342
|
+
async readIndex(login) {
|
|
343
|
+
const list = await readJson(this.indexPath(login));
|
|
344
|
+
return list ?? [];
|
|
345
|
+
}
|
|
346
|
+
async writeIndex(login, items) {
|
|
347
|
+
items.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
348
|
+
await writeJsonAtomic(this.indexPath(login), items);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Rebuild the index for a user by scanning every chat file. Used as a
|
|
352
|
+
* self-heal path when the index gets out of sync (e.g. crash mid-write).
|
|
353
|
+
*/
|
|
354
|
+
async rebuildIndex(login) {
|
|
355
|
+
const dir = join(this.dataDir, "chats");
|
|
356
|
+
let entries = [];
|
|
357
|
+
try {
|
|
358
|
+
entries = await readdir(dir);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
if (err.code !== "ENOENT") throw err;
|
|
361
|
+
}
|
|
362
|
+
const items = [];
|
|
363
|
+
const target = login.toLowerCase();
|
|
364
|
+
for (const name of entries) {
|
|
365
|
+
if (!name.endsWith(".json")) continue;
|
|
366
|
+
const file = await readJson(join(dir, name));
|
|
367
|
+
if (!file?.chat) continue;
|
|
368
|
+
if (file.chat.githubLogin.toLowerCase() !== target) continue;
|
|
369
|
+
items.push(this.toListItem(file.chat));
|
|
370
|
+
}
|
|
371
|
+
await this.writeIndex(login, items);
|
|
372
|
+
return items;
|
|
373
|
+
}
|
|
374
|
+
toListItem(c) {
|
|
375
|
+
return {
|
|
376
|
+
id: c.id,
|
|
377
|
+
title: c.title,
|
|
378
|
+
mode: c.mode,
|
|
379
|
+
model: c.model,
|
|
380
|
+
createdAt: c.createdAt,
|
|
381
|
+
updatedAt: c.updatedAt
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async upsertIndex(chat) {
|
|
385
|
+
const items = await this.readIndex(chat.githubLogin);
|
|
386
|
+
const idx = items.findIndex((c) => c.id === chat.id);
|
|
387
|
+
const item = this.toListItem(chat);
|
|
388
|
+
if (idx >= 0) items[idx] = item;
|
|
389
|
+
else items.push(item);
|
|
390
|
+
await this.writeIndex(chat.githubLogin, items);
|
|
391
|
+
}
|
|
392
|
+
async removeFromIndex(login, chatId) {
|
|
393
|
+
const items = await this.readIndex(login);
|
|
394
|
+
const filtered = items.filter((c) => c.id !== chatId);
|
|
395
|
+
if (filtered.length !== items.length) {
|
|
396
|
+
await this.writeIndex(login, filtered);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async listChats(login) {
|
|
400
|
+
const items = await this.readIndex(login);
|
|
401
|
+
if (items.length > 0) return items;
|
|
402
|
+
return this.rebuildIndex(login);
|
|
403
|
+
}
|
|
404
|
+
async loadChat(id, login) {
|
|
405
|
+
return this.withChat(id, async () => {
|
|
406
|
+
const file = await this.readChat(id);
|
|
407
|
+
if (!file) return null;
|
|
408
|
+
if (file.chat.githubLogin.toLowerCase() !== login.toLowerCase()) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const turns = [...file.turns].sort((a, b) => a.seq - b.seq);
|
|
412
|
+
return { ...file.chat, turns };
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async createChat(chat) {
|
|
416
|
+
await this.withChat(chat.id, async () => {
|
|
417
|
+
await this.writeChat({ chat, turns: [] });
|
|
418
|
+
});
|
|
419
|
+
await this.upsertIndex(chat);
|
|
420
|
+
}
|
|
421
|
+
async patchChat(id, login, patch) {
|
|
422
|
+
const updated = await this.withChat(id, async () => {
|
|
423
|
+
const file = await this.readChat(id);
|
|
424
|
+
if (!file) return null;
|
|
425
|
+
if (file.chat.githubLogin.toLowerCase() !== login.toLowerCase()) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const next = {
|
|
429
|
+
...file.chat,
|
|
430
|
+
...patch,
|
|
431
|
+
updatedAt: Date.now()
|
|
432
|
+
};
|
|
433
|
+
await this.writeChat({ chat: next, turns: file.turns });
|
|
434
|
+
return next;
|
|
435
|
+
});
|
|
436
|
+
if (updated) await this.upsertIndex(updated);
|
|
437
|
+
return updated;
|
|
438
|
+
}
|
|
439
|
+
async appendTurn(turn) {
|
|
440
|
+
const sanitized = {
|
|
441
|
+
...turn,
|
|
442
|
+
events: turn.events.map(sanitizeEvent)
|
|
443
|
+
};
|
|
444
|
+
let chat = null;
|
|
445
|
+
await this.withChat(turn.chatId, async () => {
|
|
446
|
+
const file = await this.readChat(turn.chatId);
|
|
447
|
+
if (!file) throw new Error(`chat_not_found:${turn.chatId}`);
|
|
448
|
+
file.turns = file.turns.filter((t) => t.id !== sanitized.id);
|
|
449
|
+
file.turns.push(sanitized);
|
|
450
|
+
file.chat = { ...file.chat, updatedAt: Date.now() };
|
|
451
|
+
chat = file.chat;
|
|
452
|
+
await this.writeChat(file);
|
|
453
|
+
});
|
|
454
|
+
if (chat) await this.upsertIndex(chat);
|
|
455
|
+
}
|
|
456
|
+
async patchTurn(chatId, turnId, patch) {
|
|
457
|
+
let chat = null;
|
|
458
|
+
await this.withChat(chatId, async () => {
|
|
459
|
+
const file = await this.readChat(chatId);
|
|
460
|
+
if (!file) return;
|
|
461
|
+
const idx = file.turns.findIndex((t) => t.id === turnId);
|
|
462
|
+
if (idx < 0) return;
|
|
463
|
+
const current = file.turns[idx];
|
|
464
|
+
const events = patch.events ? patch.events.map(sanitizeEvent) : current.events;
|
|
465
|
+
file.turns[idx] = { ...current, ...patch, events };
|
|
466
|
+
file.chat = { ...file.chat, updatedAt: Date.now() };
|
|
467
|
+
chat = file.chat;
|
|
468
|
+
await this.writeChat(file);
|
|
469
|
+
});
|
|
470
|
+
if (chat) await this.upsertIndex(chat);
|
|
471
|
+
}
|
|
472
|
+
async deleteChat(id, login) {
|
|
473
|
+
const removed = await this.withChat(id, async () => {
|
|
474
|
+
const file = await this.readChat(id);
|
|
475
|
+
if (!file) return false;
|
|
476
|
+
if (file.chat.githubLogin.toLowerCase() !== login.toLowerCase()) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
await rm(this.chatPath(id), { force: true });
|
|
480
|
+
return true;
|
|
481
|
+
});
|
|
482
|
+
if (removed) await this.removeFromIndex(login, id);
|
|
483
|
+
return removed;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
function createDefaultStorage(dataDir) {
|
|
487
|
+
const dir = dataDir ?? join(process.cwd(), ".coding-tab-data");
|
|
488
|
+
return new FileChatStorage({ dataDir: dir });
|
|
489
|
+
}
|
|
490
|
+
|
|
253
491
|
// src/server/agentRoutes.ts
|
|
254
492
|
var PLAN_INSTRUCTION = `You are operating in PLAN MODE.
|
|
255
493
|
|
|
@@ -291,39 +529,221 @@ function setupSseHeaders(res) {
|
|
|
291
529
|
});
|
|
292
530
|
res.flushHeaders?.();
|
|
293
531
|
}
|
|
294
|
-
|
|
532
|
+
function deriveTitle(prompt) {
|
|
533
|
+
const trimmed = prompt.trim().replace(/\s+/g, " ");
|
|
534
|
+
if (trimmed.length <= 60) return trimmed || "New chat";
|
|
535
|
+
return `${trimmed.slice(0, 57)}\u2026`;
|
|
536
|
+
}
|
|
537
|
+
var TurnBuffer = class {
|
|
538
|
+
events = [];
|
|
539
|
+
lastWasText = false;
|
|
540
|
+
pushText(text) {
|
|
541
|
+
if (this.lastWasText) {
|
|
542
|
+
const last = this.events[this.events.length - 1];
|
|
543
|
+
if (last.kind === "text") {
|
|
544
|
+
last.text += text;
|
|
545
|
+
return last;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const evt = { kind: "text", id: randomUUID2(), text };
|
|
549
|
+
this.events.push(evt);
|
|
550
|
+
this.lastWasText = true;
|
|
551
|
+
return evt;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Mark the next text event as starting a new block (called when the SDK
|
|
555
|
+
* emits a non-streaming-delta boundary, e.g. a tool call or a new assistant
|
|
556
|
+
* message). The next `pushText` will start a fresh paragraph.
|
|
557
|
+
*/
|
|
558
|
+
endTextBlock() {
|
|
559
|
+
this.lastWasText = false;
|
|
560
|
+
}
|
|
561
|
+
upsertTool(args) {
|
|
562
|
+
this.endTextBlock();
|
|
563
|
+
const existing = this.events.find(
|
|
564
|
+
(e) => e.kind === "tool" && e.callId === args.callId
|
|
565
|
+
);
|
|
566
|
+
if (existing) {
|
|
567
|
+
existing.status = args.status;
|
|
568
|
+
if (args.args !== void 0) existing.args = args.args;
|
|
569
|
+
if (args.result !== void 0) existing.result = args.result;
|
|
570
|
+
return existing;
|
|
571
|
+
}
|
|
572
|
+
const evt = {
|
|
573
|
+
kind: "tool",
|
|
574
|
+
id: randomUUID2(),
|
|
575
|
+
callId: args.callId,
|
|
576
|
+
name: args.name,
|
|
577
|
+
status: args.status,
|
|
578
|
+
args: args.args,
|
|
579
|
+
result: args.result
|
|
580
|
+
};
|
|
581
|
+
this.events.push(evt);
|
|
582
|
+
return evt;
|
|
583
|
+
}
|
|
584
|
+
snapshot() {
|
|
585
|
+
return this.events.map(sanitizeEvent);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
async function streamRun(res, run, ctx) {
|
|
589
|
+
const flushDebounceMs = 750;
|
|
590
|
+
let flushPending = false;
|
|
591
|
+
let lastFlush = 0;
|
|
592
|
+
const persist = async (status, pr) => {
|
|
593
|
+
try {
|
|
594
|
+
await ctx.storage.patchTurn(ctx.chat.id, ctx.turn.id, {
|
|
595
|
+
events: ctx.buffer.snapshot(),
|
|
596
|
+
...status ? { status } : {},
|
|
597
|
+
...pr ? { pr } : {}
|
|
598
|
+
});
|
|
599
|
+
lastFlush = Date.now();
|
|
600
|
+
} catch (err) {
|
|
601
|
+
console.error("[coding-tab] patchTurn failed", err);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const scheduleFlush = () => {
|
|
605
|
+
if (flushPending) return;
|
|
606
|
+
flushPending = true;
|
|
607
|
+
setTimeout(async () => {
|
|
608
|
+
flushPending = false;
|
|
609
|
+
const elapsed = Date.now() - lastFlush;
|
|
610
|
+
if (elapsed < flushDebounceMs) return;
|
|
611
|
+
await persist();
|
|
612
|
+
}, flushDebounceMs).unref?.();
|
|
613
|
+
};
|
|
614
|
+
let finalStatus = "finished";
|
|
615
|
+
let finalPr;
|
|
295
616
|
try {
|
|
296
617
|
for await (const message of run.stream()) {
|
|
297
618
|
if (res.writableEnded) break;
|
|
298
619
|
if (message.type === "assistant") {
|
|
620
|
+
ctx.buffer.endTextBlock();
|
|
299
621
|
for (const block of message.message.content) {
|
|
300
|
-
if (block.type === "text" && block.text)
|
|
301
|
-
|
|
622
|
+
if (block.type === "text" && block.text) {
|
|
623
|
+
ctx.buffer.pushText(block.text);
|
|
624
|
+
ctx.buffer.endTextBlock();
|
|
625
|
+
sse(res, { kind: "text", text: block.text });
|
|
626
|
+
} else if (block.type === "tool_use") {
|
|
627
|
+
ctx.buffer.upsertTool({
|
|
628
|
+
callId: block.id,
|
|
629
|
+
name: block.name,
|
|
630
|
+
status: "running",
|
|
631
|
+
args: block.input
|
|
632
|
+
});
|
|
633
|
+
sse(res, {
|
|
634
|
+
kind: "tool",
|
|
635
|
+
name: block.name,
|
|
636
|
+
status: "running",
|
|
637
|
+
callId: block.id,
|
|
638
|
+
args: block.input
|
|
639
|
+
});
|
|
640
|
+
}
|
|
302
641
|
}
|
|
303
642
|
} else if (message.type === "thinking") {
|
|
304
643
|
sse(res, { kind: "thinking", text: message.text });
|
|
305
644
|
} else if (message.type === "tool_call") {
|
|
306
|
-
|
|
645
|
+
ctx.buffer.upsertTool({
|
|
646
|
+
callId: message.call_id,
|
|
647
|
+
name: message.name,
|
|
648
|
+
status: message.status,
|
|
649
|
+
args: message.args,
|
|
650
|
+
result: message.result
|
|
651
|
+
});
|
|
652
|
+
sse(res, {
|
|
653
|
+
kind: "tool",
|
|
654
|
+
name: message.name,
|
|
655
|
+
status: message.status,
|
|
656
|
+
callId: message.call_id,
|
|
657
|
+
args: message.args,
|
|
658
|
+
result: message.result
|
|
659
|
+
});
|
|
307
660
|
} else if (message.type === "status") {
|
|
308
661
|
sse(res, { kind: "status", status: message.status, message: message.message });
|
|
309
662
|
}
|
|
663
|
+
scheduleFlush();
|
|
310
664
|
}
|
|
311
665
|
const result = await run.wait();
|
|
312
666
|
const prUrl = result.git?.branches?.find((b) => b.prUrl)?.prUrl;
|
|
667
|
+
finalPr = parsePrUrl(prUrl);
|
|
668
|
+
finalStatus = result.status ?? "finished";
|
|
313
669
|
sse(res, {
|
|
314
670
|
kind: "result",
|
|
315
671
|
status: result.status,
|
|
316
|
-
pr:
|
|
672
|
+
pr: finalPr,
|
|
317
673
|
durationMs: result.durationMs
|
|
318
674
|
});
|
|
319
675
|
} catch (err) {
|
|
676
|
+
finalStatus = "error";
|
|
320
677
|
const message = err instanceof Error ? err.message : String(err);
|
|
321
678
|
const retryable = err instanceof CursorAgentError ? Boolean(err.isRetryable) : false;
|
|
322
679
|
sse(res, { kind: "error", message, retryable });
|
|
323
680
|
} finally {
|
|
681
|
+
await persist(finalStatus, finalPr);
|
|
324
682
|
if (!res.writableEnded) res.end();
|
|
325
683
|
}
|
|
326
684
|
}
|
|
685
|
+
async function ensureAgentForChat(storage, chat, user, opts, modelChoice) {
|
|
686
|
+
const existing = getLiveSession(chat.id);
|
|
687
|
+
if (existing && existing.githubLogin.toLowerCase() === user.githubLogin.toLowerCase()) {
|
|
688
|
+
return { agent: existing.agent, agentId: existing.agentId, resumed: true };
|
|
689
|
+
}
|
|
690
|
+
const resolved = await resolveModel(opts.cursorApiKey, modelChoice);
|
|
691
|
+
const cloud = {
|
|
692
|
+
repos: [
|
|
693
|
+
{
|
|
694
|
+
url: chat.repoUrl,
|
|
695
|
+
startingRef: chat.startingRef
|
|
696
|
+
}
|
|
697
|
+
],
|
|
698
|
+
autoCreatePR: chat.mode === "agent",
|
|
699
|
+
skipReviewerRequest: opts.skipReviewerRequest ?? true,
|
|
700
|
+
envVars: { GITHUB_TOKEN: user.accessToken },
|
|
701
|
+
...opts.envName ? { env: { type: "cloud", name: opts.envName } } : {}
|
|
702
|
+
};
|
|
703
|
+
if (chat.agentId) {
|
|
704
|
+
try {
|
|
705
|
+
const resumed = await Agent.resume(chat.agentId, {
|
|
706
|
+
apiKey: opts.cursorApiKey,
|
|
707
|
+
model: { id: resolved.cursorModelId },
|
|
708
|
+
cloud
|
|
709
|
+
});
|
|
710
|
+
registerLiveSession({
|
|
711
|
+
chatId: chat.id,
|
|
712
|
+
githubLogin: user.githubLogin,
|
|
713
|
+
agent: resumed,
|
|
714
|
+
agentId: resumed.agentId
|
|
715
|
+
});
|
|
716
|
+
if (resumed.agentId !== chat.agentId) {
|
|
717
|
+
await storage.patchChat(chat.id, user.githubLogin, { agentId: resumed.agentId });
|
|
718
|
+
}
|
|
719
|
+
return { agent: resumed, agentId: resumed.agentId, resumed: true };
|
|
720
|
+
} catch (err) {
|
|
721
|
+
console.warn(
|
|
722
|
+
`[coding-tab] resume failed for chat=${chat.id} agentId=${chat.agentId}; creating fresh agent`,
|
|
723
|
+
err instanceof Error ? err.message : err
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const fresh = await Agent.create({
|
|
728
|
+
apiKey: opts.cursorApiKey,
|
|
729
|
+
model: { id: resolved.cursorModelId },
|
|
730
|
+
cloud
|
|
731
|
+
});
|
|
732
|
+
registerLiveSession({
|
|
733
|
+
chatId: chat.id,
|
|
734
|
+
githubLogin: user.githubLogin,
|
|
735
|
+
agent: fresh,
|
|
736
|
+
agentId: fresh.agentId
|
|
737
|
+
});
|
|
738
|
+
await storage.patchChat(chat.id, user.githubLogin, { agentId: fresh.agentId });
|
|
739
|
+
return { agent: fresh, agentId: fresh.agentId, resumed: false };
|
|
740
|
+
}
|
|
741
|
+
async function nextSeq(storage, chatId, login) {
|
|
742
|
+
const full = await storage.loadChat(chatId, login);
|
|
743
|
+
if (!full) throw Object.assign(new Error("chat_not_found"), { status: 404 });
|
|
744
|
+
const max = full.turns.reduce((acc, t) => Math.max(acc, t.seq), -1);
|
|
745
|
+
return max + 1;
|
|
746
|
+
}
|
|
327
747
|
function makeAgentRouter(opts) {
|
|
328
748
|
const router = Router2();
|
|
329
749
|
router.get("/models", async (_req, res) => {
|
|
@@ -335,144 +755,236 @@ function makeAgentRouter(opts) {
|
|
|
335
755
|
res.status(500).json({ error: err instanceof Error ? err.message : "models_failed" });
|
|
336
756
|
}
|
|
337
757
|
});
|
|
338
|
-
|
|
758
|
+
const handleSend = async (req, res, isExecute) => {
|
|
339
759
|
const user = req.user;
|
|
340
|
-
const
|
|
341
|
-
if (!
|
|
760
|
+
const body = req.body ?? {};
|
|
761
|
+
if (!body.chatId) {
|
|
762
|
+
res.status(400).json({ error: "missing_chatId" });
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (!isExecute && (!body.prompt || body.mode !== "plan" && body.mode !== "agent")) {
|
|
342
766
|
res.status(400).json({ error: "invalid_request" });
|
|
343
767
|
return;
|
|
344
768
|
}
|
|
769
|
+
const chat = await opts.storage.loadChat(body.chatId, user.githubLogin);
|
|
770
|
+
if (!chat) {
|
|
771
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
345
774
|
setupSseHeaders(res);
|
|
346
|
-
let
|
|
775
|
+
let assistantTurn = null;
|
|
347
776
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
777
|
+
if (!isExecute && body.mode && body.mode !== chat.mode) {
|
|
778
|
+
chat.mode = body.mode;
|
|
779
|
+
await opts.storage.patchChat(chat.id, user.githubLogin, { mode: body.mode });
|
|
780
|
+
}
|
|
781
|
+
let seq = await nextSeq(opts.storage, chat.id, user.githubLogin);
|
|
782
|
+
if (!isExecute) {
|
|
783
|
+
const userTurn = {
|
|
784
|
+
id: randomUUID2(),
|
|
785
|
+
chatId: chat.id,
|
|
786
|
+
seq: seq++,
|
|
787
|
+
role: "user",
|
|
788
|
+
isPlan: chat.mode === "plan",
|
|
789
|
+
status: "finished",
|
|
790
|
+
events: [{ kind: "text", id: randomUUID2(), text: body.prompt }],
|
|
791
|
+
prompt: body.prompt,
|
|
792
|
+
createdAt: Date.now()
|
|
793
|
+
};
|
|
794
|
+
await opts.storage.appendTurn(userTurn);
|
|
795
|
+
if (chat.title === "New chat") {
|
|
796
|
+
const title = deriveTitle(body.prompt);
|
|
797
|
+
chat.title = title;
|
|
798
|
+
await opts.storage.patchChat(chat.id, user.githubLogin, { title });
|
|
363
799
|
}
|
|
800
|
+
}
|
|
801
|
+
assistantTurn = {
|
|
802
|
+
id: randomUUID2(),
|
|
803
|
+
chatId: chat.id,
|
|
804
|
+
seq,
|
|
805
|
+
role: "assistant",
|
|
806
|
+
isPlan: !isExecute && chat.mode === "plan",
|
|
807
|
+
status: "running",
|
|
808
|
+
events: [],
|
|
809
|
+
createdAt: Date.now()
|
|
810
|
+
};
|
|
811
|
+
await opts.storage.appendTurn(assistantTurn);
|
|
812
|
+
const { agent } = await ensureAgentForChat(opts.storage, chat, user, opts, chat.model);
|
|
813
|
+
const sendText = isExecute ? EXECUTE_INSTRUCTION : modeInstructionPrefix(body.mode, body.prompt);
|
|
814
|
+
const run = await agent.send(sendText);
|
|
815
|
+
sse(res, {
|
|
816
|
+
kind: "ready",
|
|
817
|
+
chatId: chat.id,
|
|
818
|
+
turnId: assistantTurn.id,
|
|
819
|
+
agentId: agent.agentId,
|
|
820
|
+
runId: run.id
|
|
364
821
|
});
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
822
|
+
console.log(
|
|
823
|
+
`[coding-tab] ${isExecute ? "execute" : "send"} agent=${agent.agentId} run=${run.id} chat=${chat.id} login=${user.githubLogin}`
|
|
824
|
+
);
|
|
825
|
+
const buffer = new TurnBuffer();
|
|
826
|
+
await streamRun(res, run, {
|
|
827
|
+
storage: opts.storage,
|
|
828
|
+
chat,
|
|
829
|
+
turn: assistantTurn,
|
|
830
|
+
buffer
|
|
370
831
|
});
|
|
371
|
-
const run = await agent.send(modeInstructionPrefix(mode, prompt));
|
|
372
|
-
sse(res, { kind: "ready", sessionId: session.id, agentId: agent.agentId, runId: run.id });
|
|
373
|
-
console.log(`[coding-tab] start agent=${agent.agentId} run=${run.id} session=${session.id} login=${user.githubLogin}`);
|
|
374
|
-
await streamRun(res, run);
|
|
375
832
|
} catch (err) {
|
|
376
833
|
const message = err instanceof Error ? err.message : String(err);
|
|
377
834
|
const retryable = err instanceof CursorAgentError ? Boolean(err.isRetryable) : false;
|
|
378
|
-
console.error(
|
|
835
|
+
console.error(`[coding-tab] /agent/${isExecute ? "execute" : "send"} failed`, err);
|
|
379
836
|
sse(res, { kind: "error", message, retryable });
|
|
837
|
+
if (assistantTurn) {
|
|
838
|
+
await opts.storage.patchTurn(assistantTurn.chatId, assistantTurn.id, {
|
|
839
|
+
status: "error",
|
|
840
|
+
events: [
|
|
841
|
+
...assistantTurn.events,
|
|
842
|
+
{ kind: "text", id: randomUUID2(), text: `[error] ${message}` }
|
|
843
|
+
]
|
|
844
|
+
}).catch(() => {
|
|
845
|
+
});
|
|
846
|
+
}
|
|
380
847
|
if (!res.writableEnded) res.end();
|
|
381
|
-
if (agent) await agent[Symbol.asyncDispose]().catch(() => {
|
|
382
|
-
});
|
|
383
848
|
}
|
|
384
849
|
req.on("close", () => {
|
|
385
850
|
if (!res.writableEnded) res.end();
|
|
386
851
|
});
|
|
387
|
-
}
|
|
388
|
-
router.post("/agent/
|
|
389
|
-
|
|
390
|
-
|
|
852
|
+
};
|
|
853
|
+
router.post("/agent/start", (req, res) => handleSend(req, res, false));
|
|
854
|
+
router.post("/agent/send", (req, res) => handleSend(req, res, false));
|
|
855
|
+
router.post("/agent/execute", (req, res) => handleSend(req, res, true));
|
|
856
|
+
router.post("/agent/cancel", async (req, res) => {
|
|
857
|
+
const { chatId, runId } = req.body ?? {};
|
|
858
|
+
if (!chatId || !runId) {
|
|
391
859
|
res.status(400).json({ error: "invalid_request" });
|
|
392
860
|
return;
|
|
393
861
|
}
|
|
394
|
-
const
|
|
395
|
-
if (!
|
|
862
|
+
const live = getLiveSession(chatId);
|
|
863
|
+
if (!live) {
|
|
396
864
|
res.status(404).json({ error: "session_not_found" });
|
|
397
865
|
return;
|
|
398
866
|
}
|
|
399
|
-
setupSseHeaders(res);
|
|
400
867
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
868
|
+
await Agent.cancelRun(runId, {
|
|
869
|
+
runtime: "cloud",
|
|
870
|
+
agentId: live.agentId,
|
|
871
|
+
apiKey: opts.cursorApiKey
|
|
872
|
+
});
|
|
873
|
+
res.json({ ok: true });
|
|
405
874
|
} catch (err) {
|
|
406
875
|
const message = err instanceof Error ? err.message : String(err);
|
|
407
|
-
console.error("[coding-tab] /agent/
|
|
408
|
-
|
|
409
|
-
if (!res.writableEnded) res.end();
|
|
876
|
+
console.error("[coding-tab] /agent/cancel failed", err);
|
|
877
|
+
res.status(500).json({ error: message });
|
|
410
878
|
}
|
|
411
|
-
req.on("close", () => {
|
|
412
|
-
if (!res.writableEnded) res.end();
|
|
413
|
-
});
|
|
414
879
|
});
|
|
415
|
-
router.post("/agent/
|
|
416
|
-
const {
|
|
417
|
-
if (!
|
|
880
|
+
router.post("/agent/dispose", async (req, res) => {
|
|
881
|
+
const { chatId } = req.body ?? {};
|
|
882
|
+
if (!chatId) {
|
|
418
883
|
res.status(400).json({ error: "invalid_request" });
|
|
419
884
|
return;
|
|
420
885
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
886
|
+
await disposeSessionsForChat(chatId);
|
|
887
|
+
res.json({ ok: true });
|
|
888
|
+
});
|
|
889
|
+
return router;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/server/chatRoutes.ts
|
|
893
|
+
import { Router as Router3 } from "express";
|
|
894
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
895
|
+
var VALID_MODES = ["plan", "agent"];
|
|
896
|
+
var VALID_MODELS = ["sonnet", "opus"];
|
|
897
|
+
function makeChatRouter(opts) {
|
|
898
|
+
const router = Router3();
|
|
899
|
+
router.get("/chats", async (req, res) => {
|
|
900
|
+
const user = req.user;
|
|
427
901
|
try {
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
console.log(`[coding-tab] execute agent=${session.agent.agentId} run=${run.id} session=${session.id}`);
|
|
431
|
-
await streamRun(res, run);
|
|
902
|
+
const items = await opts.storage.listChats(user.githubLogin);
|
|
903
|
+
res.json({ chats: items });
|
|
432
904
|
} catch (err) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
sse(res, { kind: "error", message });
|
|
436
|
-
if (!res.writableEnded) res.end();
|
|
905
|
+
console.error("[coding-tab] /chats list failed", err);
|
|
906
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "list_failed" });
|
|
437
907
|
}
|
|
438
|
-
req.on("close", () => {
|
|
439
|
-
if (!res.writableEnded) res.end();
|
|
440
|
-
});
|
|
441
908
|
});
|
|
442
|
-
router.post("/
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
909
|
+
router.post("/chats", async (req, res) => {
|
|
910
|
+
const user = req.user;
|
|
911
|
+
const body = req.body ?? {};
|
|
912
|
+
const mode = VALID_MODES.includes(body.mode) ? body.mode : "plan";
|
|
913
|
+
const model = VALID_MODELS.includes(body.model) ? body.model : "sonnet";
|
|
914
|
+
const now = Date.now();
|
|
915
|
+
const chat = {
|
|
916
|
+
id: randomUUID3(),
|
|
917
|
+
githubLogin: user.githubLogin,
|
|
918
|
+
title: (body.title ?? "").trim() || "New chat",
|
|
919
|
+
mode,
|
|
920
|
+
model,
|
|
921
|
+
repoUrl: body.repoUrl?.trim() || opts.defaultRepo.url,
|
|
922
|
+
startingRef: body.startingRef ?? opts.defaultRepo.ref,
|
|
923
|
+
createdAt: now,
|
|
924
|
+
updatedAt: now
|
|
925
|
+
};
|
|
926
|
+
try {
|
|
927
|
+
await opts.storage.createChat(chat);
|
|
928
|
+
res.status(201).json({ chat });
|
|
929
|
+
} catch (err) {
|
|
930
|
+
console.error("[coding-tab] /chats create failed", err);
|
|
931
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "create_failed" });
|
|
447
932
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
933
|
+
});
|
|
934
|
+
router.get("/chats/:id", async (req, res) => {
|
|
935
|
+
const user = req.user;
|
|
936
|
+
try {
|
|
937
|
+
const full = await opts.storage.loadChat(req.params.id, user.githubLogin);
|
|
938
|
+
if (!full) {
|
|
939
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
res.json({ chat: full });
|
|
943
|
+
} catch (err) {
|
|
944
|
+
console.error("[coding-tab] /chats/:id load failed", err);
|
|
945
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "load_failed" });
|
|
452
946
|
}
|
|
947
|
+
});
|
|
948
|
+
router.patch("/chats/:id", async (req, res) => {
|
|
949
|
+
const user = req.user;
|
|
950
|
+
const body = req.body ?? {};
|
|
951
|
+
const patch = {};
|
|
952
|
+
if (typeof body.title === "string") patch.title = body.title.trim() || "Untitled";
|
|
953
|
+
if (body.mode && VALID_MODES.includes(body.mode)) patch.mode = body.mode;
|
|
954
|
+
if (body.model && VALID_MODELS.includes(body.model)) patch.model = body.model;
|
|
453
955
|
try {
|
|
454
|
-
|
|
455
|
-
|
|
956
|
+
const updated = await opts.storage.patchChat(req.params.id, user.githubLogin, patch);
|
|
957
|
+
if (!updated) {
|
|
958
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
res.json({ chat: updated });
|
|
456
962
|
} catch (err) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
res.status(500).json({ error: message });
|
|
963
|
+
console.error("[coding-tab] /chats/:id patch failed", err);
|
|
964
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "patch_failed" });
|
|
460
965
|
}
|
|
461
966
|
});
|
|
462
|
-
router.
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
967
|
+
router.delete("/chats/:id", async (req, res) => {
|
|
968
|
+
const user = req.user;
|
|
969
|
+
try {
|
|
970
|
+
const removed = await opts.storage.deleteChat(req.params.id, user.githubLogin);
|
|
971
|
+
if (!removed) {
|
|
972
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
await disposeSessionsForChat(req.params.id).catch(() => {
|
|
976
|
+
});
|
|
977
|
+
res.json({ ok: true });
|
|
978
|
+
} catch (err) {
|
|
979
|
+
console.error("[coding-tab] /chats/:id delete failed", err);
|
|
980
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "delete_failed" });
|
|
467
981
|
}
|
|
468
|
-
await disposeSession(sessionId);
|
|
469
|
-
res.json({ ok: true });
|
|
470
982
|
});
|
|
471
983
|
return router;
|
|
472
984
|
}
|
|
473
985
|
|
|
474
986
|
// src/server/githubRoutes.ts
|
|
475
|
-
import { Router as
|
|
987
|
+
import { Router as Router4 } from "express";
|
|
476
988
|
import { Octokit } from "@octokit/rest";
|
|
477
989
|
function parsePrUrl2(prUrl) {
|
|
478
990
|
const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
@@ -480,7 +992,7 @@ function parsePrUrl2(prUrl) {
|
|
|
480
992
|
return { owner: match[1], repo: match[2], number: Number(match[3]) };
|
|
481
993
|
}
|
|
482
994
|
function makeGitHubRouter() {
|
|
483
|
-
const router =
|
|
995
|
+
const router = Router4();
|
|
484
996
|
router.get("/pr/status", async (req, res) => {
|
|
485
997
|
const prUrl = typeof req.query.prUrl === "string" ? req.query.prUrl : null;
|
|
486
998
|
if (!prUrl) {
|
|
@@ -559,26 +1071,26 @@ function makeGitHubRouter() {
|
|
|
559
1071
|
}
|
|
560
1072
|
|
|
561
1073
|
// src/server/staticAssets.ts
|
|
562
|
-
import { Router as
|
|
563
|
-
import { readFile } from "fs/promises";
|
|
564
|
-
import { resolve, dirname } from "path";
|
|
1074
|
+
import { Router as Router5 } from "express";
|
|
1075
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1076
|
+
import { resolve as resolve2, dirname as dirname2 } from "path";
|
|
565
1077
|
import { fileURLToPath } from "url";
|
|
566
|
-
var here =
|
|
1078
|
+
var here = dirname2(fileURLToPath(import.meta.url));
|
|
567
1079
|
var ASSET_CANDIDATES = [
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
1080
|
+
resolve2(here, "browser.js"),
|
|
1081
|
+
resolve2(here, "..", "dist", "browser.js"),
|
|
1082
|
+
resolve2(here, "..", "browser.js")
|
|
571
1083
|
];
|
|
572
1084
|
var STYLE_CANDIDATES = [
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1085
|
+
resolve2(here, "style.css"),
|
|
1086
|
+
resolve2(here, "..", "dist", "style.css"),
|
|
1087
|
+
resolve2(here, "..", "style.css")
|
|
576
1088
|
];
|
|
577
1089
|
async function readFirst(paths) {
|
|
578
1090
|
let lastErr;
|
|
579
1091
|
for (const p of paths) {
|
|
580
1092
|
try {
|
|
581
|
-
const data = await
|
|
1093
|
+
const data = await readFile2(p);
|
|
582
1094
|
return { path: p, data };
|
|
583
1095
|
} catch (err) {
|
|
584
1096
|
lastErr = err;
|
|
@@ -587,7 +1099,7 @@ async function readFirst(paths) {
|
|
|
587
1099
|
throw lastErr instanceof Error ? lastErr : new Error("asset not found");
|
|
588
1100
|
}
|
|
589
1101
|
function makeAssetRouter(basePath) {
|
|
590
|
-
const router =
|
|
1102
|
+
const router = Router5();
|
|
591
1103
|
router.get("/browser.js", async (_req, res) => {
|
|
592
1104
|
try {
|
|
593
1105
|
const { data } = await readFirst(ASSET_CANDIDATES);
|
|
@@ -654,6 +1166,7 @@ function mountCodingTab(app, options) {
|
|
|
654
1166
|
if (!options.githubOAuth.allowedLogins || options.githubOAuth.allowedLogins.length === 0) {
|
|
655
1167
|
console.warn("[coding-tab] WARNING: allowedLogins is empty \u2014 anyone with a GitHub account can sign in.");
|
|
656
1168
|
}
|
|
1169
|
+
const storage = options.storage ?? createDefaultStorage(options.dataDir);
|
|
657
1170
|
const router = express.Router();
|
|
658
1171
|
router.use(express.json({ limit: "1mb" }));
|
|
659
1172
|
const assetRouter = makeAssetRouter(basePath);
|
|
@@ -672,11 +1185,14 @@ function mountCodingTab(app, options) {
|
|
|
672
1185
|
secure,
|
|
673
1186
|
allowedLogins: options.githubOAuth.allowedLogins
|
|
674
1187
|
});
|
|
1188
|
+
const chatRouter = makeChatRouter({ storage, defaultRepo: options.defaultRepo });
|
|
1189
|
+
router.use(requireAuth, chatRouter);
|
|
675
1190
|
const agentRouter = makeAgentRouter({
|
|
676
1191
|
cursorApiKey: options.cursorApiKey,
|
|
677
1192
|
defaultRepo: options.defaultRepo,
|
|
678
1193
|
envName: options.envName,
|
|
679
|
-
skipReviewerRequest: options.skipReviewerRequest
|
|
1194
|
+
skipReviewerRequest: options.skipReviewerRequest,
|
|
1195
|
+
storage
|
|
680
1196
|
});
|
|
681
1197
|
router.use(requireAuth, agentRouter);
|
|
682
1198
|
const githubRouter = makeGitHubRouter();
|
|
@@ -686,6 +1202,8 @@ function mountCodingTab(app, options) {
|
|
|
686
1202
|
return router;
|
|
687
1203
|
}
|
|
688
1204
|
export {
|
|
1205
|
+
FileChatStorage,
|
|
1206
|
+
createDefaultStorage,
|
|
689
1207
|
mountCodingTab
|
|
690
1208
|
};
|
|
691
1209
|
//# sourceMappingURL=server.js.map
|