@ottocode/server 0.1.265 → 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 (72) 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/provider/custom.ts +73 -0
  64. package/src/runtime/provider/index.ts +2 -85
  65. package/src/runtime/provider/reasoning-builders.ts +280 -0
  66. package/src/runtime/provider/reasoning.ts +67 -264
  67. package/src/tools/adapter/events.ts +116 -0
  68. package/src/tools/adapter/execution.ts +160 -0
  69. package/src/tools/adapter/pending.ts +37 -0
  70. package/src/tools/adapter/persistence.ts +166 -0
  71. package/src/tools/adapter/results.ts +97 -0
  72. package/src/tools/adapter.ts +124 -451
@@ -0,0 +1,578 @@
1
+ import type { Hono } from 'hono';
2
+ import {
3
+ authorize,
4
+ authorizeOpenAIWeb,
5
+ authorizeWeb,
6
+ exchange,
7
+ exchangeOpenAIWeb,
8
+ exchangeWeb,
9
+ setAuth,
10
+ } from '@ottocode/sdk';
11
+ import { logger } from '@ottocode/sdk';
12
+ import { openApiRoute } from '../../openapi/route.ts';
13
+ import { oauthVerifiers } from './state.ts';
14
+
15
+ export function registerAuthOAuthRoutes(app: Hono) {
16
+ openApiRoute(
17
+ app,
18
+ {
19
+ method: 'post',
20
+ path: '/v1/auth/{provider}/oauth/url',
21
+ tags: ['auth'],
22
+ operationId: 'getOAuthUrl',
23
+ summary: 'Get OAuth authorization URL',
24
+ parameters: [
25
+ {
26
+ in: 'path',
27
+ name: 'provider',
28
+ required: true,
29
+ schema: {
30
+ type: 'string',
31
+ },
32
+ },
33
+ ],
34
+ requestBody: {
35
+ required: false,
36
+ content: {
37
+ 'application/json': {
38
+ schema: {
39
+ type: 'object',
40
+ properties: {
41
+ mode: {
42
+ type: 'string',
43
+ enum: ['max', 'console'],
44
+ default: 'max',
45
+ },
46
+ },
47
+ },
48
+ },
49
+ },
50
+ },
51
+ responses: {
52
+ '200': {
53
+ description: 'OK',
54
+ content: {
55
+ 'application/json': {
56
+ schema: {
57
+ type: 'object',
58
+ properties: {
59
+ url: {
60
+ type: 'string',
61
+ },
62
+ sessionId: {
63
+ type: 'string',
64
+ },
65
+ provider: {
66
+ type: 'string',
67
+ },
68
+ },
69
+ required: ['url', 'sessionId', 'provider'],
70
+ },
71
+ },
72
+ },
73
+ },
74
+ '400': {
75
+ description: 'Bad Request',
76
+ content: {
77
+ 'application/json': {
78
+ schema: {
79
+ type: 'object',
80
+ properties: {
81
+ error: {
82
+ type: 'string',
83
+ },
84
+ },
85
+ required: ['error'],
86
+ },
87
+ },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ async (c) => {
93
+ try {
94
+ const provider = c.req.param('provider');
95
+ const body = await c.req
96
+ .json<{ mode?: string }>()
97
+ .catch(() => undefined);
98
+ const mode: 'max' | 'console' =
99
+ body?.mode === 'console' ? 'console' : 'max';
100
+
101
+ let url: string;
102
+ let verifier: string;
103
+
104
+ if (provider === 'anthropic') {
105
+ const result = await authorize(mode);
106
+ url = result.url;
107
+ verifier = result.verifier;
108
+ } else if (provider === 'openai') {
109
+ return c.json(
110
+ {
111
+ error:
112
+ 'OpenAI OAuth requires localhost callback. Use the redirect flow instead.',
113
+ },
114
+ 400,
115
+ );
116
+ } else {
117
+ return c.json(
118
+ {
119
+ error: `OAuth not supported for provider: ${provider}. Copilot uses device flow — use /v1/auth/copilot/device/start instead.`,
120
+ },
121
+ 400,
122
+ );
123
+ }
124
+
125
+ const sessionId = crypto.randomUUID();
126
+ oauthVerifiers.set(sessionId, {
127
+ verifier,
128
+ provider,
129
+ createdAt: Date.now(),
130
+ callbackUrl: '',
131
+ });
132
+
133
+ return c.json({ url, sessionId, provider });
134
+ } catch (error) {
135
+ const message =
136
+ error instanceof Error
137
+ ? error.message
138
+ : 'OAuth initialization failed';
139
+ logger.error('OAuth URL generation failed', error);
140
+ return c.json({ error: message }, 500);
141
+ }
142
+ },
143
+ );
144
+
145
+ openApiRoute(
146
+ app,
147
+ {
148
+ method: 'post',
149
+ path: '/v1/auth/{provider}/oauth/exchange',
150
+ tags: ['auth'],
151
+ operationId: 'exchangeOAuthCode',
152
+ summary: 'Exchange OAuth code for tokens',
153
+ parameters: [
154
+ {
155
+ in: 'path',
156
+ name: 'provider',
157
+ required: true,
158
+ schema: {
159
+ type: 'string',
160
+ },
161
+ },
162
+ ],
163
+ requestBody: {
164
+ required: true,
165
+ content: {
166
+ 'application/json': {
167
+ schema: {
168
+ type: 'object',
169
+ properties: {
170
+ code: {
171
+ type: 'string',
172
+ },
173
+ sessionId: {
174
+ type: 'string',
175
+ },
176
+ },
177
+ required: ['code', 'sessionId'],
178
+ },
179
+ },
180
+ },
181
+ },
182
+ responses: {
183
+ '200': {
184
+ description: 'OK',
185
+ content: {
186
+ 'application/json': {
187
+ schema: {
188
+ type: 'object',
189
+ properties: {
190
+ success: {
191
+ type: 'boolean',
192
+ },
193
+ provider: {
194
+ type: 'string',
195
+ },
196
+ },
197
+ required: ['success', 'provider'],
198
+ },
199
+ },
200
+ },
201
+ },
202
+ '400': {
203
+ description: 'Bad Request',
204
+ content: {
205
+ 'application/json': {
206
+ schema: {
207
+ type: 'object',
208
+ properties: {
209
+ error: {
210
+ type: 'string',
211
+ },
212
+ },
213
+ required: ['error'],
214
+ },
215
+ },
216
+ },
217
+ },
218
+ },
219
+ },
220
+ async (c) => {
221
+ try {
222
+ const provider = c.req.param('provider');
223
+ const { code, sessionId } = await c.req.json<{
224
+ code: string;
225
+ sessionId: string;
226
+ }>();
227
+
228
+ if (!code || !sessionId) {
229
+ return c.json({ error: 'Code and sessionId required' }, 400);
230
+ }
231
+
232
+ if (!oauthVerifiers.has(sessionId)) {
233
+ return c.json({ error: 'Session expired or invalid' }, 400);
234
+ }
235
+
236
+ const verifierEntry = oauthVerifiers.get(sessionId);
237
+ if (!verifierEntry) {
238
+ return c.json({ error: 'Session expired or invalid' }, 400);
239
+ }
240
+ const { verifier } = verifierEntry;
241
+ oauthVerifiers.delete(sessionId);
242
+
243
+ if (provider === 'anthropic') {
244
+ const tokens = await exchange(code, verifier);
245
+ await setAuth(
246
+ 'anthropic',
247
+ {
248
+ type: 'oauth',
249
+ refresh: tokens.refresh,
250
+ access: tokens.access,
251
+ expires: tokens.expires,
252
+ },
253
+ undefined,
254
+ 'global',
255
+ );
256
+ } else if (provider === 'openai') {
257
+ return c.json({ error: 'Use redirect flow for OpenAI' }, 400);
258
+ } else {
259
+ return c.json({ error: 'Unknown provider' }, 400);
260
+ }
261
+
262
+ return c.json({ success: true, provider });
263
+ } catch (error) {
264
+ const message =
265
+ error instanceof Error ? error.message : 'Token exchange failed';
266
+ logger.error('OAuth exchange failed', error);
267
+ return c.json({ error: message }, 500);
268
+ }
269
+ },
270
+ );
271
+
272
+ openApiRoute(
273
+ app,
274
+ {
275
+ method: 'get',
276
+ path: '/v1/auth/{provider}/oauth/start',
277
+ tags: ['auth'],
278
+ operationId: 'startOAuth',
279
+ summary: 'Start OAuth flow with redirect',
280
+ parameters: [
281
+ {
282
+ in: 'path',
283
+ name: 'provider',
284
+ required: true,
285
+ schema: {
286
+ type: 'string',
287
+ },
288
+ },
289
+ {
290
+ in: 'query',
291
+ name: 'mode',
292
+ required: false,
293
+ schema: {
294
+ type: 'string',
295
+ enum: ['max', 'console'],
296
+ default: 'max',
297
+ },
298
+ },
299
+ ],
300
+ responses: {
301
+ '302': {
302
+ description: 'Redirect to OAuth provider',
303
+ },
304
+ '400': {
305
+ description: 'Bad Request',
306
+ content: {
307
+ 'application/json': {
308
+ schema: {
309
+ type: 'object',
310
+ properties: {
311
+ error: {
312
+ type: 'string',
313
+ },
314
+ },
315
+ required: ['error'],
316
+ },
317
+ },
318
+ },
319
+ },
320
+ },
321
+ },
322
+ async (c) => {
323
+ try {
324
+ const provider = c.req.param('provider');
325
+ const mode = c.req.query('mode') || 'max';
326
+ const host = c.req.header('host') || 'localhost:3000';
327
+ const protocol = c.req.header('x-forwarded-proto') || 'http';
328
+
329
+ let url: string;
330
+ let verifier: string;
331
+ let callbackUrl = '';
332
+
333
+ if (provider === 'anthropic') {
334
+ callbackUrl = `${protocol}://${host}/v1/auth/${provider}/oauth/callback`;
335
+ const result = authorizeWeb(mode as 'max' | 'console', callbackUrl);
336
+ url = result.url;
337
+ verifier = result.verifier;
338
+ } else if (provider === 'openai') {
339
+ callbackUrl = `${protocol}://${host}/v1/auth/${provider}/oauth/callback`;
340
+ const result = authorizeOpenAIWeb(callbackUrl);
341
+ url = result.url;
342
+ verifier = result.verifier;
343
+ } else {
344
+ return c.json(
345
+ { error: 'OAuth not supported for this provider' },
346
+ 400,
347
+ );
348
+ }
349
+
350
+ const sessionId = crypto.randomUUID();
351
+ oauthVerifiers.set(sessionId, {
352
+ verifier,
353
+ provider,
354
+ createdAt: Date.now(),
355
+ callbackUrl,
356
+ });
357
+
358
+ c.header(
359
+ 'Set-Cookie',
360
+ `oauth_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
361
+ );
362
+
363
+ return c.redirect(url);
364
+ } catch (error) {
365
+ const message =
366
+ error instanceof Error
367
+ ? error.message
368
+ : 'OAuth initialization failed';
369
+ logger.error('OAuth start failed', error);
370
+ return c.json({ error: message }, 500);
371
+ }
372
+ },
373
+ );
374
+
375
+ openApiRoute(
376
+ app,
377
+ {
378
+ method: 'get',
379
+ path: '/v1/auth/{provider}/oauth/callback',
380
+ tags: ['auth'],
381
+ operationId: 'oauthCallback',
382
+ summary: 'OAuth callback handler',
383
+ parameters: [
384
+ {
385
+ in: 'path',
386
+ name: 'provider',
387
+ required: true,
388
+ schema: {
389
+ type: 'string',
390
+ },
391
+ },
392
+ {
393
+ in: 'query',
394
+ name: 'code',
395
+ required: false,
396
+ schema: {
397
+ type: 'string',
398
+ },
399
+ },
400
+ {
401
+ in: 'query',
402
+ name: 'fragment',
403
+ required: false,
404
+ schema: {
405
+ type: 'string',
406
+ },
407
+ },
408
+ ],
409
+ responses: {
410
+ '200': {
411
+ description: 'HTML response',
412
+ content: {
413
+ 'text/html': {
414
+ schema: {
415
+ type: 'string',
416
+ },
417
+ },
418
+ },
419
+ },
420
+ },
421
+ },
422
+ async (c) => {
423
+ try {
424
+ const provider = c.req.param('provider');
425
+ const code = c.req.query('code');
426
+ const fragment = c.req.query('fragment');
427
+
428
+ const cookies = c.req.header('Cookie') || '';
429
+ const sessionMatch = cookies.match(/oauth_session=([^;]+)/);
430
+ const sessionId = sessionMatch?.[1];
431
+
432
+ if (!sessionId || !oauthVerifiers.has(sessionId)) {
433
+ return c.html(
434
+ '<html><body><h1>Session expired</h1><p>Please close this window and try again.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
435
+ );
436
+ }
437
+
438
+ const callbackEntry = oauthVerifiers.get(sessionId);
439
+ if (!callbackEntry) {
440
+ return c.html(
441
+ '<html><body><h1>Session expired</h1><p>Please close this window and try again.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
442
+ );
443
+ }
444
+ const { verifier, callbackUrl } = callbackEntry;
445
+ oauthVerifiers.delete(sessionId);
446
+
447
+ if (provider === 'anthropic') {
448
+ const fullCode = fragment ? `${code}#${fragment}` : (code ?? '');
449
+ const tokens = await exchangeWeb(fullCode, verifier, callbackUrl);
450
+
451
+ await setAuth(
452
+ 'anthropic',
453
+ {
454
+ type: 'oauth',
455
+ refresh: tokens.refresh,
456
+ access: tokens.access,
457
+ expires: tokens.expires,
458
+ },
459
+ undefined,
460
+ 'global',
461
+ );
462
+ } else if (provider === 'openai') {
463
+ const tokens = await exchangeOpenAIWeb(
464
+ code ?? '',
465
+ verifier,
466
+ callbackUrl,
467
+ );
468
+
469
+ await setAuth(
470
+ 'openai',
471
+ {
472
+ type: 'oauth',
473
+ refresh: tokens.refresh,
474
+ access: tokens.access,
475
+ expires: tokens.expires,
476
+ accountId: tokens.accountId,
477
+ idToken: tokens.idToken,
478
+ },
479
+ undefined,
480
+ 'global',
481
+ );
482
+ }
483
+
484
+ return c.html(`
485
+ <html>
486
+ <head>
487
+ <title>Connected!</title>
488
+ <style>
489
+ body {
490
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
491
+ display: flex;
492
+ justify-content: center;
493
+ align-items: center;
494
+ height: 100vh;
495
+ margin: 0;
496
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
497
+ color: white;
498
+ }
499
+ .container {
500
+ text-align: center;
501
+ padding: 2rem;
502
+ background: rgba(255,255,255,0.1);
503
+ border-radius: 16px;
504
+ backdrop-filter: blur(10px);
505
+ }
506
+ .checkmark {
507
+ font-size: 4rem;
508
+ margin-bottom: 1rem;
509
+ }
510
+ h1 { margin: 0 0 0.5rem 0; }
511
+ p { margin: 0; opacity: 0.9; }
512
+ </style>
513
+ </head>
514
+ <body>
515
+ <div class="container">
516
+ <div class="checkmark">✓</div>
517
+ <h1>Connected!</h1>
518
+ <p>You can close this window.</p>
519
+ </div>
520
+ <script>
521
+ if (window.opener) {
522
+ window.opener.postMessage({ type: 'oauth-success', provider: '${provider}' }, '*');
523
+ }
524
+ setTimeout(() => window.close(), 1500);
525
+ </script>
526
+ </body>
527
+ </html>
528
+ `);
529
+ } catch (error) {
530
+ const message =
531
+ error instanceof Error ? error.message : 'Authentication failed';
532
+ logger.error('OAuth callback failed', error);
533
+ return c.html(`
534
+ <html>
535
+ <head>
536
+ <title>Error</title>
537
+ <style>
538
+ body {
539
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
540
+ display: flex;
541
+ justify-content: center;
542
+ align-items: center;
543
+ height: 100vh;
544
+ margin: 0;
545
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
546
+ color: white;
547
+ }
548
+ .container {
549
+ text-align: center;
550
+ padding: 2rem;
551
+ background: rgba(255,255,255,0.1);
552
+ border-radius: 16px;
553
+ backdrop-filter: blur(10px);
554
+ }
555
+ .icon { font-size: 4rem; margin-bottom: 1rem; }
556
+ h1 { margin: 0 0 0.5rem 0; }
557
+ p { margin: 0; opacity: 0.9; }
558
+ </style>
559
+ </head>
560
+ <body>
561
+ <div class="container">
562
+ <div class="icon">✗</div>
563
+ <h1>Error</h1>
564
+ <p>${message}</p>
565
+ </div>
566
+ <script>
567
+ if (window.opener) {
568
+ window.opener.postMessage({ type: 'oauth-error', provider: '${c.req.param('provider')}', error: '${message}' }, '*');
569
+ }
570
+ setTimeout(() => window.close(), 3000);
571
+ </script>
572
+ </body>
573
+ </html>
574
+ `);
575
+ }
576
+ },
577
+ );
578
+ }
@@ -0,0 +1,45 @@
1
+ import type { Hono } from 'hono';
2
+ import { logger, setOnboardingComplete } from '@ottocode/sdk';
3
+ import { openApiRoute } from '../../openapi/route.ts';
4
+ import { serializeError } from '../../runtime/errors/api-error.ts';
5
+
6
+ export function registerAuthOnboardingRoutes(app: Hono) {
7
+ openApiRoute(
8
+ app,
9
+ {
10
+ method: 'post',
11
+ path: '/v1/auth/onboarding/complete',
12
+ tags: ['auth'],
13
+ operationId: 'completeOnboarding',
14
+ summary: 'Mark onboarding as complete',
15
+ responses: {
16
+ '200': {
17
+ description: 'OK',
18
+ content: {
19
+ 'application/json': {
20
+ schema: {
21
+ type: 'object',
22
+ properties: {
23
+ success: {
24
+ type: 'boolean',
25
+ },
26
+ },
27
+ required: ['success'],
28
+ },
29
+ },
30
+ },
31
+ },
32
+ },
33
+ },
34
+ async (c) => {
35
+ try {
36
+ await setOnboardingComplete();
37
+ return c.json({ success: true });
38
+ } catch (error) {
39
+ logger.error('Failed to complete onboarding', error);
40
+ const errorResponse = serializeError(error);
41
+ return c.json(errorResponse, errorResponse.error.status || 500);
42
+ }
43
+ },
44
+ );
45
+ }