@ottocode/server 0.1.260 → 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 (67) 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/ask/service.ts +1 -0
  42. package/src/runtime/message/compaction-limits.ts +3 -3
  43. package/src/runtime/provider/reasoning.ts +2 -1
  44. package/src/runtime/session/db-operations.ts +4 -3
  45. package/src/runtime/utils/token.ts +7 -2
  46. package/src/tools/adapter.ts +21 -0
  47. package/src/openapi/paths/ask.ts +0 -81
  48. package/src/openapi/paths/auth.ts +0 -687
  49. package/src/openapi/paths/branch.ts +0 -102
  50. package/src/openapi/paths/config.ts +0 -485
  51. package/src/openapi/paths/doctor.ts +0 -165
  52. package/src/openapi/paths/files.ts +0 -236
  53. package/src/openapi/paths/git.ts +0 -690
  54. package/src/openapi/paths/mcp.ts +0 -339
  55. package/src/openapi/paths/messages.ts +0 -103
  56. package/src/openapi/paths/ottorouter.ts +0 -594
  57. package/src/openapi/paths/provider-usage.ts +0 -59
  58. package/src/openapi/paths/research.ts +0 -227
  59. package/src/openapi/paths/session-approval.ts +0 -93
  60. package/src/openapi/paths/session-extras.ts +0 -336
  61. package/src/openapi/paths/session-files.ts +0 -91
  62. package/src/openapi/paths/sessions.ts +0 -210
  63. package/src/openapi/paths/skills.ts +0 -377
  64. package/src/openapi/paths/stream.ts +0 -26
  65. package/src/openapi/paths/terminals.ts +0 -226
  66. package/src/openapi/paths/tunnel.ts +0 -163
  67. package/src/openapi/spec.ts +0 -73
@@ -5,6 +5,7 @@ import { messages, messageParts, sessions } from '@ottocode/database/schema';
5
5
  import { eq, and, inArray } from 'drizzle-orm';
6
6
  import { serializeError } from '../runtime/errors/api-error.ts';
7
7
  import { logger } from '@ottocode/sdk';
8
+ import { openApiRoute } from '../openapi/route.ts';
8
9
 
9
10
  const FILE_EDIT_TOOLS = [
10
11
  'Write',
@@ -253,160 +254,290 @@ function getOperationType(toolName: string): 'write' | 'patch' | 'create' {
253
254
  }
254
255
 
255
256
  export function registerSessionFilesRoutes(app: Hono) {
256
- app.get('/v1/sessions/:sessionId/files', async (c) => {
257
- try {
258
- const sessionId = c.req.param('sessionId');
259
- const projectRoot = c.req.query('project') || process.cwd();
260
- const cfg = await loadConfig(projectRoot);
261
- const db = await getDb(cfg.projectRoot);
262
-
263
- const sessionRows = await db
264
- .select()
265
- .from(sessions)
266
- .where(eq(sessions.id, sessionId))
267
- .limit(1);
268
-
269
- if (!sessionRows.length) {
270
- return c.json({ error: 'Session not found' }, 404);
271
- }
257
+ openApiRoute(
258
+ app,
259
+ {
260
+ method: 'get',
261
+ path: '/v1/sessions/{sessionId}/files',
262
+ tags: ['sessions'],
263
+ operationId: 'getSessionFiles',
264
+ summary: 'Get files modified in a session',
265
+ parameters: [
266
+ {
267
+ in: 'path',
268
+ name: 'sessionId',
269
+ required: true,
270
+ schema: {
271
+ type: 'string',
272
+ },
273
+ },
274
+ {
275
+ in: 'query',
276
+ name: 'project',
277
+ required: false,
278
+ schema: {
279
+ type: 'string',
280
+ },
281
+ description:
282
+ 'Project root override (defaults to current working directory).',
283
+ },
284
+ ],
285
+ responses: {
286
+ '200': {
287
+ description: 'OK',
288
+ content: {
289
+ 'application/json': {
290
+ schema: {
291
+ type: 'object',
292
+ properties: {
293
+ files: {
294
+ type: 'array',
295
+ items: {
296
+ type: 'object',
297
+ properties: {
298
+ path: {
299
+ type: 'string',
300
+ },
301
+ operationCount: {
302
+ type: 'integer',
303
+ },
304
+ firstModified: {
305
+ type: 'integer',
306
+ },
307
+ lastModified: {
308
+ type: 'integer',
309
+ },
310
+ operations: {
311
+ type: 'array',
312
+ items: {
313
+ type: 'object',
314
+ properties: {
315
+ path: {
316
+ type: 'string',
317
+ },
318
+ operation: {
319
+ type: 'string',
320
+ enum: ['write', 'patch', 'create'],
321
+ },
322
+ timestamp: {
323
+ type: 'integer',
324
+ },
325
+ toolCallId: {
326
+ type: 'string',
327
+ },
328
+ toolName: {
329
+ type: 'string',
330
+ },
331
+ },
332
+ required: [
333
+ 'path',
334
+ 'operation',
335
+ 'timestamp',
336
+ 'toolCallId',
337
+ 'toolName',
338
+ ],
339
+ },
340
+ },
341
+ },
342
+ required: [
343
+ 'path',
344
+ 'operationCount',
345
+ 'firstModified',
346
+ 'lastModified',
347
+ 'operations',
348
+ ],
349
+ },
350
+ },
351
+ totalFiles: {
352
+ type: 'integer',
353
+ },
354
+ totalOperations: {
355
+ type: 'integer',
356
+ },
357
+ },
358
+ required: ['files', 'totalFiles', 'totalOperations'],
359
+ },
360
+ },
361
+ },
362
+ },
363
+ '404': {
364
+ description: 'Session not found',
365
+ content: {
366
+ 'application/json': {
367
+ schema: {
368
+ type: 'object',
369
+ properties: {
370
+ error: {
371
+ type: 'string',
372
+ },
373
+ },
374
+ required: ['error'],
375
+ },
376
+ },
377
+ },
378
+ },
379
+ },
380
+ },
381
+ async (c) => {
382
+ try {
383
+ const sessionId = c.req.param('sessionId');
384
+ const projectRoot = c.req.query('project') || process.cwd();
385
+ const cfg = await loadConfig(projectRoot);
386
+ const db = await getDb(cfg.projectRoot);
387
+
388
+ const sessionRows = await db
389
+ .select()
390
+ .from(sessions)
391
+ .where(eq(sessions.id, sessionId))
392
+ .limit(1);
393
+
394
+ if (!sessionRows.length) {
395
+ return c.json({ error: 'Session not found' }, 404);
396
+ }
272
397
 
273
- const messageRows = await db
274
- .select({ id: messages.id })
275
- .from(messages)
276
- .where(eq(messages.sessionId, sessionId));
398
+ const messageRows = await db
399
+ .select({ id: messages.id })
400
+ .from(messages)
401
+ .where(eq(messages.sessionId, sessionId));
277
402
 
278
- const messageIds = messageRows.map((m) => m.id);
403
+ const messageIds = messageRows.map((m) => m.id);
279
404
 
280
- if (!messageIds.length) {
281
- return c.json({
282
- files: [],
283
- totalFiles: 0,
284
- totalOperations: 0,
285
- });
286
- }
287
-
288
- const parts = await db
289
- .select()
290
- .from(messageParts)
291
- .where(
292
- and(
293
- inArray(messageParts.messageId, messageIds),
294
- inArray(messageParts.toolName, FILE_EDIT_TOOLS),
295
- ),
296
- );
297
-
298
- const fileOperationsMap = new Map<string, FileOperation[]>();
299
- const toolCallDataMap = new Map<
300
- string,
301
- { patch?: string; content?: string }
302
- >();
303
-
304
- for (const part of parts) {
305
- if (!part.toolName) continue;
306
-
307
- let content: unknown;
308
- try {
309
- content = JSON.parse(part.content);
310
- } catch {
311
- continue;
405
+ if (!messageIds.length) {
406
+ return c.json({
407
+ files: [],
408
+ totalFiles: 0,
409
+ totalOperations: 0,
410
+ });
312
411
  }
313
412
 
314
- if (part.type === 'tool_call') {
315
- const callId = part.toolCallId || part.id;
316
- const patch = extractPatchFromToolCall(part.toolName, content);
317
- const writeContent = extractContentFromToolCall(
318
- part.toolName,
319
- content,
413
+ const parts = await db
414
+ .select()
415
+ .from(messageParts)
416
+ .where(
417
+ and(
418
+ inArray(messageParts.messageId, messageIds),
419
+ inArray(messageParts.toolName, FILE_EDIT_TOOLS),
420
+ ),
320
421
  );
321
422
 
322
- toolCallDataMap.set(callId, { patch, content: writeContent });
323
-
324
- const path = extractFilePathFromToolCall(part.toolName, content);
325
- if (path) {
326
- const operation: FileOperation = {
327
- path,
328
- operation: getOperationType(part.toolName),
329
- timestamp: part.startedAt || Date.now(),
330
- toolCallId: callId,
331
- toolName: part.toolName,
332
- patch,
333
- content: writeContent,
334
- };
335
-
336
- const existing = fileOperationsMap.get(path) || [];
337
- const isDuplicate = existing.some(
338
- (op) => op.toolCallId === operation.toolCallId,
339
- );
340
- if (!isDuplicate) {
341
- existing.push(operation);
342
- fileOperationsMap.set(path, existing);
343
- }
423
+ const fileOperationsMap = new Map<string, FileOperation[]>();
424
+ const toolCallDataMap = new Map<
425
+ string,
426
+ { patch?: string; content?: string }
427
+ >();
428
+
429
+ for (const part of parts) {
430
+ if (!part.toolName) continue;
431
+
432
+ let content: unknown;
433
+ try {
434
+ content = JSON.parse(part.content);
435
+ } catch {
436
+ continue;
344
437
  }
345
- } else if (part.type === 'tool_result') {
346
- const filePaths = extractFilesFromToolResult(part.toolName, content);
347
- const { patch, writeContent, artifact } = extractDataFromToolResult(
348
- part.toolName,
349
- content,
350
- );
351
- const callId = part.toolCallId || part.id;
352
- const callData = toolCallDataMap.get(callId);
353
438
 
354
- for (const filePath of filePaths) {
355
- if (!filePath) continue;
439
+ if (part.type === 'tool_call') {
440
+ const callId = part.toolCallId || part.id;
441
+ const patch = extractPatchFromToolCall(part.toolName, content);
442
+ const writeContent = extractContentFromToolCall(
443
+ part.toolName,
444
+ content,
445
+ );
356
446
 
357
- const existing = fileOperationsMap.get(filePath) || [];
358
- const existingOp = existing.find((op) => op.toolCallId === callId);
447
+ toolCallDataMap.set(callId, { patch, content: writeContent });
359
448
 
360
- if (existingOp) {
361
- existingOp.artifact = artifact;
362
- existingOp.timestamp = part.completedAt || existingOp.timestamp;
363
- if (!existingOp.patch && patch) {
364
- existingOp.patch = patch;
365
- }
366
- if (!existingOp.content && writeContent) {
367
- existingOp.content = writeContent;
368
- }
369
- } else {
449
+ const path = extractFilePathFromToolCall(part.toolName, content);
450
+ if (path) {
370
451
  const operation: FileOperation = {
371
- path: filePath,
452
+ path,
372
453
  operation: getOperationType(part.toolName),
373
- timestamp: part.completedAt || part.startedAt || Date.now(),
454
+ timestamp: part.startedAt || Date.now(),
374
455
  toolCallId: callId,
375
456
  toolName: part.toolName,
376
- patch: callData?.patch ?? patch,
377
- content: callData?.content ?? writeContent,
378
- artifact,
457
+ patch,
458
+ content: writeContent,
379
459
  };
380
- existing.push(operation);
381
- fileOperationsMap.set(filePath, existing);
460
+
461
+ const existing = fileOperationsMap.get(path) || [];
462
+ const isDuplicate = existing.some(
463
+ (op) => op.toolCallId === operation.toolCallId,
464
+ );
465
+ if (!isDuplicate) {
466
+ existing.push(operation);
467
+ fileOperationsMap.set(path, existing);
468
+ }
469
+ }
470
+ } else if (part.type === 'tool_result') {
471
+ const filePaths = extractFilesFromToolResult(
472
+ part.toolName,
473
+ content,
474
+ );
475
+ const { patch, writeContent, artifact } = extractDataFromToolResult(
476
+ part.toolName,
477
+ content,
478
+ );
479
+ const callId = part.toolCallId || part.id;
480
+ const callData = toolCallDataMap.get(callId);
481
+
482
+ for (const filePath of filePaths) {
483
+ if (!filePath) continue;
484
+
485
+ const existing = fileOperationsMap.get(filePath) || [];
486
+ const existingOp = existing.find(
487
+ (op) => op.toolCallId === callId,
488
+ );
489
+
490
+ if (existingOp) {
491
+ existingOp.artifact = artifact;
492
+ existingOp.timestamp = part.completedAt || existingOp.timestamp;
493
+ if (!existingOp.patch && patch) {
494
+ existingOp.patch = patch;
495
+ }
496
+ if (!existingOp.content && writeContent) {
497
+ existingOp.content = writeContent;
498
+ }
499
+ } else {
500
+ const operation: FileOperation = {
501
+ path: filePath,
502
+ operation: getOperationType(part.toolName),
503
+ timestamp: part.completedAt || part.startedAt || Date.now(),
504
+ toolCallId: callId,
505
+ toolName: part.toolName,
506
+ patch: callData?.patch ?? patch,
507
+ content: callData?.content ?? writeContent,
508
+ artifact,
509
+ };
510
+ existing.push(operation);
511
+ fileOperationsMap.set(filePath, existing);
512
+ }
382
513
  }
383
514
  }
384
515
  }
385
- }
386
516
 
387
- const files: SessionFile[] = [];
388
- for (const [path, operations] of fileOperationsMap) {
389
- operations.sort((a, b) => a.timestamp - b.timestamp);
390
- files.push({
391
- path,
392
- operations,
393
- operationCount: operations.length,
394
- firstModified: operations[0]?.timestamp || 0,
395
- lastModified: operations[operations.length - 1]?.timestamp || 0,
517
+ const files: SessionFile[] = [];
518
+ for (const [path, operations] of fileOperationsMap) {
519
+ operations.sort((a, b) => a.timestamp - b.timestamp);
520
+ files.push({
521
+ path,
522
+ operations,
523
+ operationCount: operations.length,
524
+ firstModified: operations[0]?.timestamp || 0,
525
+ lastModified: operations[operations.length - 1]?.timestamp || 0,
526
+ });
527
+ }
528
+
529
+ files.sort((a, b) => b.lastModified - a.lastModified);
530
+
531
+ return c.json({
532
+ files,
533
+ totalFiles: files.length,
534
+ totalOperations: parts.length,
396
535
  });
536
+ } catch (error) {
537
+ logger.error('Failed to get session files', error);
538
+ const errorResponse = serializeError(error);
539
+ return c.json(errorResponse, errorResponse.error.status || 500);
397
540
  }
398
-
399
- files.sort((a, b) => b.lastModified - a.lastModified);
400
-
401
- return c.json({
402
- files,
403
- totalFiles: files.length,
404
- totalOperations: parts.length,
405
- });
406
- } catch (error) {
407
- logger.error('Failed to get session files', error);
408
- const errorResponse = serializeError(error);
409
- return c.json(errorResponse, errorResponse.error.status || 500);
410
- }
411
- });
541
+ },
542
+ );
412
543
  }