@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
@@ -0,0 +1,563 @@
1
+ import { sessions } from '@ottocode/database/schema';
2
+ import { hasConfiguredProvider, logger, type ProviderId } from '@ottocode/sdk';
3
+ import { and, desc, eq, ne } from 'drizzle-orm';
4
+ import type { Hono } from 'hono';
5
+ import { openApiRoute } from '../../openapi/route.ts';
6
+ import { serializeError } from '../../runtime/errors/api-error.ts';
7
+ import { resolveAgentConfig } from '../../runtime/agent/registry.ts';
8
+ import { createSession as createSessionRow } from '../../runtime/session/manager.ts';
9
+ import {
10
+ buildSessionPreferenceUpdates,
11
+ deleteSessionMessagesAndParts,
12
+ findSessionById,
13
+ loadProjectDb,
14
+ normalizeSessionRow,
15
+ } from './service.ts';
16
+
17
+ export function registerSessionCrudRoutes(app: Hono) {
18
+ // List sessions
19
+ openApiRoute(
20
+ app,
21
+ {
22
+ method: 'get',
23
+ path: '/v1/sessions',
24
+ tags: ['sessions'],
25
+ operationId: 'listSessions',
26
+ summary: 'List sessions',
27
+ parameters: [
28
+ {
29
+ in: 'query',
30
+ name: 'project',
31
+ required: false,
32
+ schema: {
33
+ type: 'string',
34
+ },
35
+ description:
36
+ 'Project root override (defaults to current working directory).',
37
+ },
38
+ {
39
+ in: 'query',
40
+ name: 'limit',
41
+ schema: {
42
+ type: 'integer',
43
+ default: 50,
44
+ minimum: 1,
45
+ maximum: 200,
46
+ },
47
+ description: 'Maximum number of sessions to return',
48
+ },
49
+ {
50
+ in: 'query',
51
+ name: 'offset',
52
+ schema: {
53
+ type: 'integer',
54
+ default: 0,
55
+ minimum: 0,
56
+ },
57
+ description: 'Offset for pagination',
58
+ },
59
+ ],
60
+ responses: {
61
+ '200': {
62
+ description: 'OK',
63
+ content: {
64
+ 'application/json': {
65
+ schema: {
66
+ type: 'object',
67
+ properties: {
68
+ items: {
69
+ type: 'array',
70
+ items: {
71
+ $ref: '#/components/schemas/Session',
72
+ },
73
+ },
74
+ hasMore: {
75
+ type: 'boolean',
76
+ },
77
+ nextOffset: {
78
+ type: 'integer',
79
+ nullable: true,
80
+ },
81
+ },
82
+ required: ['items', 'hasMore', 'nextOffset'],
83
+ },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ async (c) => {
90
+ const projectRoot = c.req.query('project') || process.cwd();
91
+ const limit = Math.min(
92
+ Math.max(parseInt(c.req.query('limit') || '50', 10) || 50, 1),
93
+ 200,
94
+ );
95
+ const offset = Math.max(
96
+ parseInt(c.req.query('offset') || '0', 10) || 0,
97
+ 0,
98
+ );
99
+ const { cfg, db } = await loadProjectDb(projectRoot);
100
+ // Only return sessions for this project, excluding research sessions
101
+ const rows = await db
102
+ .select()
103
+ .from(sessions)
104
+ .where(
105
+ and(
106
+ eq(sessions.projectPath, cfg.projectRoot),
107
+ ne(sessions.sessionType, 'research'),
108
+ ),
109
+ )
110
+ .orderBy(desc(sessions.lastActiveAt), desc(sessions.createdAt))
111
+ .limit(limit + 1)
112
+ .offset(offset);
113
+ const hasMore = rows.length > limit;
114
+ const page = hasMore ? rows.slice(0, limit) : rows;
115
+ const normalized = page.map((r) =>
116
+ normalizeSessionRow(r, { includeRunning: true }),
117
+ );
118
+ return c.json({
119
+ items: normalized,
120
+ hasMore,
121
+ nextOffset: hasMore ? offset + limit : null,
122
+ });
123
+ },
124
+ );
125
+
126
+ // Create session
127
+ openApiRoute(
128
+ app,
129
+ {
130
+ method: 'post',
131
+ path: '/v1/sessions',
132
+ tags: ['sessions'],
133
+ operationId: 'createSession',
134
+ summary: 'Create a new session',
135
+ parameters: [
136
+ {
137
+ in: 'query',
138
+ name: 'project',
139
+ required: false,
140
+ schema: {
141
+ type: 'string',
142
+ },
143
+ description:
144
+ 'Project root override (defaults to current working directory).',
145
+ },
146
+ ],
147
+ requestBody: {
148
+ required: false,
149
+ content: {
150
+ 'application/json': {
151
+ schema: {
152
+ type: 'object',
153
+ properties: {
154
+ title: {
155
+ type: 'string',
156
+ nullable: true,
157
+ },
158
+ agent: {
159
+ type: 'string',
160
+ },
161
+ provider: {
162
+ $ref: '#/components/schemas/Provider',
163
+ },
164
+ model: {
165
+ type: 'string',
166
+ },
167
+ },
168
+ },
169
+ },
170
+ },
171
+ },
172
+ responses: {
173
+ '201': {
174
+ description: 'Created',
175
+ content: {
176
+ 'application/json': {
177
+ schema: {
178
+ $ref: '#/components/schemas/Session',
179
+ },
180
+ },
181
+ },
182
+ },
183
+ '400': {
184
+ description: 'Bad Request',
185
+ content: {
186
+ 'application/json': {
187
+ schema: {
188
+ type: 'object',
189
+ properties: {
190
+ error: {
191
+ type: 'string',
192
+ },
193
+ },
194
+ required: ['error'],
195
+ },
196
+ },
197
+ },
198
+ },
199
+ },
200
+ },
201
+ async (c) => {
202
+ const projectRoot = c.req.query('project') || process.cwd();
203
+ const { cfg, db } = await loadProjectDb(projectRoot);
204
+ const body = (await c.req.json().catch(() => ({}))) as Record<
205
+ string,
206
+ unknown
207
+ >;
208
+ const agent = (body.agent as string | undefined) ?? cfg.defaults.agent;
209
+ const agentCfg = await resolveAgentConfig(cfg.projectRoot, agent);
210
+ const providerCandidate =
211
+ typeof body.provider === 'string' ? body.provider : undefined;
212
+ const provider: ProviderId = (() => {
213
+ if (providerCandidate && hasConfiguredProvider(cfg, providerCandidate))
214
+ return providerCandidate;
215
+ if (hasConfiguredProvider(cfg, agentCfg.provider))
216
+ return agentCfg.provider;
217
+ return cfg.defaults.provider;
218
+ })();
219
+ const modelCandidate =
220
+ typeof body.model === 'string' ? body.model.trim() : undefined;
221
+ const model = modelCandidate?.length
222
+ ? modelCandidate
223
+ : (agentCfg.model ?? cfg.defaults.model);
224
+ try {
225
+ const row = await createSessionRow({
226
+ db,
227
+ cfg,
228
+ agent,
229
+ provider,
230
+ model,
231
+ title: (body.title as string | null | undefined) ?? null,
232
+ });
233
+ return c.json(row, 201);
234
+ } catch (err) {
235
+ logger.error('Failed to create session', err);
236
+ const errorResponse = serializeError(err);
237
+ return c.json(errorResponse, errorResponse.error.status || 400);
238
+ }
239
+ },
240
+ );
241
+
242
+ // Get single session
243
+ openApiRoute(
244
+ app,
245
+ {
246
+ method: 'get',
247
+ path: '/v1/sessions/{sessionId}',
248
+ tags: ['sessions'],
249
+ operationId: 'getSession',
250
+ summary: 'Get a single session by ID',
251
+ parameters: [
252
+ {
253
+ in: 'path',
254
+ name: 'sessionId',
255
+ required: true,
256
+ schema: {
257
+ type: 'string',
258
+ },
259
+ },
260
+ {
261
+ in: 'query',
262
+ name: 'project',
263
+ required: false,
264
+ schema: {
265
+ type: 'string',
266
+ },
267
+ description:
268
+ 'Project root override (defaults to current working directory).',
269
+ },
270
+ ],
271
+ responses: {
272
+ '200': {
273
+ description: 'OK',
274
+ content: {
275
+ 'application/json': {
276
+ schema: {
277
+ $ref: '#/components/schemas/Session',
278
+ },
279
+ },
280
+ },
281
+ },
282
+ '404': {
283
+ description: 'Bad Request',
284
+ content: {
285
+ 'application/json': {
286
+ schema: {
287
+ type: 'object',
288
+ properties: {
289
+ error: {
290
+ type: 'string',
291
+ },
292
+ },
293
+ required: ['error'],
294
+ },
295
+ },
296
+ },
297
+ },
298
+ },
299
+ },
300
+ async (c) => {
301
+ try {
302
+ const sessionId = c.req.param('sessionId');
303
+ const projectRoot = c.req.query('project') || process.cwd();
304
+ const { db } = await loadProjectDb(projectRoot);
305
+ const session = await findSessionById(db, sessionId);
306
+ if (!session) {
307
+ return c.json(
308
+ { error: { message: 'Session not found', status: 404 } },
309
+ 404,
310
+ );
311
+ }
312
+ return c.json(normalizeSessionRow(session));
313
+ } catch (err) {
314
+ logger.error('Failed to get session', err);
315
+ const errorResponse = serializeError(err);
316
+ return c.json(errorResponse, errorResponse.error.status || 500);
317
+ }
318
+ },
319
+ );
320
+
321
+ // Update session preferences
322
+ openApiRoute(
323
+ app,
324
+ {
325
+ method: 'patch',
326
+ path: '/v1/sessions/{sessionId}',
327
+ tags: ['sessions'],
328
+ operationId: 'updateSession',
329
+ summary: 'Update session preferences',
330
+ parameters: [
331
+ {
332
+ in: 'path',
333
+ name: 'sessionId',
334
+ required: true,
335
+ schema: {
336
+ type: 'string',
337
+ },
338
+ },
339
+ {
340
+ in: 'query',
341
+ name: 'project',
342
+ required: false,
343
+ schema: {
344
+ type: 'string',
345
+ },
346
+ description:
347
+ 'Project root override (defaults to current working directory).',
348
+ },
349
+ ],
350
+ requestBody: {
351
+ required: true,
352
+ content: {
353
+ 'application/json': {
354
+ schema: {
355
+ type: 'object',
356
+ properties: {
357
+ title: {
358
+ type: 'string',
359
+ },
360
+ agent: {
361
+ type: 'string',
362
+ },
363
+ provider: {
364
+ $ref: '#/components/schemas/Provider',
365
+ },
366
+ model: {
367
+ type: 'string',
368
+ },
369
+ },
370
+ },
371
+ },
372
+ },
373
+ },
374
+ responses: {
375
+ '200': {
376
+ description: 'OK',
377
+ content: {
378
+ 'application/json': {
379
+ schema: {
380
+ $ref: '#/components/schemas/Session',
381
+ },
382
+ },
383
+ },
384
+ },
385
+ '400': {
386
+ description: 'Bad Request',
387
+ content: {
388
+ 'application/json': {
389
+ schema: {
390
+ type: 'object',
391
+ properties: {
392
+ error: {
393
+ type: 'string',
394
+ },
395
+ },
396
+ required: ['error'],
397
+ },
398
+ },
399
+ },
400
+ },
401
+ '404': {
402
+ description: 'Bad Request',
403
+ content: {
404
+ 'application/json': {
405
+ schema: {
406
+ type: 'object',
407
+ properties: {
408
+ error: {
409
+ type: 'string',
410
+ },
411
+ },
412
+ required: ['error'],
413
+ },
414
+ },
415
+ },
416
+ },
417
+ },
418
+ },
419
+ async (c) => {
420
+ try {
421
+ const sessionId = c.req.param('sessionId');
422
+ const projectRoot = c.req.query('project') || process.cwd();
423
+ const { cfg, db } = await loadProjectDb(projectRoot);
424
+
425
+ const body = (await c.req.json().catch(() => ({}))) as Record<
426
+ string,
427
+ unknown
428
+ >;
429
+
430
+ const existingSession = await findSessionById(db, sessionId);
431
+
432
+ if (!existingSession) {
433
+ return c.json({ error: 'Session not found' }, 404);
434
+ }
435
+
436
+ // Verify session belongs to current project
437
+ if (existingSession.projectPath !== cfg.projectRoot) {
438
+ return c.json({ error: 'Session not found in this project' }, 404);
439
+ }
440
+
441
+ const updateResult = await buildSessionPreferenceUpdates(
442
+ cfg,
443
+ existingSession,
444
+ body,
445
+ );
446
+ if (!updateResult.ok) {
447
+ return c.json({ error: updateResult.error }, updateResult.status);
448
+ }
449
+
450
+ // Perform update
451
+ await db
452
+ .update(sessions)
453
+ .set(updateResult.updates)
454
+ .where(eq(sessions.id, sessionId));
455
+
456
+ // Return updated session
457
+ const updatedRows = await db
458
+ .select()
459
+ .from(sessions)
460
+ .where(eq(sessions.id, sessionId))
461
+ .limit(1);
462
+
463
+ return c.json(updatedRows[0]);
464
+ } catch (err) {
465
+ logger.error('Failed to update session', err);
466
+ const errorResponse = serializeError(err);
467
+ return c.json(errorResponse, errorResponse.error.status || 500);
468
+ }
469
+ },
470
+ );
471
+
472
+ // Delete session
473
+ openApiRoute(
474
+ app,
475
+ {
476
+ method: 'delete',
477
+ path: '/v1/sessions/{sessionId}',
478
+ tags: ['sessions'],
479
+ operationId: 'deleteSession',
480
+ summary: 'Delete a session',
481
+ parameters: [
482
+ {
483
+ in: 'path',
484
+ name: 'sessionId',
485
+ required: true,
486
+ schema: {
487
+ type: 'string',
488
+ },
489
+ },
490
+ {
491
+ in: 'query',
492
+ name: 'project',
493
+ required: false,
494
+ schema: {
495
+ type: 'string',
496
+ },
497
+ description:
498
+ 'Project root override (defaults to current working directory).',
499
+ },
500
+ ],
501
+ responses: {
502
+ '200': {
503
+ description: 'OK',
504
+ content: {
505
+ 'application/json': {
506
+ schema: {
507
+ type: 'object',
508
+ properties: {
509
+ success: {
510
+ type: 'boolean',
511
+ },
512
+ },
513
+ required: ['success'],
514
+ },
515
+ },
516
+ },
517
+ },
518
+ '404': {
519
+ description: 'Bad Request',
520
+ content: {
521
+ 'application/json': {
522
+ schema: {
523
+ type: 'object',
524
+ properties: {
525
+ error: {
526
+ type: 'string',
527
+ },
528
+ },
529
+ required: ['error'],
530
+ },
531
+ },
532
+ },
533
+ },
534
+ },
535
+ },
536
+ async (c) => {
537
+ try {
538
+ const sessionId = c.req.param('sessionId');
539
+ const projectRoot = c.req.query('project') || process.cwd();
540
+ const { cfg, db } = await loadProjectDb(projectRoot);
541
+
542
+ const existingSession = await findSessionById(db, sessionId);
543
+
544
+ if (!existingSession) {
545
+ return c.json({ error: 'Session not found' }, 404);
546
+ }
547
+
548
+ if (existingSession.projectPath !== cfg.projectRoot) {
549
+ return c.json({ error: 'Session not found in this project' }, 404);
550
+ }
551
+
552
+ await deleteSessionMessagesAndParts(db, sessionId);
553
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
554
+
555
+ return c.json({ success: true });
556
+ } catch (err) {
557
+ logger.error('Failed to delete session', err);
558
+ const errorResponse = serializeError(err);
559
+ return c.json(errorResponse, errorResponse.error.status || 500);
560
+ }
561
+ },
562
+ );
563
+ }