@ottocode/server 0.1.259 → 0.1.261

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 (69) hide show
  1. package/package.json +4 -3
  2. package/src/index.ts +5 -4
  3. package/src/openapi/register.ts +92 -0
  4. package/src/openapi/route.ts +22 -0
  5. package/src/routes/ask.ts +210 -99
  6. package/src/routes/auth.ts +1701 -626
  7. package/src/routes/branch.ts +281 -90
  8. package/src/routes/config/agents.ts +79 -32
  9. package/src/routes/config/cwd.ts +46 -14
  10. package/src/routes/config/debug.ts +159 -30
  11. package/src/routes/config/defaults.ts +182 -64
  12. package/src/routes/config/main.ts +109 -73
  13. package/src/routes/config/models.ts +304 -137
  14. package/src/routes/config/providers.ts +462 -166
  15. package/src/routes/config/utils.ts +2 -2
  16. package/src/routes/doctor.ts +395 -161
  17. package/src/routes/files.ts +650 -260
  18. package/src/routes/git/branch.ts +143 -52
  19. package/src/routes/git/commit.ts +347 -141
  20. package/src/routes/git/diff.ts +239 -116
  21. package/src/routes/git/init.ts +103 -23
  22. package/src/routes/git/pull.ts +167 -65
  23. package/src/routes/git/push.ts +222 -117
  24. package/src/routes/git/remote.ts +401 -100
  25. package/src/routes/git/staging.ts +502 -141
  26. package/src/routes/git/status.ts +171 -78
  27. package/src/routes/mcp.ts +1129 -404
  28. package/src/routes/openapi.ts +27 -4
  29. package/src/routes/ottorouter.ts +1221 -389
  30. package/src/routes/provider-usage.ts +153 -36
  31. package/src/routes/research.ts +817 -370
  32. package/src/routes/root.ts +50 -6
  33. package/src/routes/session-approval.ts +228 -54
  34. package/src/routes/session-files.ts +265 -134
  35. package/src/routes/session-messages.ts +330 -150
  36. package/src/routes/session-stream.ts +83 -2
  37. package/src/routes/sessions.ts +1830 -780
  38. package/src/routes/skills.ts +849 -161
  39. package/src/routes/terminals.ts +469 -103
  40. package/src/routes/tunnel.ts +394 -118
  41. package/src/runtime/agent/runner-reasoning.ts +38 -3
  42. package/src/runtime/agent/runner.ts +1 -0
  43. package/src/runtime/ask/service.ts +1 -0
  44. package/src/runtime/message/compaction-limits.ts +3 -3
  45. package/src/runtime/provider/reasoning.ts +18 -7
  46. package/src/runtime/session/db-operations.ts +4 -3
  47. package/src/runtime/utils/token.ts +7 -2
  48. package/src/tools/adapter.ts +21 -0
  49. package/src/openapi/paths/ask.ts +0 -81
  50. package/src/openapi/paths/auth.ts +0 -687
  51. package/src/openapi/paths/branch.ts +0 -102
  52. package/src/openapi/paths/config.ts +0 -485
  53. package/src/openapi/paths/doctor.ts +0 -165
  54. package/src/openapi/paths/files.ts +0 -236
  55. package/src/openapi/paths/git.ts +0 -690
  56. package/src/openapi/paths/mcp.ts +0 -339
  57. package/src/openapi/paths/messages.ts +0 -103
  58. package/src/openapi/paths/ottorouter.ts +0 -594
  59. package/src/openapi/paths/provider-usage.ts +0 -59
  60. package/src/openapi/paths/research.ts +0 -227
  61. package/src/openapi/paths/session-approval.ts +0 -93
  62. package/src/openapi/paths/session-extras.ts +0 -336
  63. package/src/openapi/paths/session-files.ts +0 -91
  64. package/src/openapi/paths/sessions.ts +0 -210
  65. package/src/openapi/paths/skills.ts +0 -377
  66. package/src/openapi/paths/stream.ts +0 -26
  67. package/src/openapi/paths/terminals.ts +0 -226
  68. package/src/openapi/paths/tunnel.ts +0 -163
  69. package/src/openapi/spec.ts +0 -73
@@ -4,72 +4,238 @@ import { streamSSE } from 'hono/streaming';
4
4
  import type { TerminalManager } from '@ottocode/sdk';
5
5
  import { logger } from '@ottocode/sdk';
6
6
  import { upgradeWebSocket } from '../ws.ts';
7
+ import { openApiRoute } from '../openapi/route.ts';
7
8
 
8
9
  export function registerTerminalsRoutes(
9
10
  app: Hono,
10
11
  terminalManager: TerminalManager,
11
12
  ) {
12
- app.get('/v1/terminals', async (c) => {
13
- const terminals = terminalManager.list();
14
- return c.json({
15
- terminals: terminals.map((t) => t.toJSON()),
16
- count: terminals.length,
17
- });
18
- });
19
-
20
- app.post('/v1/terminals', async (c) => {
21
- try {
22
- const body = await c.req.json();
23
- const { command, args, purpose, cwd, title } = body;
13
+ openApiRoute(
14
+ app,
15
+ {
16
+ method: 'get',
17
+ path: '/v1/terminals',
18
+ operationId: 'getTerminals',
19
+ summary: 'List all terminals',
20
+ description: 'Get a list of all active terminal sessions',
21
+ responses: {
22
+ '200': {
23
+ description: 'List of terminals',
24
+ content: {
25
+ 'application/json': {
26
+ schema: {
27
+ type: 'object',
28
+ properties: {
29
+ terminals: {
30
+ type: 'array',
31
+ items: {
32
+ $ref: '#/components/schemas/Terminal',
33
+ },
34
+ },
35
+ count: {
36
+ type: 'integer',
37
+ },
38
+ },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ async (c) => {
46
+ const terminals = terminalManager.list();
47
+ return c.json({
48
+ terminals: terminals.map((t) => t.toJSON()),
49
+ count: terminals.length,
50
+ });
51
+ },
52
+ );
24
53
 
25
- if (!command || !purpose) {
26
- return c.json({ error: 'command and purpose are required' }, 400);
27
- }
54
+ openApiRoute(
55
+ app,
56
+ {
57
+ method: 'post',
58
+ path: '/v1/terminals',
59
+ operationId: 'postTerminals',
60
+ summary: 'Create a new terminal',
61
+ description: 'Spawn a new terminal process',
62
+ requestBody: {
63
+ required: true,
64
+ content: {
65
+ 'application/json': {
66
+ schema: {
67
+ type: 'object',
68
+ required: ['command', 'purpose'],
69
+ properties: {
70
+ command: {
71
+ type: 'string',
72
+ description: 'Command to execute',
73
+ },
74
+ args: {
75
+ type: 'array',
76
+ items: {
77
+ type: 'string',
78
+ },
79
+ description: 'Command arguments',
80
+ },
81
+ purpose: {
82
+ type: 'string',
83
+ description: 'Description of terminal purpose',
84
+ },
85
+ cwd: {
86
+ type: 'string',
87
+ description: 'Working directory',
88
+ },
89
+ title: {
90
+ type: 'string',
91
+ description: 'Terminal title',
92
+ },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ },
98
+ responses: {
99
+ '200': {
100
+ description: 'Terminal created',
101
+ content: {
102
+ 'application/json': {
103
+ schema: {
104
+ type: 'object',
105
+ properties: {
106
+ terminalId: {
107
+ type: 'string',
108
+ },
109
+ pid: {
110
+ type: 'integer',
111
+ },
112
+ purpose: {
113
+ type: 'string',
114
+ },
115
+ command: {
116
+ type: 'string',
117
+ },
118
+ },
119
+ },
120
+ },
121
+ },
122
+ },
123
+ },
124
+ },
125
+ async (c) => {
126
+ try {
127
+ const body = await c.req.json();
128
+ const { command, args, purpose, cwd, title } = body;
129
+
130
+ if (!command || !purpose) {
131
+ return c.json({ error: 'command and purpose are required' }, 400);
132
+ }
28
133
 
29
- let resolvedCommand = command;
30
- if (command === 'bash' || command === 'sh' || command === 'shell') {
31
- resolvedCommand =
32
- process.platform === 'win32'
33
- ? process.env.COMSPEC || 'cmd.exe'
34
- : process.env.SHELL || '/bin/bash';
134
+ let resolvedCommand = command;
135
+ if (command === 'bash' || command === 'sh' || command === 'shell') {
136
+ resolvedCommand =
137
+ process.platform === 'win32'
138
+ ? process.env.COMSPEC || 'cmd.exe'
139
+ : process.env.SHELL || '/bin/bash';
140
+ }
141
+ const resolvedCwd = cwd || process.cwd();
142
+
143
+ const terminal = terminalManager.create({
144
+ command: resolvedCommand,
145
+ args: args || [],
146
+ purpose,
147
+ cwd: resolvedCwd,
148
+ createdBy: 'user',
149
+ title,
150
+ });
151
+
152
+ return c.json({
153
+ terminalId: terminal.id,
154
+ pid: terminal.pid,
155
+ purpose: terminal.purpose,
156
+ command: terminal.command,
157
+ });
158
+ } catch (error) {
159
+ logger.error('Error creating terminal', error);
160
+ const message = error instanceof Error ? error.message : String(error);
161
+ return c.json({ error: message }, 500);
35
162
  }
36
- const resolvedCwd = cwd || process.cwd();
37
-
38
- const terminal = terminalManager.create({
39
- command: resolvedCommand,
40
- args: args || [],
41
- purpose,
42
- cwd: resolvedCwd,
43
- createdBy: 'user',
44
- title,
45
- });
46
-
47
- return c.json({
48
- terminalId: terminal.id,
49
- pid: terminal.pid,
50
- purpose: terminal.purpose,
51
- command: terminal.command,
52
- });
53
- } catch (error) {
54
- logger.error('Error creating terminal', error);
55
- const message = error instanceof Error ? error.message : String(error);
56
- return c.json({ error: message }, 500);
57
- }
58
- });
163
+ },
164
+ );
59
165
 
60
- app.get('/v1/terminals/:id', async (c) => {
61
- const id = c.req.param('id');
62
- const terminal = terminalManager.get(id);
166
+ openApiRoute(
167
+ app,
168
+ {
169
+ method: 'get',
170
+ path: '/v1/terminals/{id}',
171
+ operationId: 'getTerminalsById',
172
+ summary: 'Get terminal details',
173
+ description: 'Get information about a specific terminal',
174
+ parameters: [
175
+ {
176
+ name: 'id',
177
+ in: 'path',
178
+ required: true,
179
+ schema: {
180
+ type: 'string',
181
+ },
182
+ },
183
+ ],
184
+ responses: {
185
+ '200': {
186
+ description: 'Terminal details',
187
+ content: {
188
+ 'application/json': {
189
+ schema: {
190
+ type: 'object',
191
+ properties: {
192
+ terminal: {
193
+ $ref: '#/components/schemas/Terminal',
194
+ },
195
+ },
196
+ },
197
+ },
198
+ },
199
+ },
200
+ '404': {
201
+ description: 'Terminal not found',
202
+ },
203
+ },
204
+ },
205
+ async (c) => {
206
+ const id = c.req.param('id');
207
+ const terminal = terminalManager.get(id);
63
208
 
64
- if (!terminal) {
65
- return c.json({ error: 'Terminal not found' }, 404);
66
- }
209
+ if (!terminal) {
210
+ return c.json({ error: 'Terminal not found' }, 404);
211
+ }
67
212
 
68
- return c.json({ terminal: terminal.toJSON() });
69
- });
213
+ return c.json({ terminal: terminal.toJSON() });
214
+ },
215
+ );
70
216
 
71
- app.get(
72
- '/v1/terminals/:id/ws',
217
+ openApiRoute(
218
+ app,
219
+ {
220
+ method: 'get',
221
+ path: '/v1/terminals/{id}/ws',
222
+ operationId: 'connectTerminalWebSocket',
223
+ summary: 'Connect to terminal WebSocket',
224
+ description:
225
+ 'Upgrade to a WebSocket for bidirectional terminal I/O. Generated HTTP clients cannot consume the upgraded connection directly.',
226
+ parameters: [
227
+ {
228
+ name: 'id',
229
+ in: 'path',
230
+ required: true,
231
+ schema: { type: 'string' },
232
+ },
233
+ ],
234
+ responses: {
235
+ '101': { description: 'WebSocket upgrade accepted' },
236
+ '404': { description: 'Terminal not found' },
237
+ },
238
+ },
73
239
  upgradeWebSocket((c) => {
74
240
  const id = c.req.param('id');
75
241
 
@@ -252,66 +418,266 @@ export function registerTerminalsRoutes(
252
418
  });
253
419
  };
254
420
 
255
- app.get('/v1/terminals/:id/output', handleTerminalOutput);
256
- app.post('/v1/terminals/:id/output', handleTerminalOutput);
421
+ openApiRoute(
422
+ app,
423
+ {
424
+ method: 'get',
425
+ path: '/v1/terminals/{id}/output',
426
+ operationId: 'getTerminalsByIdOutput',
427
+ summary: 'Stream terminal output',
428
+ description: 'Get real-time terminal output via SSE',
429
+ parameters: [
430
+ {
431
+ name: 'id',
432
+ in: 'path',
433
+ required: true,
434
+ schema: {
435
+ type: 'string',
436
+ },
437
+ },
438
+ ],
439
+ responses: {
440
+ '200': {
441
+ description: 'SSE stream of terminal output',
442
+ content: {
443
+ 'text/event-stream': {
444
+ schema: {
445
+ type: 'string',
446
+ },
447
+ },
448
+ },
449
+ },
450
+ },
451
+ },
452
+ handleTerminalOutput,
453
+ );
454
+ openApiRoute(
455
+ app,
456
+ {
457
+ method: 'post',
458
+ path: '/v1/terminals/{id}/output',
459
+ operationId: 'postTerminalsByIdOutput',
460
+ summary: 'Stream terminal output using POST',
461
+ description: 'Compatibility alias for terminal output SSE',
462
+ parameters: [
463
+ {
464
+ name: 'id',
465
+ in: 'path',
466
+ required: true,
467
+ schema: { type: 'string' },
468
+ },
469
+ ],
470
+ responses: {
471
+ '200': {
472
+ description: 'SSE stream of terminal output',
473
+ content: {
474
+ 'text/event-stream': {
475
+ schema: { type: 'string' },
476
+ },
477
+ },
478
+ },
479
+ },
480
+ },
481
+ handleTerminalOutput,
482
+ );
483
+
484
+ openApiRoute(
485
+ app,
486
+ {
487
+ method: 'post',
488
+ path: '/v1/terminals/{id}/input',
489
+ operationId: 'postTerminalsByIdInput',
490
+ summary: 'Send input to terminal',
491
+ description: 'Write data to terminal stdin',
492
+ parameters: [
493
+ {
494
+ name: 'id',
495
+ in: 'path',
496
+ required: true,
497
+ schema: {
498
+ type: 'string',
499
+ },
500
+ },
501
+ ],
502
+ requestBody: {
503
+ required: true,
504
+ content: {
505
+ 'application/json': {
506
+ schema: {
507
+ type: 'object',
508
+ required: ['input'],
509
+ properties: {
510
+ input: {
511
+ type: 'string',
512
+ description: 'Input to send to terminal',
513
+ },
514
+ },
515
+ },
516
+ },
517
+ },
518
+ },
519
+ responses: {
520
+ '200': {
521
+ description: 'Input sent',
522
+ content: {
523
+ 'application/json': {
524
+ schema: {
525
+ type: 'object',
526
+ properties: {
527
+ success: {
528
+ type: 'boolean',
529
+ },
530
+ },
531
+ },
532
+ },
533
+ },
534
+ },
535
+ },
536
+ },
537
+ async (c) => {
538
+ const id = c.req.param('id');
539
+ const terminal = terminalManager.get(id);
257
540
 
258
- app.post('/v1/terminals/:id/input', async (c) => {
259
- const id = c.req.param('id');
260
- const terminal = terminalManager.get(id);
541
+ if (!terminal) {
542
+ return c.json({ error: 'Terminal not found' }, 404);
543
+ }
261
544
 
262
- if (!terminal) {
263
- return c.json({ error: 'Terminal not found' }, 404);
264
- }
545
+ try {
546
+ const body = await c.req.json();
547
+ const { input } = body;
265
548
 
266
- try {
267
- const body = await c.req.json();
268
- const { input } = body;
549
+ if (!input) {
550
+ return c.json({ error: 'input is required' }, 400);
551
+ }
269
552
 
270
- if (!input) {
271
- return c.json({ error: 'input is required' }, 400);
553
+ terminal.write(input);
554
+ return c.json({ success: true });
555
+ } catch (error) {
556
+ const message = error instanceof Error ? error.message : String(error);
557
+ return c.json({ error: message }, 500);
272
558
  }
559
+ },
560
+ );
273
561
 
274
- terminal.write(input);
275
- return c.json({ success: true });
276
- } catch (error) {
277
- const message = error instanceof Error ? error.message : String(error);
278
- return c.json({ error: message }, 500);
279
- }
280
- });
562
+ openApiRoute(
563
+ app,
564
+ {
565
+ method: 'delete',
566
+ path: '/v1/terminals/{id}',
567
+ operationId: 'deleteTerminalsById',
568
+ summary: 'Kill terminal',
569
+ description: 'Terminate a running terminal process',
570
+ parameters: [
571
+ {
572
+ name: 'id',
573
+ in: 'path',
574
+ required: true,
575
+ schema: {
576
+ type: 'string',
577
+ },
578
+ },
579
+ ],
580
+ responses: {
581
+ '200': {
582
+ description: 'Terminal killed',
583
+ content: {
584
+ 'application/json': {
585
+ schema: {
586
+ type: 'object',
587
+ properties: {
588
+ success: {
589
+ type: 'boolean',
590
+ },
591
+ },
592
+ },
593
+ },
594
+ },
595
+ },
596
+ },
597
+ },
598
+ async (c) => {
599
+ const id = c.req.param('id');
281
600
 
282
- app.delete('/v1/terminals/:id', async (c) => {
283
- const id = c.req.param('id');
601
+ try {
602
+ await terminalManager.kill(id);
603
+ return c.json({ success: true });
604
+ } catch (error) {
605
+ const message = error instanceof Error ? error.message : String(error);
606
+ return c.json({ error: message }, 500);
607
+ }
608
+ },
609
+ );
284
610
 
285
- try {
286
- await terminalManager.kill(id);
287
- return c.json({ success: true });
288
- } catch (error) {
289
- const message = error instanceof Error ? error.message : String(error);
290
- return c.json({ error: message }, 500);
291
- }
292
- });
611
+ openApiRoute(
612
+ app,
613
+ {
614
+ method: 'post',
615
+ path: '/v1/terminals/{id}/resize',
616
+ operationId: 'resizeTerminal',
617
+ summary: 'Resize terminal',
618
+ description: 'Resize the pseudo-terminal dimensions.',
619
+ parameters: [
620
+ {
621
+ name: 'id',
622
+ in: 'path',
623
+ required: true,
624
+ schema: { type: 'string' },
625
+ },
626
+ ],
627
+ requestBody: {
628
+ required: true,
629
+ content: {
630
+ 'application/json': {
631
+ schema: {
632
+ type: 'object',
633
+ required: ['cols', 'rows'],
634
+ properties: {
635
+ cols: { type: 'integer', minimum: 1 },
636
+ rows: { type: 'integer', minimum: 1 },
637
+ },
638
+ },
639
+ },
640
+ },
641
+ },
642
+ responses: {
643
+ '200': {
644
+ description: 'Terminal resized',
645
+ content: {
646
+ 'application/json': {
647
+ schema: {
648
+ type: 'object',
649
+ required: ['success'],
650
+ properties: { success: { type: 'boolean' } },
651
+ },
652
+ },
653
+ },
654
+ },
655
+ '400': { description: 'Invalid terminal size' },
656
+ '404': { description: 'Terminal not found' },
657
+ },
658
+ },
659
+ async (c) => {
660
+ const id = c.req.param('id');
661
+ const terminal = terminalManager.get(id);
293
662
 
294
- app.post('/v1/terminals/:id/resize', async (c) => {
295
- const id = c.req.param('id');
296
- const terminal = terminalManager.get(id);
663
+ if (!terminal) {
664
+ return c.json({ error: 'Terminal not found' }, 404);
665
+ }
297
666
 
298
- if (!terminal) {
299
- return c.json({ error: 'Terminal not found' }, 404);
300
- }
667
+ try {
668
+ const body = await c.req.json();
669
+ const { cols, rows } = body;
301
670
 
302
- try {
303
- const body = await c.req.json();
304
- const { cols, rows } = body;
671
+ if (!cols || !rows || cols < 1 || rows < 1) {
672
+ return c.json({ error: 'valid cols and rows are required' }, 400);
673
+ }
305
674
 
306
- if (!cols || !rows || cols < 1 || rows < 1) {
307
- return c.json({ error: 'valid cols and rows are required' }, 400);
675
+ terminal.resize(cols, rows);
676
+ return c.json({ success: true });
677
+ } catch (error) {
678
+ const message = error instanceof Error ? error.message : String(error);
679
+ return c.json({ error: message }, 500);
308
680
  }
309
-
310
- terminal.resize(cols, rows);
311
- return c.json({ success: true });
312
- } catch (error) {
313
- const message = error instanceof Error ? error.message : String(error);
314
- return c.json({ error: message }, 500);
315
- }
316
- });
681
+ },
682
+ );
317
683
  }