@binarycheater/research-sidecar 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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +244 -0
  3. package/README.zh.md +244 -0
  4. package/bin/research-sidecar.mjs +87 -0
  5. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/dist/client/assets/index-BpVgCKdz.css +1 -0
  65. package/dist/client/assets/index-D7VDrQ1Q.js +324 -0
  66. package/dist/client/index.html +13 -0
  67. package/dist-server/lib/context.js +70 -0
  68. package/dist-server/lib/files.js +118 -0
  69. package/dist-server/lib/graphDiscovery.js +69 -0
  70. package/dist-server/lib/openaiProvider.js +89 -0
  71. package/dist-server/lib/prompt.js +30 -0
  72. package/dist-server/lib/researchGraph.js +144 -0
  73. package/dist-server/lib/researchGraphManifest.js +221 -0
  74. package/dist-server/lib/sidebarLayout.js +17 -0
  75. package/dist-server/lib/store.js +190 -0
  76. package/dist-server/lib/tools.js +205 -0
  77. package/dist-server/lib/types.js +1 -0
  78. package/dist-server/lib/workspaceInstall.js +157 -0
  79. package/dist-server/lib/workspaceMeta.js +171 -0
  80. package/dist-server/server/config.js +82 -0
  81. package/dist-server/server/index.js +365 -0
  82. package/package.json +83 -0
  83. package/scripts/codex-sidecar.mjs +325 -0
  84. package/scripts/prepare-package.mjs +14 -0
  85. package/skills/research-graph-sop/SKILL.md +183 -0
  86. package/skills/research-graph-sop/agents/openai.yaml +4 -0
  87. package/skills/scholar-mode/SKILL.md +34 -0
  88. package/skills/scholar-mode/agents/openai.yaml +4 -0
  89. package/skills/sidecar-thinking/SKILL.md +67 -0
  90. package/skills/sidecar-thinking/agents/openai.yaml +4 -0
  91. package/skills/writing-explanatory-reports/SKILL.md +134 -0
  92. package/skills/writing-explanatory-reports/agents/openai.yaml +4 -0
@@ -0,0 +1,365 @@
1
+ import express from "express";
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { buildContextPacket } from "../lib/context.js";
5
+ import { readWorkspaceFile } from "../lib/files.js";
6
+ import { discoverGraphManifests } from "../lib/graphDiscovery.js";
7
+ import { DEFAULT_REVIEW_PROMPT } from "../lib/prompt.js";
8
+ import { loadResearchGraphManifest } from "../lib/researchGraphManifest.js";
9
+ import { streamOpenAIReview } from "../lib/openaiProvider.js";
10
+ import { JsonSessionStore } from "../lib/store.js";
11
+ import { installBundledSkills } from "../lib/workspaceInstall.js";
12
+ import { findInstructionFiles, loadTriggeredSkillFiles, scanWorkspaceSkills, selectSkillTriggers } from "../lib/workspaceMeta.js";
13
+ import { loadConfig, updateGraphManifestPath } from "./config.js";
14
+ const config = loadConfig();
15
+ const store = new JsonSessionStore(config.dataFile, { legacyFile: resolve(process.cwd(), "data", "sessions.json") });
16
+ const app = express();
17
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
18
+ const clientDist = resolve(__dirname, "../../dist/client");
19
+ app.use(express.json({ limit: "4mb" }));
20
+ app.get("/api/config", (_req, res) => {
21
+ res.json(publicConfig());
22
+ });
23
+ app.patch("/api/config", (req, res, next) => {
24
+ try {
25
+ if (typeof req.body?.graphManifestPath === "string") {
26
+ updateGraphManifestPath(config, req.body.graphManifestPath);
27
+ }
28
+ res.json(publicConfig());
29
+ }
30
+ catch (error) {
31
+ next(error);
32
+ }
33
+ });
34
+ app.get("/api/graphs", async (_req, res, next) => {
35
+ try {
36
+ res.json({
37
+ current: config.graphManifestPath,
38
+ candidates: await discoverGraphManifests(config.workspaceRoot, config.graphManifestPath)
39
+ });
40
+ }
41
+ catch (error) {
42
+ next(error);
43
+ }
44
+ });
45
+ app.get("/api/workspace", async (_req, res, next) => {
46
+ try {
47
+ res.json({
48
+ instructionFiles: (await findInstructionFiles(config.workspaceRoot)).map(({ path, bytes }) => ({ path, bytes })),
49
+ skills: await scanWorkspaceSkills(config.workspaceRoot)
50
+ });
51
+ }
52
+ catch (error) {
53
+ next(error);
54
+ }
55
+ });
56
+ app.post("/api/workspace/skills/install", async (req, res, next) => {
57
+ try {
58
+ const result = await installBundledSkills(config.workspaceRoot, { force: Boolean(req.body?.force) });
59
+ res.json(result);
60
+ }
61
+ catch (error) {
62
+ next(error);
63
+ }
64
+ });
65
+ app.get("/api/workspace/file", async (req, res, next) => {
66
+ try {
67
+ const path = String(req.query.path || "");
68
+ const snapshot = await readWorkspaceFile(config.workspaceRoot, path);
69
+ res.json(snapshot);
70
+ }
71
+ catch (error) {
72
+ next(error);
73
+ }
74
+ });
75
+ app.get("/api/workspace/raw", async (req, res, next) => {
76
+ try {
77
+ const path = String(req.query.path || "");
78
+ const snapshot = await readWorkspaceFile(config.workspaceRoot, path);
79
+ res.type(snapshot.mimeType).send(snapshot.content);
80
+ }
81
+ catch (error) {
82
+ next(error);
83
+ }
84
+ });
85
+ app.get(/^\/api\/workspace\/raw-path\/(.+)$/, async (req, res, next) => {
86
+ try {
87
+ const path = decodeURIComponent(String(req.params[0] || ""));
88
+ const snapshot = await readWorkspaceFile(config.workspaceRoot, path);
89
+ res.type(snapshot.mimeType).send(snapshot.content);
90
+ }
91
+ catch (error) {
92
+ next(error);
93
+ }
94
+ });
95
+ app.get("/api/research-graph", async (_req, res, next) => {
96
+ try {
97
+ res.json(await loadResearchGraphManifest(config.workspaceRoot, config.graphManifestPath));
98
+ }
99
+ catch (error) {
100
+ next(error);
101
+ }
102
+ });
103
+ app.get("/api/sessions", async (_req, res, next) => {
104
+ try {
105
+ res.json(await store.listSessions());
106
+ }
107
+ catch (error) {
108
+ next(error);
109
+ }
110
+ });
111
+ app.post("/api/sessions", async (req, res, next) => {
112
+ try {
113
+ const session = await store.createSession({
114
+ title: req.body?.title,
115
+ model: req.body?.model || config.defaultModel,
116
+ apiMode: config.apiMode
117
+ });
118
+ res.status(201).json(session);
119
+ }
120
+ catch (error) {
121
+ next(error);
122
+ }
123
+ });
124
+ app.get("/api/sessions/:id", async (req, res, next) => {
125
+ try {
126
+ const session = await store.getSession(req.params.id);
127
+ if (!session) {
128
+ res.status(404).json({ error: "Session not found." });
129
+ return;
130
+ }
131
+ res.json(session);
132
+ }
133
+ catch (error) {
134
+ next(error);
135
+ }
136
+ });
137
+ app.patch("/api/sessions/:id", async (req, res, next) => {
138
+ try {
139
+ const session = await store.updateSession(req.params.id, {
140
+ title: req.body?.title,
141
+ manualContext: req.body?.manualContext,
142
+ reviewPrompt: req.body?.reviewPrompt || DEFAULT_REVIEW_PROMPT,
143
+ model: req.body?.model,
144
+ apiMode: config.apiMode
145
+ });
146
+ res.json(session);
147
+ }
148
+ catch (error) {
149
+ next(error);
150
+ }
151
+ });
152
+ app.post("/api/sessions/:id/messages/:messageId/edit", async (req, res, next) => {
153
+ try {
154
+ const content = String(req.body?.content || "").trim();
155
+ if (!content) {
156
+ res.status(400).json({ error: "Content is required." });
157
+ return;
158
+ }
159
+ const session = await store.replaceMessageAndTruncate(req.params.id, req.params.messageId, content);
160
+ res.json(session);
161
+ }
162
+ catch (error) {
163
+ next(error);
164
+ }
165
+ });
166
+ app.post("/api/sessions/:id/messages", async (req, res, next) => {
167
+ try {
168
+ const role = req.body?.role === "user" ? "user" : null;
169
+ const content = String(req.body?.content || "").trim();
170
+ if (!role || !content) {
171
+ res.status(400).json({ error: "A user message content is required." });
172
+ return;
173
+ }
174
+ const session = await store.addMessage(req.params.id, {
175
+ role,
176
+ content,
177
+ source: "manual"
178
+ });
179
+ res.status(201).json(session);
180
+ }
181
+ catch (error) {
182
+ next(error);
183
+ }
184
+ });
185
+ app.post("/api/sessions/:id/files", async (req, res, next) => {
186
+ try {
187
+ const snapshot = await readWorkspaceFile(config.workspaceRoot, req.body?.path || "");
188
+ const session = await store.addFile(req.params.id, snapshot);
189
+ res.status(201).json(session);
190
+ }
191
+ catch (error) {
192
+ next(error);
193
+ }
194
+ });
195
+ app.delete("/api/sessions/:id/files/:fileId", async (req, res, next) => {
196
+ try {
197
+ const session = await store.removeFile(req.params.id, req.params.fileId);
198
+ res.json(session);
199
+ }
200
+ catch (error) {
201
+ next(error);
202
+ }
203
+ });
204
+ app.post("/api/sessions/:id/stream", async (req, res, next) => {
205
+ const abortController = new AbortController();
206
+ req.on("aborted", () => abortController.abort());
207
+ res.on("close", () => {
208
+ if (!res.writableEnded) {
209
+ abortController.abort();
210
+ }
211
+ });
212
+ try {
213
+ const session = await store.getSession(req.params.id);
214
+ if (!session) {
215
+ res.status(404).json({ error: "Session not found." });
216
+ return;
217
+ }
218
+ const userMessage = String(req.body?.message || "").trim();
219
+ if (!userMessage) {
220
+ res.status(400).json({ error: "Message is required." });
221
+ return;
222
+ }
223
+ const model = String(req.body?.model || session.model || config.defaultModel);
224
+ const apiMode = config.apiMode;
225
+ const manualContext = String(req.body?.manualContext ?? session.manualContext);
226
+ const reviewPrompt = String(req.body?.reviewPrompt || session.reviewPrompt || DEFAULT_REVIEW_PROMPT);
227
+ const apiKey = config.openaiAPIKey;
228
+ if (!apiKey) {
229
+ res.status(400).json({ error: "OPENAI_API_KEY is not set." });
230
+ return;
231
+ }
232
+ await store.updateSession(session.id, { model, apiMode, manualContext, reviewPrompt });
233
+ const existingMessageId = typeof req.body?.existingMessageId === "string" ? req.body.existingMessageId : null;
234
+ let history = session.messages;
235
+ if (existingMessageId) {
236
+ const existingIndex = session.messages.findIndex((message) => message.id === existingMessageId && message.role === "user");
237
+ if (existingIndex < 0) {
238
+ res.status(400).json({ error: "Existing user message not found." });
239
+ return;
240
+ }
241
+ history = session.messages.slice(0, existingIndex);
242
+ }
243
+ else {
244
+ await store.addMessage(session.id, { role: "user", content: userMessage, source: "manual" });
245
+ }
246
+ res.writeHead(200, {
247
+ "Content-Type": "text/event-stream",
248
+ "Cache-Control": "no-cache, no-transform",
249
+ Connection: "keep-alive"
250
+ });
251
+ let assistantContent = "";
252
+ const toolMessages = [];
253
+ const workspaceSkills = await scanWorkspaceSkills(config.workspaceRoot);
254
+ const skillTriggers = selectSkillTriggers(workspaceSkills, `${manualContext}\n${userMessage}`);
255
+ const loadedSkillFiles = await loadTriggeredSkillFiles(config.workspaceRoot, skillTriggers);
256
+ const instructionFiles = req.body?.includeInstructionFiles ? await findInstructionFiles(config.workspaceRoot) : [];
257
+ if (skillTriggers.length) {
258
+ const skillTrace = skillTriggers
259
+ .map((trigger) => `- \`${trigger.skill.name}\` - ${trigger.confidence}; ${trigger.disclosure}; ${trigger.reason}`)
260
+ .join("\n");
261
+ await store.addMessage(session.id, {
262
+ role: "system",
263
+ content: `Workspace skill routing:\n\n${skillTrace}`,
264
+ source: "system"
265
+ });
266
+ res.write(`data: ${JSON.stringify({
267
+ type: "skills",
268
+ skills: skillTriggers.map((trigger) => trigger.skill),
269
+ triggers: skillTriggers,
270
+ loadedSkills: loadedSkillFiles.map(({ path, bytes }) => ({ path, bytes }))
271
+ })}\n\n`);
272
+ }
273
+ const contextPacket = buildContextPacket({
274
+ reviewPrompt,
275
+ manualContext,
276
+ files: session.files,
277
+ instructionFiles,
278
+ workspaceSkills,
279
+ skillTriggers,
280
+ loadedSkillFiles,
281
+ history,
282
+ userMessage
283
+ });
284
+ await streamOpenAIReview({
285
+ apiKey,
286
+ baseURL: config.openaiBaseURL,
287
+ apiMode,
288
+ model,
289
+ contextPacket,
290
+ workspaceRoot: config.workspaceRoot,
291
+ allowedWriteExtensions: config.allowedWriteExtensions,
292
+ enableTools: Boolean(req.body?.enableTools) && apiMode === "chat",
293
+ signal: abortController.signal,
294
+ onToolCall(name, args) {
295
+ toolMessages.push({
296
+ name,
297
+ content: `Calling \`${name}\` with:\n\n\`\`\`json\n${args}\n\`\`\``
298
+ });
299
+ res.write(`data: ${JSON.stringify({ type: "tool_call", name, args })}\n\n`);
300
+ },
301
+ onToolResult(name, result) {
302
+ toolMessages.push({
303
+ name,
304
+ content: `Result from \`${name}\`:\n\n\`\`\`\n${String(result).slice(0, 4000)}\n\`\`\``
305
+ });
306
+ res.write(`data: ${JSON.stringify({ type: "tool_result", name, result })}\n\n`);
307
+ },
308
+ onDelta(delta) {
309
+ assistantContent += delta;
310
+ res.write(`data: ${JSON.stringify({ type: "delta", delta })}\n\n`);
311
+ }
312
+ });
313
+ for (const toolMessage of toolMessages) {
314
+ await store.addMessage(session.id, {
315
+ role: "tool",
316
+ content: toolMessage.content,
317
+ source: "model",
318
+ toolName: toolMessage.name
319
+ });
320
+ }
321
+ if (assistantContent.trim()) {
322
+ await store.addMessage(session.id, {
323
+ role: "assistant",
324
+ content: assistantContent,
325
+ source: "model",
326
+ model,
327
+ apiMode
328
+ });
329
+ }
330
+ res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
331
+ res.end();
332
+ }
333
+ catch (error) {
334
+ if (!res.headersSent) {
335
+ next(error);
336
+ return;
337
+ }
338
+ res.write(`data: ${JSON.stringify({ type: "error", error: errorMessage(error) })}\n\n`);
339
+ res.end();
340
+ }
341
+ });
342
+ app.use(express.static(clientDist));
343
+ app.get("*path", (_req, res) => {
344
+ res.sendFile(resolve(clientDist, "index.html"));
345
+ });
346
+ app.use((error, _req, res, _next) => {
347
+ res.status(400).json({ error: errorMessage(error) });
348
+ });
349
+ app.listen(config.port, () => {
350
+ console.log(`Research Sidecar: http://localhost:${config.port}`);
351
+ console.log(`Workspace root: ${config.workspaceRoot}`);
352
+ });
353
+ function errorMessage(error) {
354
+ return error instanceof Error ? error.message : "Unknown error.";
355
+ }
356
+ function publicConfig() {
357
+ return {
358
+ workspaceRoot: config.workspaceRoot,
359
+ graphManifestPath: config.graphManifestPath,
360
+ defaultModel: config.defaultModel,
361
+ openaiBaseURL: config.openaiBaseURL || null,
362
+ apiMode: config.apiMode,
363
+ hasOpenAIKey: Boolean(config.openaiAPIKey)
364
+ };
365
+ }
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@binarycheater/research-sidecar",
3
+ "version": "0.1.0",
4
+ "description": "Local research graph sidecar and CLI for Codex-assisted research workflows.",
5
+ "keywords": [
6
+ "research",
7
+ "codex",
8
+ "graph",
9
+ "markdown",
10
+ "sidecar",
11
+ "workspace",
12
+ "skills"
13
+ ],
14
+ "homepage": "https://github.com/BinaryCheater/scholar-mode#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/BinaryCheater/scholar-mode/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/BinaryCheater/scholar-mode.git",
21
+ "directory": "sidecar"
22
+ },
23
+ "license": "MIT",
24
+ "author": "BinaryCheater",
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "private": false,
29
+ "type": "module",
30
+ "bin": {
31
+ "research-sidecar": "bin/research-sidecar.mjs"
32
+ },
33
+ "files": [
34
+ "bin",
35
+ "dist",
36
+ "dist-server",
37
+ "!dist-server/**/*.tsbuildinfo",
38
+ "scripts",
39
+ "skills",
40
+ "package.json",
41
+ "LICENSE",
42
+ "README.md",
43
+ "README.zh.md"
44
+ ],
45
+ "scripts": {
46
+ "dev": "tsx src/server/index.ts",
47
+ "build": "vite build && tsc -p tsconfig.server.json",
48
+ "prepack": "npm run build && node scripts/prepare-package.mjs",
49
+ "start": "node dist-server/server/index.js",
50
+ "codex:install": "node scripts/codex-sidecar.mjs install",
51
+ "codex:ask": "node scripts/codex-sidecar.mjs ask",
52
+ "codex:call": "node scripts/codex-sidecar.mjs call",
53
+ "codex:session": "node scripts/codex-sidecar.mjs",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "typecheck": "tsc --noEmit"
57
+ },
58
+ "dependencies": {
59
+ "@vitejs/plugin-react": "^5.1.1",
60
+ "@xyflow/react": "^12.10.2",
61
+ "express": "^5.2.1",
62
+ "katex": "^0.16.46",
63
+ "openai": "^6.10.0",
64
+ "react": "^19.2.1",
65
+ "react-dom": "^19.2.1",
66
+ "react-markdown": "^10.1.0",
67
+ "rehype-katex": "^7.0.1",
68
+ "remark-gfm": "^4.0.1",
69
+ "remark-math": "^6.0.0",
70
+ "vite": "^7.2.7",
71
+ "yaml": "^2.9.0",
72
+ "zod": "^4.2.0"
73
+ },
74
+ "devDependencies": {
75
+ "@types/express": "^5.0.6",
76
+ "@types/node": "^25.0.0",
77
+ "@types/react": "^19.2.7",
78
+ "@types/react-dom": "^19.2.3",
79
+ "tsx": "^4.21.0",
80
+ "typescript": "^5.9.3",
81
+ "vitest": "^4.0.15"
82
+ }
83
+ }