open-research 1.1.1 → 1.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.
@@ -0,0 +1,533 @@
1
+ import {
2
+ TurnManager,
3
+ applyProposedUpdate,
4
+ classifyUpdateRisk,
5
+ clearMemories,
6
+ createProviderFromStoredAuth,
7
+ deleteMemory,
8
+ getAuthStatus,
9
+ importCodexAuth,
10
+ initWorkspace,
11
+ listAvailableSkills,
12
+ loadAllMemories,
13
+ loginWithBrowser,
14
+ runAgentTurn,
15
+ scanWorkspace
16
+ } from "./chunk-32Q63SUH.js";
17
+ import {
18
+ appendSessionEvent,
19
+ listSessions,
20
+ loadSessionHistory
21
+ } from "./chunk-KOBMIIQM.js";
22
+ import {
23
+ clearStoredAuth
24
+ } from "./chunk-3GZIDCV2.js";
25
+ import {
26
+ ensureOpenResearchConfig,
27
+ saveOpenResearchConfig
28
+ } from "./chunk-CRSHN3PQ.js";
29
+ import {
30
+ getAvailableModels
31
+ } from "./chunk-GVEVKDGV.js";
32
+
33
+ // src/server/index.ts
34
+ import { Hono as Hono12 } from "hono";
35
+
36
+ // src/server/session-store.ts
37
+ import { randomUUID } from "crypto";
38
+
39
+ // src/server/bus.ts
40
+ import { EventEmitter } from "events";
41
+ var SessionBus = class {
42
+ emitter = new EventEmitter();
43
+ constructor() {
44
+ this.emitter.setMaxListeners(50);
45
+ }
46
+ emit(event) {
47
+ this.emitter.emit("event", event);
48
+ }
49
+ subscribe(cb) {
50
+ this.emitter.on("event", cb);
51
+ return () => {
52
+ this.emitter.off("event", cb);
53
+ };
54
+ }
55
+ /** Number of active subscribers */
56
+ listenerCount() {
57
+ return this.emitter.listenerCount("event");
58
+ }
59
+ /** Remove all subscribers */
60
+ removeAllListeners() {
61
+ this.emitter.removeAllListeners("event");
62
+ }
63
+ };
64
+
65
+ // src/server/deferred.ts
66
+ function createDeferred() {
67
+ let resolve;
68
+ let reject;
69
+ const promise = new Promise((res, rej) => {
70
+ resolve = res;
71
+ reject = rej;
72
+ });
73
+ return { promise, resolve, reject };
74
+ }
75
+
76
+ // src/server/session-store.ts
77
+ var SessionManager = class {
78
+ sessions = /* @__PURE__ */ new Map();
79
+ create(workspaceDir) {
80
+ const session = {
81
+ id: randomUUID(),
82
+ workspaceDir,
83
+ bus: new SessionBus(),
84
+ history: [],
85
+ activeSkills: [],
86
+ tokenUsage: null,
87
+ pendingUpdates: [],
88
+ pendingQuestions: /* @__PURE__ */ new Map(),
89
+ abortController: null,
90
+ turnSnapshots: [],
91
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
92
+ };
93
+ this.sessions.set(session.id, session);
94
+ return session;
95
+ }
96
+ get(id) {
97
+ return this.sessions.get(id);
98
+ }
99
+ delete(id) {
100
+ const session = this.sessions.get(id);
101
+ if (session) {
102
+ session.bus.removeAllListeners();
103
+ session.abortController?.abort();
104
+ for (const deferred of session.pendingQuestions.values()) {
105
+ deferred.reject(new Error("Session deleted"));
106
+ }
107
+ }
108
+ return this.sessions.delete(id);
109
+ }
110
+ list() {
111
+ return Array.from(this.sessions.values());
112
+ }
113
+ /** Create a QuestionBridge for a session that uses Deferred + bus */
114
+ createQuestionBridge(session) {
115
+ return {
116
+ async createQuestion(question) {
117
+ const deferred = createDeferred();
118
+ session.pendingQuestions.set(question.id, deferred);
119
+ session.bus.emit({ type: "ask_user", question });
120
+ try {
121
+ const answer = await deferred.promise;
122
+ return answer;
123
+ } finally {
124
+ session.pendingQuestions.delete(question.id);
125
+ session.bus.emit({ type: "question_resolved", questionId: question.id });
126
+ }
127
+ }
128
+ };
129
+ }
130
+ /** Create an EventSink that forwards to the session bus */
131
+ createEventSink(session) {
132
+ return (event) => {
133
+ session.bus.emit(event);
134
+ };
135
+ }
136
+ };
137
+
138
+ // src/server/routes/sessions.ts
139
+ import { Hono } from "hono";
140
+ import { streamSSE } from "hono/streaming";
141
+ function createSessionRoutes(sessionManager, options) {
142
+ const app = new Hono();
143
+ app.post("/", async (c) => {
144
+ const { workspaceDir } = await c.req.json();
145
+ const session = sessionManager.create(workspaceDir);
146
+ return c.json({ id: session.id });
147
+ });
148
+ app.get("/", async (c) => {
149
+ const workspaceDir = c.req.query("workspaceDir");
150
+ if (!workspaceDir) return c.json([]);
151
+ const sessions = await listSessions(workspaceDir);
152
+ return c.json(sessions);
153
+ });
154
+ app.get("/:id/history", async (c) => {
155
+ const sessionId = c.req.param("id");
156
+ const session = sessionManager.get(sessionId);
157
+ if (!session) return c.json({ error: "Session not found" }, 404);
158
+ const restored = await loadSessionHistory(session.workspaceDir, sessionId);
159
+ return c.json(restored);
160
+ });
161
+ app.delete("/:id", (c) => {
162
+ const sessionId = c.req.param("id");
163
+ sessionManager.delete(sessionId);
164
+ return c.json({ ok: true });
165
+ });
166
+ app.post("/:id/message", async (c) => {
167
+ const sessionId = c.req.param("id");
168
+ const session = sessionManager.get(sessionId);
169
+ if (!session) return c.json({ error: "Session not found" }, 404);
170
+ const body = await c.req.json();
171
+ const controller = new AbortController();
172
+ session.abortController = controller;
173
+ return streamSSE(c, async (stream) => {
174
+ const sink = (event) => {
175
+ void stream.writeSSE({ data: JSON.stringify(event) });
176
+ };
177
+ const questionBridge = sessionManager.createQuestionBridge(session);
178
+ const eventSink = sessionManager.createEventSink(session);
179
+ try {
180
+ const provider = await createProviderFromStoredAuth({ homeDir: options?.homeDir });
181
+ const workspace = await scanWorkspace(session.workspaceDir);
182
+ const workspaceContext = {
183
+ workspaceDir: session.workspaceDir,
184
+ runId: sessionId,
185
+ workspaceFiles: Object.fromEntries(workspace.files.map((f) => [f.key, f.content])),
186
+ availableKeys: workspace.files.map((f) => f.key),
187
+ fileLabels: Object.fromEntries(workspace.files.map((f) => [f.key, f.label]))
188
+ };
189
+ const result = await runAgentTurn({
190
+ provider,
191
+ message: body.message,
192
+ history: session.history,
193
+ workspace: workspaceContext,
194
+ homeDir: options?.homeDir,
195
+ model: body.model,
196
+ reasoningEffort: body.reasoningEffort,
197
+ activeSkills: session.activeSkills,
198
+ signal: controller.signal,
199
+ questionBridge,
200
+ eventSink: (event) => {
201
+ sink(event);
202
+ eventSink(event);
203
+ },
204
+ onTextDelta: (chunk) => sink({ type: "text_delta", content: chunk }),
205
+ onToolActivity: (activity) => sink({ type: "tool_activity", activity }),
206
+ onSubAgentProgress: (progress) => sink({ type: "subagent_progress", progress }),
207
+ onCompaction: () => sink({ type: "context_compacted", scope: "history", estimatedTokensBefore: 0, estimatedTokensAfter: 0 }),
208
+ onTokenUpdate: (usage) => sink({ type: "token_update", usage }),
209
+ onMemoryExtracted: (memories) => sink({ type: "memory_extracted", memories })
210
+ });
211
+ session.history.push(
212
+ { role: "user", content: body.message },
213
+ { role: "assistant", content: result.text }
214
+ );
215
+ session.activeSkills = result.activeSkills;
216
+ session.tokenUsage = result.tokenUsage;
217
+ for (const update of result.proposedUpdates) {
218
+ const policy = classifyUpdateRisk(update);
219
+ if (body.agentMode === "auto-approve" || policy.policy === "auto-apply") {
220
+ await applyProposedUpdate(session.workspaceDir, update);
221
+ } else {
222
+ session.pendingUpdates.push(update);
223
+ sink({ type: "proposed_update", update });
224
+ }
225
+ }
226
+ await appendSessionEvent(session.workspaceDir, sessionId, {
227
+ type: "chat.turn",
228
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
229
+ payload: {
230
+ prompt: body.message,
231
+ response: result.text,
232
+ proposedUpdates: result.proposedUpdates.map((u) => ({ key: u.key, summary: u.summary }))
233
+ }
234
+ });
235
+ sink({ type: "done", usage: result.tokenUsage });
236
+ } catch (error) {
237
+ sink({ type: "error", message: error instanceof Error ? error.message : String(error) });
238
+ } finally {
239
+ session.abortController = null;
240
+ await stream.writeSSE({ data: "[DONE]" });
241
+ }
242
+ });
243
+ });
244
+ app.post("/:id/abort", (c) => {
245
+ const session = sessionManager.get(c.req.param("id"));
246
+ if (!session) return c.json({ error: "Session not found" }, 404);
247
+ session.abortController?.abort();
248
+ return c.json({ ok: true });
249
+ });
250
+ app.post("/:id/compact", async (c) => {
251
+ const session = sessionManager.get(c.req.param("id"));
252
+ if (!session) return c.json({ error: "Session not found" }, 404);
253
+ const { manualCompact } = await import("./context-manager-2FA5ANQN.js");
254
+ const provider = await createProviderFromStoredAuth({ homeDir: options?.homeDir });
255
+ const compacted = await manualCompact(session.history, provider, { homeDir: options?.homeDir });
256
+ session.history = compacted.messages;
257
+ return c.json({ ok: true });
258
+ });
259
+ return app;
260
+ }
261
+
262
+ // src/server/routes/events.ts
263
+ import { Hono as Hono2 } from "hono";
264
+ import { streamSSE as streamSSE2 } from "hono/streaming";
265
+ function createEventRoutes(sessionManager) {
266
+ const app = new Hono2();
267
+ app.get("/:id", async (c) => {
268
+ const session = sessionManager.get(c.req.param("id"));
269
+ if (!session) return c.json({ error: "Session not found" }, 404);
270
+ return streamSSE2(c, async (stream) => {
271
+ const unsubscribe = session.bus.subscribe((event) => {
272
+ void stream.writeSSE({ data: JSON.stringify(event) });
273
+ });
274
+ const heartbeat = setInterval(() => {
275
+ void stream.writeSSE({ data: '{"type":"heartbeat"}' });
276
+ }, 1e4);
277
+ stream.onAbort(() => {
278
+ clearInterval(heartbeat);
279
+ unsubscribe();
280
+ });
281
+ await new Promise(() => {
282
+ });
283
+ });
284
+ });
285
+ return app;
286
+ }
287
+
288
+ // src/server/routes/questions.ts
289
+ import { Hono as Hono3 } from "hono";
290
+ function createQuestionRoutes(sessionManager) {
291
+ const app = new Hono3();
292
+ app.post("/:id/:qid", async (c) => {
293
+ const session = sessionManager.get(c.req.param("id"));
294
+ if (!session) return c.json({ error: "Session not found" }, 404);
295
+ const questionId = c.req.param("qid");
296
+ const deferred = session.pendingQuestions.get(questionId);
297
+ if (!deferred) return c.json({ error: "No pending question with that ID" }, 404);
298
+ const body = await c.req.json();
299
+ deferred.resolve({ questionId, answer: body.answer, isCustom: body.isCustom });
300
+ return c.json({ ok: true });
301
+ });
302
+ return app;
303
+ }
304
+
305
+ // src/server/routes/updates.ts
306
+ import { Hono as Hono4 } from "hono";
307
+ function createUpdateRoutes(sessionManager) {
308
+ const app = new Hono4();
309
+ app.get("/:id", (c) => {
310
+ const session = sessionManager.get(c.req.param("id"));
311
+ if (!session) return c.json({ error: "Session not found" }, 404);
312
+ return c.json(session.pendingUpdates);
313
+ });
314
+ app.post("/:id/:uid/accept", async (c) => {
315
+ const session = sessionManager.get(c.req.param("id"));
316
+ if (!session) return c.json({ error: "Session not found" }, 404);
317
+ const updateId = c.req.param("uid");
318
+ const index = session.pendingUpdates.findIndex((u) => u.id === updateId);
319
+ if (index === -1) return c.json({ error: "Update not found" }, 404);
320
+ const update = session.pendingUpdates[index];
321
+ await applyProposedUpdate(session.workspaceDir, update);
322
+ session.pendingUpdates.splice(index, 1);
323
+ session.bus.emit({ type: "update_resolved", updateId, action: "accepted" });
324
+ return c.json({ ok: true });
325
+ });
326
+ app.post("/:id/:uid/reject", (c) => {
327
+ const session = sessionManager.get(c.req.param("id"));
328
+ if (!session) return c.json({ error: "Session not found" }, 404);
329
+ const updateId = c.req.param("uid");
330
+ const index = session.pendingUpdates.findIndex((u) => u.id === updateId);
331
+ if (index === -1) return c.json({ error: "Update not found" }, 404);
332
+ session.pendingUpdates.splice(index, 1);
333
+ session.bus.emit({ type: "update_resolved", updateId, action: "rejected" });
334
+ return c.json({ ok: true });
335
+ });
336
+ return app;
337
+ }
338
+
339
+ // src/server/routes/snapshots.ts
340
+ import { Hono as Hono5 } from "hono";
341
+ var turnManagers = /* @__PURE__ */ new Map();
342
+ function getTurnManager(session) {
343
+ let tm = turnManagers.get(session.id);
344
+ if (!tm) {
345
+ tm = new TurnManager(session.workspaceDir);
346
+ turnManagers.set(session.id, tm);
347
+ void tm.init().catch(() => {
348
+ });
349
+ }
350
+ return tm;
351
+ }
352
+ function createSnapshotRoutes(sessionManager) {
353
+ const app = new Hono5();
354
+ app.get("/:id", (c) => {
355
+ const session = sessionManager.get(c.req.param("id"));
356
+ if (!session) return c.json({ error: "Session not found" }, 404);
357
+ const tm = getTurnManager(session);
358
+ return c.json(
359
+ tm.getTurnSnapshots().map((s) => ({
360
+ turnIndex: s.turnIndex,
361
+ patch: s.patch,
362
+ timestamp: s.timestamp
363
+ }))
364
+ );
365
+ });
366
+ app.post("/:id/revert", async (c) => {
367
+ const session = sessionManager.get(c.req.param("id"));
368
+ if (!session) return c.json({ error: "Session not found" }, 404);
369
+ const { afterTurn } = await c.req.json();
370
+ const tm = getTurnManager(session);
371
+ try {
372
+ const result = await tm.revertToTurn(afterTurn);
373
+ session.bus.emit({
374
+ type: "snapshot_reverted",
375
+ revertedTurns: result.revertedTurns,
376
+ filesRestored: result.filesRestored
377
+ });
378
+ return c.json({ revertedTurns: result.revertedTurns, filesRestored: result.filesRestored });
379
+ } catch (error) {
380
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
381
+ }
382
+ });
383
+ app.post("/:id/unrevert", async (c) => {
384
+ const session = sessionManager.get(c.req.param("id"));
385
+ if (!session) return c.json({ error: "Session not found" }, 404);
386
+ const tm = getTurnManager(session);
387
+ try {
388
+ await tm.unrevert();
389
+ return c.json({ ok: true });
390
+ } catch (error) {
391
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
392
+ }
393
+ });
394
+ return app;
395
+ }
396
+
397
+ // src/server/routes/workspace.ts
398
+ import { Hono as Hono6 } from "hono";
399
+ function createWorkspaceRoutes() {
400
+ const app = new Hono6();
401
+ app.post("/init", async (c) => {
402
+ const { workspaceDir } = await c.req.json();
403
+ await initWorkspace({ workspaceDir });
404
+ return c.json({ ok: true });
405
+ });
406
+ app.get("/scan", async (c) => {
407
+ const workspaceDir = c.req.query("workspaceDir") ?? process.cwd();
408
+ const result = await scanWorkspace(workspaceDir);
409
+ return c.json(result.files.map((f) => ({ key: f.key, label: f.label })));
410
+ });
411
+ return app;
412
+ }
413
+
414
+ // src/server/routes/auth.ts
415
+ import { Hono as Hono7 } from "hono";
416
+ function createAuthRoutes(options) {
417
+ const app = new Hono7();
418
+ app.get("/status", async (c) => {
419
+ const status = await getAuthStatus(options);
420
+ return c.json(status);
421
+ });
422
+ app.post("/login", async (c) => {
423
+ const stored = await loginWithBrowser(options);
424
+ return c.json({ ok: true, provider: stored.provider });
425
+ });
426
+ app.post("/login-gemini", async (c) => {
427
+ const { loginWithGemini } = await import("./gemini-login-EYY3EFH4.js");
428
+ const stored = await loginWithGemini(options);
429
+ return c.json({ ok: true, provider: stored.provider });
430
+ });
431
+ app.post("/import-codex", async (c) => {
432
+ const result = await importCodexAuth({ homeDir: options?.homeDir });
433
+ return c.json({ ok: true, accountId: result.accountId });
434
+ });
435
+ app.post("/logout", async (c) => {
436
+ await clearStoredAuth(options);
437
+ return c.json({ ok: true });
438
+ });
439
+ return app;
440
+ }
441
+
442
+ // src/server/routes/config.ts
443
+ import { Hono as Hono8 } from "hono";
444
+ function createConfigRoutes(options) {
445
+ const app = new Hono8();
446
+ app.get("/", async (c) => {
447
+ const config = await ensureOpenResearchConfig(options);
448
+ return c.json(config);
449
+ });
450
+ app.put("/", async (c) => {
451
+ const body = await c.req.json();
452
+ await saveOpenResearchConfig(body, options);
453
+ return c.json({ ok: true });
454
+ });
455
+ app.get("/models", (c) => {
456
+ const models = getAvailableModels();
457
+ return c.json(models);
458
+ });
459
+ return app;
460
+ }
461
+
462
+ // src/server/routes/skills.ts
463
+ import { Hono as Hono9 } from "hono";
464
+ function createSkillRoutes(options) {
465
+ const app = new Hono9();
466
+ app.get("/", async (c) => {
467
+ const skills = await listAvailableSkills(options);
468
+ return c.json(skills);
469
+ });
470
+ return app;
471
+ }
472
+
473
+ // src/server/routes/memory.ts
474
+ import { Hono as Hono10 } from "hono";
475
+ function createMemoryRoutes(options) {
476
+ const app = new Hono10();
477
+ app.get("/", async (c) => {
478
+ const memories = await loadAllMemories(options);
479
+ return c.json(memories);
480
+ });
481
+ app.delete("/clear", async (c) => {
482
+ await clearMemories(options);
483
+ return c.json({ ok: true });
484
+ });
485
+ app.delete("/:id", async (c) => {
486
+ const id = c.req.param("id");
487
+ const deleted = await deleteMemory(id, options);
488
+ return c.json({ ok: deleted });
489
+ });
490
+ return app;
491
+ }
492
+
493
+ // src/server/routes/ontology.ts
494
+ import { Hono as Hono11 } from "hono";
495
+ function createOntologyRoutes() {
496
+ const app = new Hono11();
497
+ app.get("/status", async (c) => {
498
+ const workspaceDir = c.req.query("workspaceDir") ?? process.cwd();
499
+ const [{ loadOntology }, { getOntologyStatus }] = await Promise.all([
500
+ import("./store-LT5EGDOI.js"),
501
+ import("./status-GEEAGLPF.js")
502
+ ]);
503
+ const ontology = await loadOntology(workspaceDir);
504
+ const noteCount = ontology.notes.length;
505
+ const edgeCount = ontology.notes.reduce((sum, n) => sum + n.edges.length, 0);
506
+ const summary = getOntologyStatus(ontology);
507
+ return c.json({ noteCount, edgeCount, summary });
508
+ });
509
+ return app;
510
+ }
511
+
512
+ // src/server/index.ts
513
+ function createApp(options) {
514
+ const app = new Hono12();
515
+ const sessionManager = new SessionManager();
516
+ app.route("/api/sessions", createSessionRoutes(sessionManager, options));
517
+ app.route("/api/events", createEventRoutes(sessionManager));
518
+ app.route("/api/questions", createQuestionRoutes(sessionManager));
519
+ app.route("/api/updates", createUpdateRoutes(sessionManager));
520
+ app.route("/api/snapshots", createSnapshotRoutes(sessionManager));
521
+ app.route("/api/workspace", createWorkspaceRoutes());
522
+ app.route("/api/auth", createAuthRoutes(options));
523
+ app.route("/api/config", createConfigRoutes(options));
524
+ app.route("/api/skills", createSkillRoutes(options));
525
+ app.route("/api/memory", createMemoryRoutes(options));
526
+ app.route("/api/ontology", createOntologyRoutes());
527
+ app.get("/api/health", (c) => c.json({ status: "ok" }));
528
+ return { app, sessionManager };
529
+ }
530
+
531
+ export {
532
+ createApp
533
+ };