@tstax/coding-tab 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/README.md +119 -0
- package/dist/browser.js +60 -0
- package/dist/browser.js.map +1 -0
- package/dist/server.cjs +709 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +111 -0
- package/dist/server.d.ts +111 -0
- package/dist/server.js +668 -0
- package/dist/server.js.map +1 -0
- package/dist/style.css +244 -0
- package/package.json +73 -0
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
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/server/index.ts
|
|
31
|
+
var server_exports = {};
|
|
32
|
+
__export(server_exports, {
|
|
33
|
+
mountCodingTab: () => mountCodingTab
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(server_exports);
|
|
36
|
+
|
|
37
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
38
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
39
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
40
|
+
|
|
41
|
+
// src/server/index.ts
|
|
42
|
+
var import_express5 = __toESM(require("express"), 1);
|
|
43
|
+
|
|
44
|
+
// src/server/authRoutes.ts
|
|
45
|
+
var import_express = require("express");
|
|
46
|
+
var import_node_crypto = require("crypto");
|
|
47
|
+
|
|
48
|
+
// src/server/authMiddleware.ts
|
|
49
|
+
var import_iron_session = require("iron-session");
|
|
50
|
+
|
|
51
|
+
// src/server/session.ts
|
|
52
|
+
var SESSION_COOKIE_NAME = "coding_tab_session";
|
|
53
|
+
function buildSessionOptions(password, secure) {
|
|
54
|
+
return {
|
|
55
|
+
password,
|
|
56
|
+
cookieName: SESSION_COOKIE_NAME,
|
|
57
|
+
cookieOptions: {
|
|
58
|
+
httpOnly: true,
|
|
59
|
+
secure,
|
|
60
|
+
sameSite: "lax",
|
|
61
|
+
path: "/",
|
|
62
|
+
maxAge: 60 * 60 * 24 * 30
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/server/authMiddleware.ts
|
|
68
|
+
function getSession(req, res, opts) {
|
|
69
|
+
return (0, import_iron_session.getIronSession)(req, res, buildSessionOptions(opts.sessionPassword, opts.secure));
|
|
70
|
+
}
|
|
71
|
+
function makeRequireAuth(opts) {
|
|
72
|
+
const allowed = new Set(opts.allowedLogins.map((l) => l.toLowerCase()));
|
|
73
|
+
return async function requireAuth(req, res, next) {
|
|
74
|
+
try {
|
|
75
|
+
const session = await getSession(req, res, opts);
|
|
76
|
+
if (!session.githubLogin || !session.accessToken) {
|
|
77
|
+
res.status(401).json({ error: "unauthenticated" });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (allowed.size > 0 && !allowed.has(session.githubLogin.toLowerCase())) {
|
|
81
|
+
res.status(403).json({ error: "not_authorized" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
req.user = {
|
|
85
|
+
githubLogin: session.githubLogin,
|
|
86
|
+
accessToken: session.accessToken,
|
|
87
|
+
avatarUrl: session.avatarUrl
|
|
88
|
+
};
|
|
89
|
+
next();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(`[${SESSION_COOKIE_NAME}] requireAuth failed`, err);
|
|
92
|
+
res.status(500).json({ error: "session_error" });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/server/authRoutes.ts
|
|
98
|
+
var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
|
|
99
|
+
function makeAuthRouter(opts) {
|
|
100
|
+
const router = (0, import_express.Router)();
|
|
101
|
+
const allowed = new Set(opts.oauth.allowedLogins.map((l) => l.toLowerCase()));
|
|
102
|
+
const scopes = opts.oauth.scopes ?? ["repo", "read:user"];
|
|
103
|
+
router.get("/auth/login", async (req, res) => {
|
|
104
|
+
const session = await getSession(req, res, opts);
|
|
105
|
+
const state = (0, import_node_crypto.randomBytes)(24).toString("hex");
|
|
106
|
+
session.oauthState = state;
|
|
107
|
+
session.oauthStateCreatedAt = Date.now();
|
|
108
|
+
await session.save();
|
|
109
|
+
const url = new URL("https://github.com/login/oauth/authorize");
|
|
110
|
+
url.searchParams.set("client_id", opts.oauth.clientId);
|
|
111
|
+
url.searchParams.set("redirect_uri", opts.oauth.callbackUrl);
|
|
112
|
+
url.searchParams.set("scope", scopes.join(" "));
|
|
113
|
+
url.searchParams.set("state", state);
|
|
114
|
+
url.searchParams.set("allow_signup", "false");
|
|
115
|
+
res.redirect(302, url.toString());
|
|
116
|
+
});
|
|
117
|
+
router.get("/auth/callback", async (req, res) => {
|
|
118
|
+
const session = await getSession(req, res, opts);
|
|
119
|
+
const code = typeof req.query.code === "string" ? req.query.code : null;
|
|
120
|
+
const state = typeof req.query.state === "string" ? req.query.state : null;
|
|
121
|
+
const expectedState = session.oauthState;
|
|
122
|
+
const stateAge = session.oauthStateCreatedAt ? Date.now() - session.oauthStateCreatedAt : Number.POSITIVE_INFINITY;
|
|
123
|
+
session.oauthState = void 0;
|
|
124
|
+
session.oauthStateCreatedAt = void 0;
|
|
125
|
+
if (!code || !state || !expectedState || state !== expectedState || stateAge > OAUTH_STATE_TTL_MS) {
|
|
126
|
+
await session.save();
|
|
127
|
+
res.status(400).send("OAuth state mismatch or expired. Try signing in again.");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const tokenResp = await fetch("https://github.com/login/oauth/access_token", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
Accept: "application/json",
|
|
135
|
+
"Content-Type": "application/json"
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
client_id: opts.oauth.clientId,
|
|
139
|
+
client_secret: opts.oauth.clientSecret,
|
|
140
|
+
code,
|
|
141
|
+
redirect_uri: opts.oauth.callbackUrl
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
const tokenJson = await tokenResp.json();
|
|
145
|
+
if (!tokenJson.access_token) {
|
|
146
|
+
await session.save();
|
|
147
|
+
res.status(401).send(`GitHub token exchange failed: ${tokenJson.error_description ?? tokenJson.error ?? "unknown"}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const userResp = await fetch("https://api.github.com/user", {
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${tokenJson.access_token}`,
|
|
153
|
+
Accept: "application/vnd.github+json",
|
|
154
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
if (!userResp.ok) {
|
|
158
|
+
await session.save();
|
|
159
|
+
res.status(401).send(`GitHub user lookup failed: ${userResp.status}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const user = await userResp.json();
|
|
163
|
+
if (allowed.size > 0 && !allowed.has(user.login.toLowerCase())) {
|
|
164
|
+
await session.save();
|
|
165
|
+
res.status(403).send(`@${user.login} is not on the allowlist for this app.`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
session.githubLogin = user.login;
|
|
169
|
+
session.avatarUrl = user.avatar_url;
|
|
170
|
+
session.accessToken = tokenJson.access_token;
|
|
171
|
+
await session.save();
|
|
172
|
+
res.redirect(302, opts.basePath + "/");
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error("[coding-tab] OAuth callback failed", err);
|
|
175
|
+
res.status(500).send("OAuth callback failed. Check server logs.");
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
router.post("/auth/logout", async (req, res) => {
|
|
179
|
+
const session = await getSession(req, res, opts);
|
|
180
|
+
session.destroy();
|
|
181
|
+
res.json({ ok: true });
|
|
182
|
+
});
|
|
183
|
+
router.get("/auth/me", async (req, res) => {
|
|
184
|
+
const session = await getSession(req, res, opts);
|
|
185
|
+
if (!session.githubLogin || !session.accessToken) {
|
|
186
|
+
res.status(401).json({ error: "unauthenticated" });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (allowed.size > 0 && !allowed.has(session.githubLogin.toLowerCase())) {
|
|
190
|
+
res.status(403).json({ error: "not_authorized" });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
res.json({ githubLogin: session.githubLogin, avatarUrl: session.avatarUrl });
|
|
194
|
+
});
|
|
195
|
+
return router;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/server/agentRoutes.ts
|
|
199
|
+
var import_express2 = require("express");
|
|
200
|
+
var import_sdk2 = require("@cursor/sdk");
|
|
201
|
+
|
|
202
|
+
// src/server/models.ts
|
|
203
|
+
var import_sdk = require("@cursor/sdk");
|
|
204
|
+
var SONNET_PATTERN = /sonnet/i;
|
|
205
|
+
var OPUS_PATTERN = /opus/i;
|
|
206
|
+
var cache = null;
|
|
207
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
208
|
+
async function getModelList(apiKey) {
|
|
209
|
+
if (cache && Date.now() - cache.fetchedAt < CACHE_TTL_MS) return cache.models;
|
|
210
|
+
const models = await import_sdk.Cursor.models.list({ apiKey });
|
|
211
|
+
cache = { fetchedAt: Date.now(), models };
|
|
212
|
+
return models;
|
|
213
|
+
}
|
|
214
|
+
function pickBest(models, pattern) {
|
|
215
|
+
const matches = models.filter((m) => pattern.test(m.id) || pattern.test(m.displayName) || (m.aliases ?? []).some((a) => pattern.test(a)));
|
|
216
|
+
if (matches.length === 0) return void 0;
|
|
217
|
+
matches.sort((a, b) => b.id.localeCompare(a.id));
|
|
218
|
+
return matches[0];
|
|
219
|
+
}
|
|
220
|
+
async function listAvailableModels(apiKey) {
|
|
221
|
+
const models = await getModelList(apiKey);
|
|
222
|
+
const out = [];
|
|
223
|
+
const sonnet = pickBest(models, SONNET_PATTERN);
|
|
224
|
+
if (sonnet) out.push({ choice: "sonnet", cursorModelId: sonnet.id, displayName: sonnet.displayName ?? sonnet.id });
|
|
225
|
+
const opus = pickBest(models, OPUS_PATTERN);
|
|
226
|
+
if (opus) out.push({ choice: "opus", cursorModelId: opus.id, displayName: opus.displayName ?? opus.id });
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
async function resolveModel(apiKey, choice) {
|
|
230
|
+
const models = await getModelList(apiKey);
|
|
231
|
+
const pattern = choice === "sonnet" ? SONNET_PATTERN : OPUS_PATTERN;
|
|
232
|
+
const picked = pickBest(models, pattern);
|
|
233
|
+
if (!picked) {
|
|
234
|
+
const available = models.map((m) => `${m.displayName} (${m.id})`).join(", ");
|
|
235
|
+
throw new Error(`No Cursor-routed model matches "${choice}". Available: ${available}`);
|
|
236
|
+
}
|
|
237
|
+
return { cursorModelId: picked.id, displayName: picked.displayName ?? picked.id };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/server/sessions.ts
|
|
241
|
+
var import_node_crypto2 = require("crypto");
|
|
242
|
+
var SESSION_IDLE_TTL_MS = 30 * 60 * 1e3;
|
|
243
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
244
|
+
var sweeperStarted = false;
|
|
245
|
+
function startSweeper() {
|
|
246
|
+
if (sweeperStarted) return;
|
|
247
|
+
sweeperStarted = true;
|
|
248
|
+
setInterval(() => {
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
for (const [id, s] of sessions) {
|
|
251
|
+
if (now - s.lastUsedAt > SESSION_IDLE_TTL_MS) {
|
|
252
|
+
disposeSession(id).catch((e) => console.error("[coding-tab] session sweep dispose failed", e));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}, 5 * 60 * 1e3).unref?.();
|
|
256
|
+
}
|
|
257
|
+
function registerSession(opts) {
|
|
258
|
+
startSweeper();
|
|
259
|
+
const id = (0, import_node_crypto2.randomUUID)();
|
|
260
|
+
const session = {
|
|
261
|
+
id,
|
|
262
|
+
githubLogin: opts.githubLogin,
|
|
263
|
+
agent: opts.agent,
|
|
264
|
+
repoUrl: opts.repoUrl,
|
|
265
|
+
startingRef: opts.startingRef,
|
|
266
|
+
createdAt: Date.now(),
|
|
267
|
+
lastUsedAt: Date.now()
|
|
268
|
+
};
|
|
269
|
+
sessions.set(id, session);
|
|
270
|
+
return session;
|
|
271
|
+
}
|
|
272
|
+
function getSession2(id) {
|
|
273
|
+
const s = sessions.get(id);
|
|
274
|
+
if (s) s.lastUsedAt = Date.now();
|
|
275
|
+
return s;
|
|
276
|
+
}
|
|
277
|
+
async function disposeSession(id) {
|
|
278
|
+
const s = sessions.get(id);
|
|
279
|
+
if (!s) return;
|
|
280
|
+
sessions.delete(id);
|
|
281
|
+
try {
|
|
282
|
+
await s.agent[Symbol.asyncDispose]();
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.error("[coding-tab] dispose failed", err);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/server/agentRoutes.ts
|
|
289
|
+
var PLAN_INSTRUCTION = `You are operating in PLAN MODE.
|
|
290
|
+
|
|
291
|
+
Produce a clear, concise markdown plan for the user's request. Do NOT modify any files. Do NOT call any file-editing or shell tools that mutate state. Read-only exploration tools (Read, Grep, Glob, semantic search) are encouraged.
|
|
292
|
+
|
|
293
|
+
Structure the plan with:
|
|
294
|
+
- Goal (1 line)
|
|
295
|
+
- Approach (3-7 bullet points)
|
|
296
|
+
- Files that will change (bulleted, with absolute or repo-relative paths)
|
|
297
|
+
- Risks / things to verify after implementation
|
|
298
|
+
|
|
299
|
+
When you are done, end your message with a single line: PLAN READY`;
|
|
300
|
+
var EXECUTE_INSTRUCTION = `You are now in AGENT MODE. Implement the plan you produced above. Make all required file changes, run any necessary commands, and prepare the changes for a pull request. When complete, summarize what changed in 3-5 bullets.`;
|
|
301
|
+
function sse(res, event) {
|
|
302
|
+
res.write(`data: ${JSON.stringify(event)}
|
|
303
|
+
|
|
304
|
+
`);
|
|
305
|
+
}
|
|
306
|
+
function parsePrUrl(url) {
|
|
307
|
+
if (!url) return void 0;
|
|
308
|
+
const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
309
|
+
if (!match) return void 0;
|
|
310
|
+
return { url, owner: match[1], repo: match[2], number: Number(match[3]) };
|
|
311
|
+
}
|
|
312
|
+
function modeInstructionPrefix(mode, prompt) {
|
|
313
|
+
return mode === "plan" ? `${PLAN_INSTRUCTION}
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
User request:
|
|
318
|
+
${prompt}` : prompt;
|
|
319
|
+
}
|
|
320
|
+
function setupSseHeaders(res) {
|
|
321
|
+
res.set({
|
|
322
|
+
"Content-Type": "text/event-stream",
|
|
323
|
+
"Cache-Control": "no-cache, no-transform",
|
|
324
|
+
Connection: "keep-alive",
|
|
325
|
+
"X-Accel-Buffering": "no"
|
|
326
|
+
});
|
|
327
|
+
res.flushHeaders?.();
|
|
328
|
+
}
|
|
329
|
+
async function streamRun(res, run) {
|
|
330
|
+
try {
|
|
331
|
+
for await (const message of run.stream()) {
|
|
332
|
+
if (res.writableEnded) break;
|
|
333
|
+
if (message.type === "assistant") {
|
|
334
|
+
for (const block of message.message.content) {
|
|
335
|
+
if (block.type === "text" && block.text) sse(res, { kind: "text", text: block.text });
|
|
336
|
+
else if (block.type === "tool_use") sse(res, { kind: "tool", name: block.name, status: "running", callId: block.id, args: block.input });
|
|
337
|
+
}
|
|
338
|
+
} else if (message.type === "thinking") {
|
|
339
|
+
sse(res, { kind: "thinking", text: message.text });
|
|
340
|
+
} else if (message.type === "tool_call") {
|
|
341
|
+
sse(res, { kind: "tool", name: message.name, status: message.status, callId: message.call_id, args: message.args, result: message.result });
|
|
342
|
+
} else if (message.type === "status") {
|
|
343
|
+
sse(res, { kind: "status", status: message.status, message: message.message });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const result = await run.wait();
|
|
347
|
+
const prUrl = result.git?.branches?.find((b) => b.prUrl)?.prUrl;
|
|
348
|
+
sse(res, {
|
|
349
|
+
kind: "result",
|
|
350
|
+
status: result.status,
|
|
351
|
+
pr: parsePrUrl(prUrl),
|
|
352
|
+
durationMs: result.durationMs
|
|
353
|
+
});
|
|
354
|
+
} catch (err) {
|
|
355
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
356
|
+
const retryable = err instanceof import_sdk2.CursorAgentError ? Boolean(err.isRetryable) : false;
|
|
357
|
+
sse(res, { kind: "error", message, retryable });
|
|
358
|
+
} finally {
|
|
359
|
+
if (!res.writableEnded) res.end();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function makeAgentRouter(opts) {
|
|
363
|
+
const router = (0, import_express2.Router)();
|
|
364
|
+
router.get("/models", async (_req, res) => {
|
|
365
|
+
try {
|
|
366
|
+
const models = await listAvailableModels(opts.cursorApiKey);
|
|
367
|
+
res.json({ models });
|
|
368
|
+
} catch (err) {
|
|
369
|
+
console.error("[coding-tab] /models failed", err);
|
|
370
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "models_failed" });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
router.post("/agent/start", async (req, res) => {
|
|
374
|
+
const user = req.user;
|
|
375
|
+
const { prompt, mode, model, repoUrl, startingRef } = req.body ?? {};
|
|
376
|
+
if (!prompt || mode !== "plan" && mode !== "agent" || model !== "sonnet" && model !== "opus") {
|
|
377
|
+
res.status(400).json({ error: "invalid_request" });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
setupSseHeaders(res);
|
|
381
|
+
let agent;
|
|
382
|
+
try {
|
|
383
|
+
const resolved = await resolveModel(opts.cursorApiKey, model);
|
|
384
|
+
agent = await import_sdk2.Agent.create({
|
|
385
|
+
apiKey: opts.cursorApiKey,
|
|
386
|
+
model: { id: resolved.cursorModelId },
|
|
387
|
+
cloud: {
|
|
388
|
+
repos: [
|
|
389
|
+
{
|
|
390
|
+
url: repoUrl ?? opts.defaultRepo.url,
|
|
391
|
+
startingRef: startingRef ?? opts.defaultRepo.ref
|
|
392
|
+
}
|
|
393
|
+
],
|
|
394
|
+
autoCreatePR: mode === "agent",
|
|
395
|
+
skipReviewerRequest: opts.skipReviewerRequest ?? true,
|
|
396
|
+
envVars: { GITHUB_TOKEN: user.accessToken },
|
|
397
|
+
...opts.envName ? { env: { type: "cloud", name: opts.envName } } : {}
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
const session = registerSession({
|
|
401
|
+
githubLogin: user.githubLogin,
|
|
402
|
+
agent,
|
|
403
|
+
repoUrl: repoUrl ?? opts.defaultRepo.url,
|
|
404
|
+
startingRef: startingRef ?? opts.defaultRepo.ref
|
|
405
|
+
});
|
|
406
|
+
const run = await agent.send(modeInstructionPrefix(mode, prompt));
|
|
407
|
+
sse(res, { kind: "ready", sessionId: session.id, agentId: agent.agentId, runId: run.id });
|
|
408
|
+
console.log(`[coding-tab] start agent=${agent.agentId} run=${run.id} session=${session.id} login=${user.githubLogin}`);
|
|
409
|
+
await streamRun(res, run);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
412
|
+
const retryable = err instanceof import_sdk2.CursorAgentError ? Boolean(err.isRetryable) : false;
|
|
413
|
+
console.error("[coding-tab] /agent/start failed", err);
|
|
414
|
+
sse(res, { kind: "error", message, retryable });
|
|
415
|
+
if (!res.writableEnded) res.end();
|
|
416
|
+
if (agent) await agent[Symbol.asyncDispose]().catch(() => {
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
req.on("close", () => {
|
|
420
|
+
if (!res.writableEnded) res.end();
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
router.post("/agent/send", async (req, res) => {
|
|
424
|
+
const { sessionId, prompt, mode } = req.body ?? {};
|
|
425
|
+
if (!sessionId || !prompt || mode !== "plan" && mode !== "agent") {
|
|
426
|
+
res.status(400).json({ error: "invalid_request" });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const session = getSession2(sessionId);
|
|
430
|
+
if (!session) {
|
|
431
|
+
res.status(404).json({ error: "session_not_found" });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
setupSseHeaders(res);
|
|
435
|
+
try {
|
|
436
|
+
const run = await session.agent.send(modeInstructionPrefix(mode, prompt));
|
|
437
|
+
sse(res, { kind: "ready", sessionId: session.id, agentId: session.agent.agentId, runId: run.id });
|
|
438
|
+
console.log(`[coding-tab] send agent=${session.agent.agentId} run=${run.id} session=${session.id}`);
|
|
439
|
+
await streamRun(res, run);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
442
|
+
console.error("[coding-tab] /agent/send failed", err);
|
|
443
|
+
sse(res, { kind: "error", message });
|
|
444
|
+
if (!res.writableEnded) res.end();
|
|
445
|
+
}
|
|
446
|
+
req.on("close", () => {
|
|
447
|
+
if (!res.writableEnded) res.end();
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
router.post("/agent/execute", async (req, res) => {
|
|
451
|
+
const { sessionId } = req.body ?? {};
|
|
452
|
+
if (!sessionId) {
|
|
453
|
+
res.status(400).json({ error: "invalid_request" });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const session = getSession2(sessionId);
|
|
457
|
+
if (!session) {
|
|
458
|
+
res.status(404).json({ error: "session_not_found" });
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
setupSseHeaders(res);
|
|
462
|
+
try {
|
|
463
|
+
const run = await session.agent.send(EXECUTE_INSTRUCTION);
|
|
464
|
+
sse(res, { kind: "ready", sessionId: session.id, agentId: session.agent.agentId, runId: run.id });
|
|
465
|
+
console.log(`[coding-tab] execute agent=${session.agent.agentId} run=${run.id} session=${session.id}`);
|
|
466
|
+
await streamRun(res, run);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
469
|
+
console.error("[coding-tab] /agent/execute failed", err);
|
|
470
|
+
sse(res, { kind: "error", message });
|
|
471
|
+
if (!res.writableEnded) res.end();
|
|
472
|
+
}
|
|
473
|
+
req.on("close", () => {
|
|
474
|
+
if (!res.writableEnded) res.end();
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
router.post("/agent/cancel", async (req, res) => {
|
|
478
|
+
const { sessionId, runId } = req.body ?? {};
|
|
479
|
+
if (!sessionId || !runId) {
|
|
480
|
+
res.status(400).json({ error: "invalid_request" });
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const session = getSession2(sessionId);
|
|
484
|
+
if (!session) {
|
|
485
|
+
res.status(404).json({ error: "session_not_found" });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
await import_sdk2.Agent.cancelRun(runId, { runtime: "cloud", agentId: session.agent.agentId, apiKey: opts.cursorApiKey });
|
|
490
|
+
res.json({ ok: true });
|
|
491
|
+
} catch (err) {
|
|
492
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
493
|
+
console.error("[coding-tab] /agent/cancel failed", err);
|
|
494
|
+
res.status(500).json({ error: message });
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
router.post("/agent/dispose", async (req, res) => {
|
|
498
|
+
const { sessionId } = req.body ?? {};
|
|
499
|
+
if (!sessionId) {
|
|
500
|
+
res.status(400).json({ error: "invalid_request" });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
await disposeSession(sessionId);
|
|
504
|
+
res.json({ ok: true });
|
|
505
|
+
});
|
|
506
|
+
return router;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/server/githubRoutes.ts
|
|
510
|
+
var import_express3 = require("express");
|
|
511
|
+
var import_rest = require("@octokit/rest");
|
|
512
|
+
function parsePrUrl2(prUrl) {
|
|
513
|
+
const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
514
|
+
if (!match) return null;
|
|
515
|
+
return { owner: match[1], repo: match[2], number: Number(match[3]) };
|
|
516
|
+
}
|
|
517
|
+
function makeGitHubRouter() {
|
|
518
|
+
const router = (0, import_express3.Router)();
|
|
519
|
+
router.get("/pr/status", async (req, res) => {
|
|
520
|
+
const prUrl = typeof req.query.prUrl === "string" ? req.query.prUrl : null;
|
|
521
|
+
if (!prUrl) {
|
|
522
|
+
res.status(400).json({ error: "missing_prUrl" });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const parsed = parsePrUrl2(prUrl);
|
|
526
|
+
if (!parsed) {
|
|
527
|
+
res.status(400).json({ error: "invalid_prUrl" });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const user = req.user;
|
|
531
|
+
const octokit = new import_rest.Octokit({ auth: user.accessToken });
|
|
532
|
+
try {
|
|
533
|
+
const pr = await octokit.pulls.get({ owner: parsed.owner, repo: parsed.repo, pull_number: parsed.number });
|
|
534
|
+
const info = {
|
|
535
|
+
url: pr.data.html_url,
|
|
536
|
+
owner: parsed.owner,
|
|
537
|
+
repo: parsed.repo,
|
|
538
|
+
number: parsed.number,
|
|
539
|
+
branch: pr.data.head?.ref,
|
|
540
|
+
state: pr.data.merged ? "merged" : pr.data.state,
|
|
541
|
+
title: pr.data.title,
|
|
542
|
+
body: pr.data.body ?? void 0
|
|
543
|
+
};
|
|
544
|
+
res.json({ pr: info, mergeable: pr.data.mergeable, mergeable_state: pr.data.mergeable_state });
|
|
545
|
+
} catch (err) {
|
|
546
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
547
|
+
res.status(500).json({ error: message });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
router.post("/pr/merge", async (req, res) => {
|
|
551
|
+
const body = req.body ?? {};
|
|
552
|
+
if (!body.prUrl) {
|
|
553
|
+
res.status(400).json({ error: "missing_prUrl" });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const parsed = parsePrUrl2(body.prUrl);
|
|
557
|
+
if (!parsed) {
|
|
558
|
+
res.status(400).json({ error: "invalid_prUrl" });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const user = req.user;
|
|
562
|
+
const octokit = new import_rest.Octokit({ auth: user.accessToken });
|
|
563
|
+
try {
|
|
564
|
+
const merge = await octokit.pulls.merge({
|
|
565
|
+
owner: parsed.owner,
|
|
566
|
+
repo: parsed.repo,
|
|
567
|
+
pull_number: parsed.number,
|
|
568
|
+
merge_method: body.mergeMethod ?? "squash"
|
|
569
|
+
});
|
|
570
|
+
res.json({ sha: merge.data.sha, merged: merge.data.merged });
|
|
571
|
+
} catch (err) {
|
|
572
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
573
|
+
console.error("[coding-tab] /pr/merge failed", err);
|
|
574
|
+
res.status(500).json({ error: message });
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
return router;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/server/staticAssets.ts
|
|
581
|
+
var import_express4 = require("express");
|
|
582
|
+
var import_promises = require("fs/promises");
|
|
583
|
+
var import_node_path = require("path");
|
|
584
|
+
var import_node_url = require("url");
|
|
585
|
+
var here = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
586
|
+
var ASSET_CANDIDATES = [
|
|
587
|
+
(0, import_node_path.resolve)(here, "browser.js"),
|
|
588
|
+
(0, import_node_path.resolve)(here, "..", "dist", "browser.js"),
|
|
589
|
+
(0, import_node_path.resolve)(here, "..", "browser.js")
|
|
590
|
+
];
|
|
591
|
+
var STYLE_CANDIDATES = [
|
|
592
|
+
(0, import_node_path.resolve)(here, "style.css"),
|
|
593
|
+
(0, import_node_path.resolve)(here, "..", "dist", "style.css"),
|
|
594
|
+
(0, import_node_path.resolve)(here, "..", "style.css")
|
|
595
|
+
];
|
|
596
|
+
async function readFirst(paths) {
|
|
597
|
+
let lastErr;
|
|
598
|
+
for (const p of paths) {
|
|
599
|
+
try {
|
|
600
|
+
const data = await (0, import_promises.readFile)(p);
|
|
601
|
+
return { path: p, data };
|
|
602
|
+
} catch (err) {
|
|
603
|
+
lastErr = err;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
throw lastErr instanceof Error ? lastErr : new Error("asset not found");
|
|
607
|
+
}
|
|
608
|
+
function makeAssetRouter(basePath) {
|
|
609
|
+
const router = (0, import_express4.Router)();
|
|
610
|
+
router.get("/browser.js", async (_req, res) => {
|
|
611
|
+
try {
|
|
612
|
+
const { data } = await readFirst(ASSET_CANDIDATES);
|
|
613
|
+
res.set("Content-Type", "application/javascript; charset=utf-8");
|
|
614
|
+
res.set("Cache-Control", "public, max-age=300");
|
|
615
|
+
res.send(data);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
res.status(500).send("// coding-tab browser bundle missing \u2014 did you run `npm run build`?");
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
router.get("/style.css", async (_req, res) => {
|
|
621
|
+
try {
|
|
622
|
+
const { data } = await readFirst(STYLE_CANDIDATES);
|
|
623
|
+
res.set("Content-Type", "text/css; charset=utf-8");
|
|
624
|
+
res.set("Cache-Control", "public, max-age=300");
|
|
625
|
+
res.send(data);
|
|
626
|
+
} catch {
|
|
627
|
+
res.status(404).send("/* style.css missing */");
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
router.get("/", async (_req, res) => {
|
|
631
|
+
const html = renderHostHtml(basePath);
|
|
632
|
+
res.set("Content-Type", "text/html; charset=utf-8");
|
|
633
|
+
res.send(html);
|
|
634
|
+
});
|
|
635
|
+
return router;
|
|
636
|
+
}
|
|
637
|
+
function renderHostHtml(basePath) {
|
|
638
|
+
return `<!doctype html>
|
|
639
|
+
<html lang="en">
|
|
640
|
+
<head>
|
|
641
|
+
<meta charset="utf-8" />
|
|
642
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
643
|
+
<title>Coding Tab</title>
|
|
644
|
+
<link rel="stylesheet" href="${basePath}/style.css" />
|
|
645
|
+
</head>
|
|
646
|
+
<body>
|
|
647
|
+
<div id="coding-tab-root"></div>
|
|
648
|
+
<script src="${basePath}/browser.js"></script>
|
|
649
|
+
<script>
|
|
650
|
+
window.CodingTab.mountCodingTab(
|
|
651
|
+
document.getElementById("coding-tab-root"),
|
|
652
|
+
{ apiBase: ${JSON.stringify(basePath)} },
|
|
653
|
+
);
|
|
654
|
+
</script>
|
|
655
|
+
</body>
|
|
656
|
+
</html>`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/server/index.ts
|
|
660
|
+
function mountCodingTab(app, options) {
|
|
661
|
+
const basePath = (options.basePath ?? "/coding-tab").replace(/\/$/, "");
|
|
662
|
+
const secure = options.secure ?? process.env.NODE_ENV === "production";
|
|
663
|
+
if (!options.cursorApiKey) throw new Error("[coding-tab] cursorApiKey is required");
|
|
664
|
+
if (!options.sessionPassword || options.sessionPassword.length < 32) {
|
|
665
|
+
throw new Error("[coding-tab] sessionPassword must be at least 32 characters");
|
|
666
|
+
}
|
|
667
|
+
if (!options.githubOAuth?.clientId || !options.githubOAuth?.clientSecret) {
|
|
668
|
+
throw new Error("[coding-tab] githubOAuth.clientId and clientSecret are required");
|
|
669
|
+
}
|
|
670
|
+
if (!options.githubOAuth.callbackUrl) {
|
|
671
|
+
throw new Error("[coding-tab] githubOAuth.callbackUrl is required");
|
|
672
|
+
}
|
|
673
|
+
if (!options.githubOAuth.allowedLogins || options.githubOAuth.allowedLogins.length === 0) {
|
|
674
|
+
console.warn("[coding-tab] WARNING: allowedLogins is empty \u2014 anyone with a GitHub account can sign in.");
|
|
675
|
+
}
|
|
676
|
+
const router = import_express5.default.Router();
|
|
677
|
+
router.use(import_express5.default.json({ limit: "1mb" }));
|
|
678
|
+
const assetRouter = makeAssetRouter(basePath);
|
|
679
|
+
router.use(assetRouter);
|
|
680
|
+
const authRouter = makeAuthRouter({
|
|
681
|
+
oauth: options.githubOAuth,
|
|
682
|
+
sessionPassword: options.sessionPassword,
|
|
683
|
+
secure,
|
|
684
|
+
basePath
|
|
685
|
+
});
|
|
686
|
+
router.use(authRouter);
|
|
687
|
+
const requireAuth = makeRequireAuth({
|
|
688
|
+
sessionPassword: options.sessionPassword,
|
|
689
|
+
secure,
|
|
690
|
+
allowedLogins: options.githubOAuth.allowedLogins
|
|
691
|
+
});
|
|
692
|
+
const agentRouter = makeAgentRouter({
|
|
693
|
+
cursorApiKey: options.cursorApiKey,
|
|
694
|
+
defaultRepo: options.defaultRepo,
|
|
695
|
+
envName: options.envName,
|
|
696
|
+
skipReviewerRequest: options.skipReviewerRequest
|
|
697
|
+
});
|
|
698
|
+
router.use(requireAuth, agentRouter);
|
|
699
|
+
const githubRouter = makeGitHubRouter();
|
|
700
|
+
router.use(requireAuth, githubRouter);
|
|
701
|
+
app.use(basePath, router);
|
|
702
|
+
console.log(`[coding-tab] mounted at ${basePath}`);
|
|
703
|
+
return router;
|
|
704
|
+
}
|
|
705
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
706
|
+
0 && (module.exports = {
|
|
707
|
+
mountCodingTab
|
|
708
|
+
});
|
|
709
|
+
//# sourceMappingURL=server.cjs.map
|