@ottocode/server 0.1.264 → 0.1.266

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 (74) hide show
  1. package/package.json +3 -3
  2. package/src/routes/auth/copilot.ts +699 -0
  3. package/src/routes/auth/oauth.ts +578 -0
  4. package/src/routes/auth/onboarding.ts +45 -0
  5. package/src/routes/auth/providers.ts +189 -0
  6. package/src/routes/auth/service.ts +167 -0
  7. package/src/routes/auth/state.ts +23 -0
  8. package/src/routes/auth/status.ts +203 -0
  9. package/src/routes/auth/wallet.ts +229 -0
  10. package/src/routes/auth.ts +12 -2080
  11. package/src/routes/config/models-service.ts +411 -0
  12. package/src/routes/config/models.ts +6 -426
  13. package/src/routes/config/providers-service.ts +237 -0
  14. package/src/routes/config/providers.ts +10 -242
  15. package/src/routes/files/handlers.ts +297 -0
  16. package/src/routes/files/service.ts +313 -0
  17. package/src/routes/files.ts +12 -608
  18. package/src/routes/git/commit-service.ts +207 -0
  19. package/src/routes/git/commit.ts +6 -220
  20. package/src/routes/git/remote-service.ts +116 -0
  21. package/src/routes/git/remote.ts +8 -115
  22. package/src/routes/git/staging-service.ts +111 -0
  23. package/src/routes/git/staging.ts +10 -205
  24. package/src/routes/mcp/auth.ts +338 -0
  25. package/src/routes/mcp/lifecycle.ts +263 -0
  26. package/src/routes/mcp/servers.ts +212 -0
  27. package/src/routes/mcp/service.ts +664 -0
  28. package/src/routes/mcp/state.ts +13 -0
  29. package/src/routes/mcp.ts +6 -1233
  30. package/src/routes/ottorouter/billing.ts +593 -0
  31. package/src/routes/ottorouter/service.ts +92 -0
  32. package/src/routes/ottorouter/topup.ts +301 -0
  33. package/src/routes/ottorouter/wallet.ts +370 -0
  34. package/src/routes/ottorouter.ts +6 -1319
  35. package/src/routes/research/service.ts +339 -0
  36. package/src/routes/research.ts +12 -390
  37. package/src/routes/sessions/crud.ts +563 -0
  38. package/src/routes/sessions/queue.ts +242 -0
  39. package/src/routes/sessions/retry.ts +121 -0
  40. package/src/routes/sessions/service.ts +768 -0
  41. package/src/routes/sessions/share.ts +434 -0
  42. package/src/routes/sessions.ts +8 -1977
  43. package/src/routes/skills/service.ts +221 -0
  44. package/src/routes/skills/spec.ts +309 -0
  45. package/src/routes/skills.ts +31 -909
  46. package/src/routes/terminals/service.ts +326 -0
  47. package/src/routes/terminals.ts +19 -295
  48. package/src/routes/tunnel/service.ts +217 -0
  49. package/src/routes/tunnel.ts +29 -219
  50. package/src/runtime/agent/registry-prompts.ts +147 -0
  51. package/src/runtime/agent/registry.ts +6 -124
  52. package/src/runtime/agent/runner-errors.ts +116 -0
  53. package/src/runtime/agent/runner-reminders.ts +45 -0
  54. package/src/runtime/agent/runner-setup-model.ts +75 -0
  55. package/src/runtime/agent/runner-setup-prompt.ts +185 -0
  56. package/src/runtime/agent/runner-setup-tools.ts +103 -0
  57. package/src/runtime/agent/runner-setup-utils.ts +21 -0
  58. package/src/runtime/agent/runner-setup.ts +54 -288
  59. package/src/runtime/agent/runner-telemetry.ts +112 -0
  60. package/src/runtime/agent/runner-text.ts +108 -0
  61. package/src/runtime/agent/runner-tool-observer.ts +86 -0
  62. package/src/runtime/agent/runner.ts +79 -378
  63. package/src/runtime/ask/service.ts +1 -0
  64. package/src/runtime/provider/custom.ts +73 -0
  65. package/src/runtime/provider/index.ts +6 -85
  66. package/src/runtime/provider/reasoning-builders.ts +280 -0
  67. package/src/runtime/provider/reasoning.ts +68 -264
  68. package/src/runtime/provider/xai.ts +8 -0
  69. package/src/tools/adapter/events.ts +116 -0
  70. package/src/tools/adapter/execution.ts +160 -0
  71. package/src/tools/adapter/pending.ts +37 -0
  72. package/src/tools/adapter/persistence.ts +166 -0
  73. package/src/tools/adapter/results.ts +97 -0
  74. package/src/tools/adapter.ts +124 -451
@@ -1,14 +1,12 @@
1
1
  import type { Hono } from 'hono';
2
- import { loadConfig } from '@ottocode/sdk';
3
- import { getDb } from '@ottocode/database';
4
- import { sessions, messages, messageParts } from '@ottocode/database/schema';
5
- import { desc, eq, and, asc, count } from 'drizzle-orm';
6
- import type { ProviderId } from '@ottocode/sdk';
7
- import { hasConfiguredProvider } from '@ottocode/sdk';
8
- import { serializeError } from '../runtime/errors/api-error.ts';
9
- import { logger } from '@ottocode/sdk';
10
- import { publish } from '../events/bus.ts';
11
2
  import { openApiRoute } from '../openapi/route.ts';
3
+ import {
4
+ createResearchSession,
5
+ deleteResearchSession,
6
+ exportResearchSession,
7
+ injectResearchContext,
8
+ listResearchSessions,
9
+ } from './research/service.ts';
12
10
 
13
11
  export function registerResearchRoutes(app: Hono) {
14
12
  openApiRoute(
@@ -77,59 +75,7 @@ export function registerResearchRoutes(app: Hono) {
77
75
  },
78
76
  },
79
77
  },
80
- async (c) => {
81
- const parentId = c.req.param('parentId');
82
- const projectRoot = c.req.query('project') || process.cwd();
83
- const cfg = await loadConfig(projectRoot);
84
- const db = await getDb(cfg.projectRoot);
85
-
86
- const parentRows = await db
87
- .select()
88
- .from(sessions)
89
- .where(eq(sessions.id, parentId))
90
- .limit(1);
91
-
92
- if (!parentRows.length || parentRows[0].projectPath !== cfg.projectRoot) {
93
- return c.json({ error: 'Parent session not found' }, 404);
94
- }
95
-
96
- const researchRows = await db
97
- .select({
98
- id: sessions.id,
99
- title: sessions.title,
100
- createdAt: sessions.createdAt,
101
- lastActiveAt: sessions.lastActiveAt,
102
- provider: sessions.provider,
103
- model: sessions.model,
104
- totalInputTokens: sessions.totalInputTokens,
105
- totalOutputTokens: sessions.totalOutputTokens,
106
- totalCachedTokens: sessions.totalCachedTokens,
107
- totalCacheCreationTokens: sessions.totalCacheCreationTokens,
108
- })
109
- .from(sessions)
110
- .where(
111
- and(
112
- eq(sessions.parentSessionId, parentId),
113
- eq(sessions.sessionType, 'research'),
114
- ),
115
- )
116
- .orderBy(desc(sessions.lastActiveAt), desc(sessions.createdAt));
117
-
118
- const sessionsWithCounts = await Promise.all(
119
- researchRows.map(async (row) => {
120
- const msgCount = await db
121
- .select({ count: count() })
122
- .from(messages)
123
- .where(eq(messages.sessionId, row.id));
124
- return {
125
- ...row,
126
- messageCount: msgCount[0]?.count ?? 0,
127
- };
128
- }),
129
- );
130
-
131
- return c.json({ sessions: sessionsWithCounts });
132
- },
78
+ listResearchSessions,
133
79
  );
134
80
 
135
81
  openApiRoute(
@@ -219,74 +165,7 @@ export function registerResearchRoutes(app: Hono) {
219
165
  },
220
166
  },
221
167
  },
222
- async (c) => {
223
- const parentId = c.req.param('parentId');
224
- const projectRoot = c.req.query('project') || process.cwd();
225
- const cfg = await loadConfig(projectRoot);
226
- const db = await getDb(cfg.projectRoot);
227
- const body = (await c.req.json().catch(() => ({}))) as Record<
228
- string,
229
- unknown
230
- >;
231
-
232
- const parentRows = await db
233
- .select()
234
- .from(sessions)
235
- .where(eq(sessions.id, parentId))
236
- .limit(1);
237
-
238
- if (!parentRows.length || parentRows[0].projectPath !== cfg.projectRoot) {
239
- return c.json({ error: 'Parent session not found' }, 404);
240
- }
241
-
242
- const parent = parentRows[0];
243
-
244
- const providerCandidate =
245
- typeof body.provider === 'string' ? body.provider : undefined;
246
- const provider: ProviderId = (() => {
247
- if (providerCandidate && hasConfiguredProvider(cfg, providerCandidate))
248
- return providerCandidate;
249
- return parent.provider as ProviderId;
250
- })();
251
-
252
- const modelCandidate =
253
- typeof body.model === 'string' ? body.model.trim() : undefined;
254
- const model = modelCandidate?.length ? modelCandidate : parent.model;
255
-
256
- const id = crypto.randomUUID();
257
- const now = Date.now();
258
- const title = typeof body.title === 'string' ? body.title : null;
259
-
260
- const row = {
261
- id,
262
- title,
263
- agent: 'research',
264
- provider,
265
- model,
266
- projectPath: cfg.projectRoot,
267
- createdAt: now,
268
- lastActiveAt: now,
269
- parentSessionId: parentId,
270
- sessionType: 'research',
271
- totalInputTokens: null,
272
- totalOutputTokens: null,
273
- totalCachedTokens: null,
274
- totalCacheCreationTokens: null,
275
- totalReasoningTokens: null,
276
- totalToolTimeMs: null,
277
- toolCountsJson: null,
278
- };
279
-
280
- try {
281
- await db.insert(sessions).values(row);
282
- publish({ type: 'session.created', sessionId: id, payload: row });
283
- return c.json({ session: row, parentSessionId: parentId }, 201);
284
- } catch (err) {
285
- logger.error('Failed to create research session', err);
286
- const errorResponse = serializeError(err);
287
- return c.json(errorResponse, errorResponse.error.status || 400);
288
- }
289
- },
168
+ createResearchSession,
290
169
  );
291
170
 
292
171
  openApiRoute(
@@ -368,43 +247,7 @@ export function registerResearchRoutes(app: Hono) {
368
247
  },
369
248
  },
370
249
  },
371
- async (c) => {
372
- const researchId = c.req.param('researchId');
373
- const projectRoot = c.req.query('project') || process.cwd();
374
- const cfg = await loadConfig(projectRoot);
375
- const db = await getDb(cfg.projectRoot);
376
-
377
- const rows = await db
378
- .select()
379
- .from(sessions)
380
- .where(eq(sessions.id, researchId))
381
- .limit(1);
382
-
383
- if (!rows.length) {
384
- return c.json({ error: 'Research session not found' }, 404);
385
- }
386
-
387
- const session = rows[0];
388
- if (session.projectPath !== cfg.projectRoot) {
389
- return c.json(
390
- { error: 'Research session not found in this project' },
391
- 404,
392
- );
393
- }
394
-
395
- if (session.sessionType !== 'research') {
396
- return c.json({ error: 'Session is not a research session' }, 400);
397
- }
398
-
399
- await db.delete(sessions).where(eq(sessions.id, researchId));
400
- publish({
401
- type: 'session.deleted',
402
- sessionId: researchId,
403
- payload: { id: researchId },
404
- });
405
-
406
- return c.json({ success: true });
407
- },
250
+ deleteResearchSession,
408
251
  );
409
252
 
410
253
  openApiRoute(
@@ -523,85 +366,7 @@ export function registerResearchRoutes(app: Hono) {
523
366
  },
524
367
  },
525
368
  },
526
- async (c) => {
527
- const parentId = c.req.param('parentId');
528
- const projectRoot = c.req.query('project') || process.cwd();
529
- const cfg = await loadConfig(projectRoot);
530
- const db = await getDb(cfg.projectRoot);
531
- const body = (await c.req.json().catch(() => ({}))) as Record<
532
- string,
533
- unknown
534
- >;
535
-
536
- const researchSessionId =
537
- typeof body.researchSessionId === 'string'
538
- ? body.researchSessionId
539
- : '';
540
- const label =
541
- typeof body.label === 'string' ? body.label : 'Research context';
542
-
543
- if (!researchSessionId) {
544
- return c.json({ error: 'researchSessionId is required' }, 400);
545
- }
546
-
547
- const [parentRows, researchRows] = await Promise.all([
548
- db.select().from(sessions).where(eq(sessions.id, parentId)).limit(1),
549
- db
550
- .select()
551
- .from(sessions)
552
- .where(eq(sessions.id, researchSessionId))
553
- .limit(1),
554
- ]);
555
-
556
- if (!parentRows.length || parentRows[0].projectPath !== cfg.projectRoot) {
557
- return c.json({ error: 'Parent session not found' }, 404);
558
- }
559
-
560
- if (!researchRows.length || researchRows[0].sessionType !== 'research') {
561
- return c.json({ error: 'Research session not found' }, 404);
562
- }
563
-
564
- const _researchSession = researchRows[0];
565
-
566
- const researchMessages = await db
567
- .select({
568
- id: messages.id,
569
- role: messages.role,
570
- createdAt: messages.createdAt,
571
- })
572
- .from(messages)
573
- .where(eq(messages.sessionId, researchSessionId))
574
- .orderBy(asc(messages.createdAt));
575
-
576
- let contextContent = '';
577
- for (const msg of researchMessages) {
578
- if (msg.role === 'user' || msg.role === 'assistant') {
579
- const parts = await db
580
- .select({ type: messageParts.type, content: messageParts.content })
581
- .from(messageParts)
582
- .where(eq(messageParts.messageId, msg.id))
583
- .orderBy(asc(messageParts.index));
584
-
585
- for (const part of parts) {
586
- if (part.type === 'text' && part.content) {
587
- contextContent += `[${msg.role}]: ${part.content}\n\n`;
588
- }
589
- }
590
- }
591
- }
592
-
593
- const injectedContext = `<research-context from="${researchSessionId}" label="${label}" injected-at="${new Date().toISOString()}">\n${contextContent}</research-context>`;
594
-
595
- // Return the content to the client instead of creating a system message
596
- // The client will store it in zustand and include it in the next user message
597
- return c.json({
598
- content: injectedContext,
599
- label,
600
- sessionId: researchSessionId,
601
- parentSessionId: parentId,
602
- tokenEstimate: Math.ceil(injectedContext.length / 4),
603
- });
604
- },
369
+ injectResearchContext,
605
370
  );
606
371
 
607
372
  openApiRoute(
@@ -691,149 +456,6 @@ export function registerResearchRoutes(app: Hono) {
691
456
  },
692
457
  },
693
458
  },
694
- async (c) => {
695
- const researchId = c.req.param('researchId');
696
- const projectRoot = c.req.query('project') || process.cwd();
697
- const cfg = await loadConfig(projectRoot);
698
- const db = await getDb(cfg.projectRoot);
699
- const body = (await c.req.json().catch(() => ({}))) as Record<
700
- string,
701
- unknown
702
- >;
703
-
704
- const researchRows = await db
705
- .select()
706
- .from(sessions)
707
- .where(eq(sessions.id, researchId))
708
- .limit(1);
709
-
710
- if (!researchRows.length || researchRows[0].sessionType !== 'research') {
711
- return c.json({ error: 'Research session not found' }, 404);
712
- }
713
-
714
- const researchSession = researchRows[0];
715
-
716
- if (researchSession.projectPath !== cfg.projectRoot) {
717
- return c.json({ error: 'Research session not in this project' }, 404);
718
- }
719
-
720
- const providerCandidate =
721
- typeof body.provider === 'string' ? body.provider : undefined;
722
- const provider: ProviderId = (() => {
723
- if (providerCandidate && hasConfiguredProvider(cfg, providerCandidate))
724
- return providerCandidate;
725
- return cfg.defaults.provider;
726
- })();
727
-
728
- const modelCandidate =
729
- typeof body.model === 'string' ? body.model.trim() : undefined;
730
- const model = modelCandidate?.length
731
- ? modelCandidate
732
- : cfg.defaults.model;
733
-
734
- const agentCandidate =
735
- typeof body.agent === 'string' ? body.agent.trim() : undefined;
736
- const agent = agentCandidate?.length
737
- ? agentCandidate
738
- : cfg.defaults.agent;
739
-
740
- const researchMessages = await db
741
- .select({
742
- id: messages.id,
743
- role: messages.role,
744
- createdAt: messages.createdAt,
745
- })
746
- .from(messages)
747
- .where(eq(messages.sessionId, researchId))
748
- .orderBy(asc(messages.createdAt));
749
-
750
- let contextContent = '';
751
- for (const msg of researchMessages) {
752
- if (msg.role === 'user' || msg.role === 'assistant') {
753
- const parts = await db
754
- .select({ type: messageParts.type, content: messageParts.content })
755
- .from(messageParts)
756
- .where(eq(messageParts.messageId, msg.id))
757
- .orderBy(asc(messageParts.index));
758
-
759
- for (const part of parts) {
760
- if (part.type === 'text' && part.content) {
761
- contextContent += `[${msg.role}]: ${part.content}\n\n`;
762
- }
763
- }
764
- }
765
- }
766
-
767
- const injectedContext = `<research-context from="${researchId}" exported-at="${new Date().toISOString()}">\n${contextContent}</research-context>`;
768
-
769
- const newSessionId = crypto.randomUUID();
770
- const now = Date.now();
771
-
772
- await db.insert(sessions).values({
773
- id: newSessionId,
774
- title: researchSession.title ? `From: ${researchSession.title}` : null,
775
- agent,
776
- provider,
777
- model,
778
- projectPath: cfg.projectRoot,
779
- createdAt: now,
780
- lastActiveAt: now,
781
- parentSessionId: null,
782
- sessionType: 'main',
783
- totalInputTokens: null,
784
- totalOutputTokens: null,
785
- totalCachedTokens: null,
786
- totalCacheCreationTokens: null,
787
- totalReasoningTokens: null,
788
- totalToolTimeMs: null,
789
- toolCountsJson: null,
790
- });
791
-
792
- const msgId = crypto.randomUUID();
793
- const partId = crypto.randomUUID();
794
-
795
- await db.insert(messages).values({
796
- id: msgId,
797
- sessionId: newSessionId,
798
- role: 'system',
799
- status: 'complete',
800
- agent,
801
- provider,
802
- model,
803
- createdAt: now,
804
- completedAt: now,
805
- });
806
-
807
- await db.insert(messageParts).values({
808
- id: partId,
809
- messageId: msgId,
810
- index: 0,
811
- type: 'text',
812
- content: injectedContext,
813
- agent,
814
- provider,
815
- model,
816
- });
817
-
818
- publish({
819
- type: 'session.created',
820
- sessionId: newSessionId,
821
- payload: { id: newSessionId },
822
- });
823
-
824
- const newSession = await db
825
- .select()
826
- .from(sessions)
827
- .where(eq(sessions.id, newSessionId))
828
- .limit(1);
829
-
830
- return c.json(
831
- {
832
- newSession: newSession[0],
833
- injectedContext,
834
- },
835
- 201,
836
- );
837
- },
459
+ exportResearchSession,
838
460
  );
839
461
  }