newpr 0.4.0 → 0.5.1

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.
@@ -1,15 +1,17 @@
1
1
  import type { NewprConfig } from "../../types/config.ts";
2
2
  import type { NewprOutput, ChatMessage, ChatToolCall, ChatSegment } from "../../types/output.ts";
3
3
  import { DEFAULT_CONFIG } from "../../types/config.ts";
4
- import { listSessions, loadSession, loadSinglePatch, savePatchesSidecar, loadCommentsSidecar, saveCommentsSidecar, loadChatSidecar, saveChatSidecar, loadPatchesSidecar, saveCartoonSidecar, loadCartoonSidecar } from "../../history/store.ts";
4
+ import { listSessions, loadSession, loadSinglePatch, savePatchesSidecar, loadCommentsSidecar, saveCommentsSidecar, loadChatSidecar, saveChatSidecar, loadPatchesSidecar, saveCartoonSidecar, loadCartoonSidecar, saveSlidesSidecar, loadSlidesSidecar } from "../../history/store.ts";
5
5
  import type { DiffComment } from "../../types/output.ts";
6
6
  import { fetchPrDiff } from "../../github/fetch-diff.ts";
7
7
  import { fetchPrBody, fetchPrComments } from "../../github/fetch-pr.ts";
8
8
  import { parseDiff } from "../../diff/parser.ts";
9
9
  import { parsePrInput } from "../../github/parse-pr.ts";
10
- import { writeStoredConfig, type StoredConfig } from "../../config/store.ts";
10
+ import { readStoredConfig, writeStoredConfig, type StoredConfig } from "../../config/store.ts";
11
11
  import { startAnalysis, getSession, cancelAnalysis, subscribe, listActiveSessions } from "./session-manager.ts";
12
12
  import { generateCartoon } from "../../llm/cartoon.ts";
13
+ import { generateSlides } from "../../llm/slides.ts";
14
+ import { getPlugin, getAllPlugins } from "../../plugins/registry.ts";
13
15
  import { chatWithTools, type ChatTool, type ChatStreamEvent } from "../../llm/client.ts";
14
16
  import { detectAgents, runAgent } from "../../workspace/agent.ts";
15
17
  import { randomBytes } from "node:crypto";
@@ -70,6 +72,24 @@ export function createRoutes(token: string, config: NewprConfig, options: RouteO
70
72
  return { login: "anonymous" };
71
73
  }
72
74
 
75
+ interface SlideJob {
76
+ status: "running" | "done" | "error";
77
+ message: string;
78
+ current: number;
79
+ total: number;
80
+ plan?: { stylePrompt: string; slides: Array<{ index: number; title: string; contentPrompt: string }> };
81
+ imagePrompts?: Array<{ index: number; prompt: string }>;
82
+ }
83
+ const slideJobs = new Map<string, SlideJob>();
84
+
85
+ interface PluginJob {
86
+ status: "running" | "done" | "error";
87
+ message: string;
88
+ current: number;
89
+ total: number;
90
+ }
91
+ const pluginJobs = new Map<string, PluginJob>();
92
+
73
93
  function buildChatSystemPrompt(data: NewprOutput): string {
74
94
  const fileSummaries = data.files
75
95
  .map((f) => `- ${f.path} (${f.status}, +${f.additions}/-${f.deletions}): ${f.summary}`)
@@ -218,12 +238,44 @@ $$
218
238
  },
219
239
  },
220
240
  },
241
+ {
242
+ type: "function",
243
+ function: {
244
+ name: "create_review_comment",
245
+ description: "Create an inline review comment on a specific line or line range of a file in this PR. The comment will be posted to GitHub. Use this when the user asks to leave a comment, suggestion, or feedback on specific code.",
246
+ parameters: {
247
+ type: "object",
248
+ properties: {
249
+ path: { type: "string", description: "File path (e.g. 'src/auth/session.ts')" },
250
+ line: { type: "number", description: "Line number to comment on (end line if range)" },
251
+ start_line: { type: "number", description: "Start line for multi-line comment (optional)" },
252
+ body: { type: "string", description: "Comment body in markdown" },
253
+ },
254
+ required: ["path", "line", "body"],
255
+ },
256
+ },
257
+ },
258
+ {
259
+ type: "function",
260
+ function: {
261
+ name: "submit_review",
262
+ description: "Submit a PR review with a verdict: APPROVE, REQUEST_CHANGES, or COMMENT. Use when the user asks to approve or request changes on the PR.",
263
+ parameters: {
264
+ type: "object",
265
+ properties: {
266
+ event: { type: "string", enum: ["APPROVE", "REQUEST_CHANGES", "COMMENT"], description: "Review action" },
267
+ body: { type: "string", description: "Optional review summary message" },
268
+ },
269
+ required: ["event"],
270
+ },
271
+ },
272
+ },
221
273
  ];
222
274
  }
223
275
 
224
276
  return {
225
277
  "POST /api/analysis": async (req: Request) => {
226
- const body = await req.json() as { pr: string };
278
+ const body = await req.json() as { pr: string; reuseSessionId?: string };
227
279
  if (!body.pr) return json({ error: "Missing 'pr' field" }, 400);
228
280
 
229
281
  const result = startAnalysis(body.pr, token, config);
@@ -231,6 +283,7 @@ $$
231
283
 
232
284
  return json({
233
285
  sessionId: result.sessionId,
286
+ reuseSessionId: body.reuseSessionId,
234
287
  eventsUrl: `/api/analysis/${result.sessionId}/events`,
235
288
  });
236
289
  },
@@ -471,7 +524,38 @@ $$
471
524
  }
472
525
  },
473
526
 
527
+ "GET /api/models": async () => {
528
+ if (!config.openrouter_api_key) return json([]);
529
+ try {
530
+ const res = await fetch("https://openrouter.ai/api/v1/models", {
531
+ headers: { Authorization: `Bearer ${config.openrouter_api_key}` },
532
+ });
533
+ if (!res.ok) return json([]);
534
+ const data = await res.json() as { data?: Array<{ id: string; name: string; created?: number; context_length?: number }> };
535
+ const models = (data.data ?? [])
536
+ .filter((m) => m.id && !m.id.includes(":free") && !m.id.includes(":extended"))
537
+ .map((m) => ({
538
+ id: m.id,
539
+ name: m.name ?? m.id,
540
+ provider: m.id.split("/")[0] ?? "",
541
+ created: m.created ?? 0,
542
+ contextLength: m.context_length,
543
+ }))
544
+ .sort((a, b) => {
545
+ const provCmp = a.provider.localeCompare(b.provider);
546
+ if (provCmp !== 0) return provCmp;
547
+ return b.created - a.created;
548
+ });
549
+ return json(models);
550
+ } catch {
551
+ return json([]);
552
+ }
553
+ },
554
+
474
555
  "GET /api/config": async () => {
556
+ const stored = await readStoredConfig();
557
+ const pluginList = getAllPlugins().map((p) => ({ id: p.id, name: p.name }));
558
+ const enabledPlugins = stored.enabled_plugins ?? pluginList.map((p) => p.id);
475
559
  return json({
476
560
  model: config.model,
477
561
  agent: config.agent ?? null,
@@ -481,6 +565,8 @@ $$
481
565
  concurrency: config.concurrency,
482
566
  has_api_key: !!config.openrouter_api_key,
483
567
  has_github_token: !!token,
568
+ enabled_plugins: enabledPlugins,
569
+ available_plugins: pluginList,
484
570
  defaults: {
485
571
  model: DEFAULT_CONFIG.model,
486
572
  language: DEFAULT_CONFIG.language,
@@ -528,14 +614,20 @@ $$
528
614
  update.concurrency = body.concurrency;
529
615
  config.concurrency = body.concurrency;
530
616
  }
617
+ if ((body as Record<string, unknown>).enabled_plugins !== undefined) {
618
+ update.enabled_plugins = (body as Record<string, unknown>).enabled_plugins as string[];
619
+ }
531
620
 
532
621
  await writeStoredConfig(update);
533
622
  return json({ ok: true });
534
623
  },
535
624
 
536
- "GET /api/features": () => {
625
+ "GET /api/features": async () => {
537
626
  const { getVersion } = require("../../version.ts");
538
- return json({ cartoon: !!options.cartoon, version: getVersion() });
627
+ const stored = await readStoredConfig();
628
+ const allPluginIds = getAllPlugins().map((p) => p.id);
629
+ const enabledPlugins = stored.enabled_plugins ?? allPluginIds;
630
+ return json({ cartoon: !!options.cartoon, version: getVersion(), enabledPlugins });
539
631
  },
540
632
 
541
633
  "POST /api/review": async (req: Request) => {
@@ -726,6 +818,107 @@ $$
726
818
  return json({ ok: true });
727
819
  },
728
820
 
821
+ "POST /api/sessions/:id/ask-inline": async (req: Request) => {
822
+ const url = new URL(req.url);
823
+ const segments = url.pathname.split("/");
824
+ const sessionId = segments[3]!;
825
+
826
+ if (!config.openrouter_api_key) {
827
+ return json({ error: "OpenRouter API key required" }, 400);
828
+ }
829
+
830
+ const body = await req.json() as { message: string };
831
+ if (!body.message?.trim()) return json({ error: "Missing message" }, 400);
832
+
833
+ const sessionData = await loadSession(sessionId);
834
+ if (!sessionData) return json({ error: "Session not found" }, 404);
835
+
836
+ const systemPrompt = buildChatSystemPrompt(sessionData);
837
+ const apiMessages = [
838
+ { role: "system" as const, content: systemPrompt },
839
+ { role: "user" as const, content: body.message.trim() },
840
+ ];
841
+
842
+ const encoder = new TextEncoder();
843
+ const stream = new ReadableStream({
844
+ async start(controller) {
845
+ const send = (eventType: string, data: string) => {
846
+ controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${data}\n\n`));
847
+ };
848
+ try {
849
+ await chatWithTools(
850
+ { api_key: config.openrouter_api_key, model: config.model, timeout: config.timeout },
851
+ apiMessages as Parameters<typeof chatWithTools>[1],
852
+ buildChatTools(),
853
+ async (name: string, args: Record<string, unknown>): Promise<string> => {
854
+ if (name === "get_file_diff") {
855
+ const filePath = args.path as string;
856
+ if (!filePath) return "Error: path required";
857
+ const patches = await loadPatchesSidecar(sessionId);
858
+ if (patches?.[filePath]) return patches[filePath];
859
+ const patch = await loadSinglePatch(sessionId, filePath);
860
+ if (patch) return patch;
861
+ return `File "${filePath}" not found`;
862
+ }
863
+ if (name === "list_files") {
864
+ return sessionData.files.map((f) => `${f.path} (${f.status}): ${f.summary}`).join("\n");
865
+ }
866
+ return `Tool ${name} not available in inline mode`;
867
+ },
868
+ (event: ChatStreamEvent) => {
869
+ if (event.type === "text") send("text", JSON.stringify({ content: event.content }));
870
+ else if (event.type === "error") send("chat_error", JSON.stringify({ message: event.error }));
871
+ else if (event.type === "done") send("done", JSON.stringify({}));
872
+ },
873
+ );
874
+ send("done", JSON.stringify({}));
875
+ } catch (err) {
876
+ send("chat_error", JSON.stringify({ message: err instanceof Error ? err.message : String(err) }));
877
+ } finally {
878
+ controller.close();
879
+ }
880
+ },
881
+ });
882
+
883
+ return new Response(stream, {
884
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
885
+ });
886
+ },
887
+
888
+ "GET /api/sessions/:id/outdated": async (req: Request) => {
889
+ const url = new URL(req.url);
890
+ const segments = url.pathname.split("/");
891
+ const id = segments[3]!;
892
+ const sessionData = await loadSession(id);
893
+ if (!sessionData) return json({ error: "Session not found" }, 404);
894
+
895
+ const prUrl = sessionData.meta.pr_url;
896
+ const analyzedUpdatedAt = sessionData.meta.pr_updated_at;
897
+ if (!analyzedUpdatedAt) return json({ outdated: false, reason: "no_baseline" });
898
+
899
+ try {
900
+ const pr = parsePrInput(prUrl);
901
+ const res = await fetch(
902
+ `https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}`,
903
+ { headers: ghHeaders },
904
+ );
905
+ if (!res.ok) return json({ outdated: false, reason: "api_error" });
906
+ const data = await res.json() as { updated_at?: string; title?: string; state?: string; merged?: boolean; draft?: boolean };
907
+ const currentUpdatedAt = data.updated_at ?? "";
908
+ const outdated = currentUpdatedAt !== analyzedUpdatedAt;
909
+ return json({
910
+ outdated,
911
+ analyzed_at: sessionData.meta.analyzed_at,
912
+ analyzed_updated_at: analyzedUpdatedAt,
913
+ current_updated_at: currentUpdatedAt,
914
+ current_title: data.title,
915
+ current_state: data.draft ? "draft" : data.merged ? "merged" : data.state === "closed" ? "closed" : "open",
916
+ });
917
+ } catch {
918
+ return json({ outdated: false, reason: "fetch_error" });
919
+ }
920
+ },
921
+
729
922
  "GET /api/sessions/:id/chat": async (req: Request) => {
730
923
  const url = new URL(req.url);
731
924
  const segments = url.pathname.split("/");
@@ -983,6 +1176,67 @@ $$
983
1176
  return `Fetch error: ${err instanceof Error ? err.message : String(err)}`;
984
1177
  }
985
1178
  }
1179
+ case "create_review_comment": {
1180
+ const filePath = args.path as string;
1181
+ const line = args.line as number;
1182
+ const startLine = args.start_line as number | undefined;
1183
+ const body = args.body as string;
1184
+ if (!filePath || !line || !body) return "Error: path, line, and body are required";
1185
+ try {
1186
+ const pr = parsePrInput(sessionData.meta.pr_url);
1187
+ const sha = await fetchHeadSha(pr);
1188
+ if (!sha) return "Error: could not determine HEAD SHA";
1189
+ const ghBody: Record<string, unknown> = {
1190
+ commit_id: sha,
1191
+ path: filePath,
1192
+ line,
1193
+ side: "RIGHT",
1194
+ body,
1195
+ };
1196
+ if (startLine && startLine !== line) {
1197
+ ghBody.start_line = startLine;
1198
+ ghBody.start_side = "RIGHT";
1199
+ }
1200
+ const res = await fetch(
1201
+ `https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}/comments`,
1202
+ { method: "POST", headers: ghHeaders, body: JSON.stringify(ghBody) },
1203
+ );
1204
+ if (!res.ok) {
1205
+ const errBody = await res.text();
1206
+ return `GitHub API error ${res.status}: ${errBody.slice(0, 200)}`;
1207
+ }
1208
+ const data = await res.json() as { id?: number; html_url?: string };
1209
+ return `Comment created on ${filePath}:${startLine && startLine !== line ? `${startLine}-` : ""}${line}. ${data.html_url ?? ""}`;
1210
+ } catch (err) {
1211
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
1212
+ }
1213
+ }
1214
+ case "submit_review": {
1215
+ const event = args.event as string;
1216
+ const body = (args.body as string) ?? "";
1217
+ if (!event || !["APPROVE", "REQUEST_CHANGES", "COMMENT"].includes(event)) {
1218
+ return "Error: event must be APPROVE, REQUEST_CHANGES, or COMMENT";
1219
+ }
1220
+ try {
1221
+ const pr = parsePrInput(sessionData.meta.pr_url);
1222
+ const res = await fetch(
1223
+ `https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}/reviews`,
1224
+ {
1225
+ method: "POST",
1226
+ headers: ghHeaders,
1227
+ body: JSON.stringify({ body, event }),
1228
+ },
1229
+ );
1230
+ if (!res.ok) {
1231
+ const errBody = await res.text();
1232
+ return `GitHub API error ${res.status}: ${errBody.slice(0, 200)}`;
1233
+ }
1234
+ const data = await res.json() as { html_url?: string; state?: string };
1235
+ return `Review submitted: ${data.state ?? event}. ${data.html_url ?? ""}`;
1236
+ } catch (err) {
1237
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
1238
+ }
1239
+ }
986
1240
  default:
987
1241
  return `Unknown tool: ${name}`;
988
1242
  }
@@ -1127,5 +1381,153 @@ $$
1127
1381
  return json({ error: msg }, 500);
1128
1382
  }
1129
1383
  },
1384
+ "GET /api/sessions/:id/slides": async (req: Request) => {
1385
+ const url = new URL(req.url);
1386
+ const segments = url.pathname.split("/");
1387
+ const id = segments[3]!;
1388
+ const deck = await loadSlidesSidecar(id);
1389
+ if (!deck) return json(null);
1390
+ return json(deck);
1391
+ },
1392
+
1393
+ "POST /api/slides": async (req: Request) => {
1394
+ if (!config.openrouter_api_key) return json({ error: "OpenRouter API key required" }, 400);
1395
+
1396
+ const body = await req.json() as { sessionId?: string; language?: string; resume?: boolean };
1397
+ const sessionId = body.sessionId;
1398
+ if (!sessionId) return json({ error: "Missing sessionId" }, 400);
1399
+
1400
+ const data = await loadSession(sessionId);
1401
+ if (!data) return json({ error: "Session not found" }, 404);
1402
+
1403
+ if (slideJobs.has(sessionId) && slideJobs.get(sessionId)!.status === "running") {
1404
+ return json({ status: "already_running" });
1405
+ }
1406
+
1407
+ const existingDeck = body.resume ? await loadSlidesSidecar(sessionId) : null;
1408
+ const job: SlideJob = { status: "running", message: "Planning slide deck...", current: 0, total: 0 };
1409
+ slideJobs.set(sessionId, job);
1410
+
1411
+ (async () => {
1412
+ try {
1413
+ const deck = await generateSlides(
1414
+ config.openrouter_api_key,
1415
+ data,
1416
+ config.model,
1417
+ body.language ?? config.language,
1418
+ (msg, current, total) => {
1419
+ job.message = msg;
1420
+ job.current = current;
1421
+ job.total = total;
1422
+ },
1423
+ existingDeck,
1424
+ (plan, prompts) => {
1425
+ job.plan = plan;
1426
+ job.imagePrompts = prompts;
1427
+ },
1428
+ (partialDeck) => {
1429
+ saveSlidesSidecar(sessionId, partialDeck).catch(() => {});
1430
+ },
1431
+ );
1432
+ await saveSlidesSidecar(sessionId, deck);
1433
+ job.status = "done";
1434
+ job.message = `Generated ${deck.slides.length} slides`;
1435
+ job.total = deck.slides.length;
1436
+ job.current = deck.slides.length;
1437
+ } catch (err) {
1438
+ job.status = "error";
1439
+ job.message = err instanceof Error ? err.message : String(err);
1440
+ }
1441
+ })();
1442
+
1443
+ return json({ status: "started" });
1444
+ },
1445
+
1446
+ "GET /api/slides/status": async (req: Request) => {
1447
+ const url = new URL(req.url);
1448
+ const sessionId = url.searchParams.get("sessionId");
1449
+ if (!sessionId) return json({ error: "Missing sessionId" }, 400);
1450
+ const job = slideJobs.get(sessionId);
1451
+ if (!job) return json({ status: "idle" });
1452
+ return json(job);
1453
+ },
1454
+
1455
+ "GET /api/plugins": () => {
1456
+ const plugins = getAllPlugins().map((p) => ({
1457
+ id: p.id,
1458
+ name: p.name,
1459
+ description: p.description,
1460
+ icon: p.icon,
1461
+ tabLabel: p.tabLabel,
1462
+ }));
1463
+ return json(plugins);
1464
+ },
1465
+
1466
+ "GET /api/plugins/:id/data": async (req: Request) => {
1467
+ const url = new URL(req.url);
1468
+ const segments = url.pathname.split("/");
1469
+ const pluginId = segments[3]!;
1470
+ const sessionId = url.searchParams.get("sessionId");
1471
+ if (!sessionId) return json({ error: "Missing sessionId" }, 400);
1472
+ const plugin = getPlugin(pluginId);
1473
+ if (!plugin) return json({ error: `Unknown plugin: ${pluginId}` }, 404);
1474
+ const data = await plugin.load(sessionId);
1475
+ return json(data);
1476
+ },
1477
+
1478
+ "POST /api/plugins/:id/generate": async (req: Request) => {
1479
+ const url = new URL(req.url);
1480
+ const segments = url.pathname.split("/");
1481
+ const pluginId = segments[3]!;
1482
+ const body = await req.json() as { sessionId?: string; resume?: boolean };
1483
+ const sessionId = body.sessionId;
1484
+ if (!sessionId) return json({ error: "Missing sessionId" }, 400);
1485
+ if (!config.openrouter_api_key) return json({ error: "API key required" }, 400);
1486
+
1487
+ const plugin = getPlugin(pluginId);
1488
+ if (!plugin) return json({ error: `Unknown plugin: ${pluginId}` }, 404);
1489
+
1490
+ const data = await loadSession(sessionId);
1491
+ if (!data) return json({ error: "Session not found" }, 404);
1492
+
1493
+ const jobKey = `${pluginId}:${sessionId}`;
1494
+ if (pluginJobs.has(jobKey) && pluginJobs.get(jobKey)!.status === "running") {
1495
+ return json({ status: "already_running" });
1496
+ }
1497
+
1498
+ const job: PluginJob = { status: "running", message: "Starting...", current: 0, total: 0 };
1499
+ pluginJobs.set(jobKey, job);
1500
+
1501
+ const existingData = body.resume ? await plugin.load(sessionId) : null;
1502
+
1503
+ (async () => {
1504
+ try {
1505
+ const result = await plugin.generate(
1506
+ { apiKey: config.openrouter_api_key, sessionId, data, language: config.language },
1507
+ (event) => { job.message = event.message; job.current = event.current; job.total = event.total; },
1508
+ existingData,
1509
+ );
1510
+ await plugin.save(sessionId, result.data);
1511
+ job.status = "done";
1512
+ job.message = "Complete";
1513
+ } catch (err) {
1514
+ job.status = "error";
1515
+ job.message = err instanceof Error ? err.message : String(err);
1516
+ }
1517
+ })();
1518
+
1519
+ return json({ status: "started" });
1520
+ },
1521
+
1522
+ "GET /api/plugins/:id/status": async (req: Request) => {
1523
+ const url = new URL(req.url);
1524
+ const segments = url.pathname.split("/");
1525
+ const pluginId = segments[3]!;
1526
+ const sessionId = url.searchParams.get("sessionId");
1527
+ if (!sessionId) return json({ error: "Missing sessionId" }, 400);
1528
+ const job = pluginJobs.get(`${pluginId}:${sessionId}`);
1529
+ if (!job) return json({ status: "idle" });
1530
+ return json(job);
1531
+ },
1130
1532
  };
1131
1533
  }
package/src/web/server.ts CHANGED
@@ -70,6 +70,9 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
70
70
  GET: routes["GET /api/config"],
71
71
  PUT: routes["PUT /api/config"],
72
72
  },
73
+ "/api/models": {
74
+ GET: routes["GET /api/models"],
75
+ },
73
76
  },
74
77
  fetch(req) {
75
78
  const url = new URL(req.url);
@@ -102,6 +105,12 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
102
105
  if (path.match(/^\/api\/sessions\/[^/]+\/comments$/) && req.method === "POST") {
103
106
  return routes["POST /api/sessions/:id/comments"](req);
104
107
  }
108
+ if (path.match(/^\/api\/sessions\/[^/]+\/ask-inline$/) && req.method === "POST") {
109
+ return routes["POST /api/sessions/:id/ask-inline"](req);
110
+ }
111
+ if (path.match(/^\/api\/sessions\/[^/]+\/outdated$/) && req.method === "GET") {
112
+ return routes["GET /api/sessions/:id/outdated"](req);
113
+ }
105
114
  if (path.match(/^\/api\/sessions\/[^/]+\/chat\/undo$/) && req.method === "POST") {
106
115
  return routes["POST /api/sessions/:id/chat/undo"](req);
107
116
  }
@@ -132,6 +141,27 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
132
141
  if (path === "/api/cartoon" && req.method === "POST") {
133
142
  return routes["POST /api/cartoon"](req);
134
143
  }
144
+ if (path.match(/^\/api\/sessions\/[^/]+\/slides$/) && req.method === "GET") {
145
+ return routes["GET /api/sessions/:id/slides"](req);
146
+ }
147
+ if (path === "/api/slides" && req.method === "POST") {
148
+ return routes["POST /api/slides"](req);
149
+ }
150
+ if (path === "/api/slides/status" && req.method === "GET") {
151
+ return routes["GET /api/slides/status"](req);
152
+ }
153
+ if (path === "/api/plugins" && req.method === "GET") {
154
+ return routes["GET /api/plugins"]();
155
+ }
156
+ if (path.match(/^\/api\/plugins\/[^/]+\/data$/) && req.method === "GET") {
157
+ return routes["GET /api/plugins/:id/data"](req);
158
+ }
159
+ if (path.match(/^\/api\/plugins\/[^/]+\/generate$/) && req.method === "POST") {
160
+ return routes["POST /api/plugins/:id/generate"](req);
161
+ }
162
+ if (path.match(/^\/api\/plugins\/[^/]+\/status$/) && req.method === "GET") {
163
+ return routes["GET /api/plugins/:id/status"](req);
164
+ }
135
165
  if (path === "/api/review" && req.method === "POST") {
136
166
  return routes["POST /api/review"](req);
137
167
  }