@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
@@ -13,6 +13,7 @@ import {
13
13
  writeSkillSettings,
14
14
  } from '@ottocode/sdk';
15
15
  import { serializeError } from '../runtime/errors/api-error.ts';
16
+ import { openApiRoute } from '../openapi/route.ts';
16
17
 
17
18
  function dedupeSkillsByName<T extends { name: string }>(skills: T[]): T[] {
18
19
  const seen = new Set<string>();
@@ -47,181 +48,868 @@ function mapSkillsWithEnabled(
47
48
  }
48
49
 
49
50
  export function registerSkillsRoutes(app: Hono) {
50
- app.get('/v1/skills', async (c) => {
51
- try {
52
- const projectRoot = c.req.query('project') || process.cwd();
53
- const cfg = await loadConfig(projectRoot);
54
- const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
55
- const discovered = sortSkillsByName(
56
- await discoverSkills(projectRoot, repoRoot),
57
- );
58
- const filtered = filterDiscoveredSkills(discovered, cfg.skills);
59
- const unique = sortSkillsByName(dedupeSkillsByName(filtered));
60
- return c.json({
61
- skills: mapSkillsWithEnabled(unique, cfg),
62
- });
63
- } catch (error) {
64
- logger.error('Failed to list skills', error);
65
- const errorResponse = serializeError(error);
66
- return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
67
- }
68
- });
51
+ openApiRoute(
52
+ app,
53
+ {
54
+ method: 'get',
55
+ path: '/v1/skills',
56
+ tags: ['config'],
57
+ operationId: 'listSkills',
58
+ summary: 'List discovered skills',
59
+ parameters: [
60
+ {
61
+ in: 'query',
62
+ name: 'project',
63
+ required: false,
64
+ schema: {
65
+ type: 'string',
66
+ },
67
+ description:
68
+ 'Project root override (defaults to current working directory).',
69
+ },
70
+ ],
71
+ responses: {
72
+ '200': {
73
+ description: 'OK',
74
+ content: {
75
+ 'application/json': {
76
+ schema: {
77
+ type: 'object',
78
+ properties: {
79
+ skills: {
80
+ type: 'array',
81
+ items: {
82
+ type: 'object',
83
+ properties: {
84
+ name: {
85
+ type: 'string',
86
+ },
87
+ description: {
88
+ type: 'string',
89
+ },
90
+ scope: {
91
+ type: 'string',
92
+ },
93
+ path: {
94
+ type: 'string',
95
+ },
96
+ },
97
+ required: ['name', 'description', 'scope', 'path'],
98
+ },
99
+ },
100
+ },
101
+ required: ['skills'],
102
+ },
103
+ },
104
+ },
105
+ },
106
+ '500': {
107
+ description: 'Bad Request',
108
+ content: {
109
+ 'application/json': {
110
+ schema: {
111
+ type: 'object',
112
+ properties: {
113
+ error: {
114
+ type: 'string',
115
+ },
116
+ },
117
+ required: ['error'],
118
+ },
119
+ },
120
+ },
121
+ },
122
+ },
123
+ },
124
+ async (c) => {
125
+ try {
126
+ const projectRoot = c.req.query('project') || process.cwd();
127
+ const cfg = await loadConfig(projectRoot);
128
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
129
+ const discovered = sortSkillsByName(
130
+ await discoverSkills(projectRoot, repoRoot),
131
+ );
132
+ const filtered = filterDiscoveredSkills(discovered, cfg.skills);
133
+ const unique = sortSkillsByName(dedupeSkillsByName(filtered));
134
+ return c.json({
135
+ skills: mapSkillsWithEnabled(unique, cfg),
136
+ });
137
+ } catch (error) {
138
+ logger.error('Failed to list skills', error);
139
+ const errorResponse = serializeError(error);
140
+ return c.json(
141
+ errorResponse,
142
+ (errorResponse.error.status || 500) as 500,
143
+ );
144
+ }
145
+ },
146
+ );
69
147
 
70
- app.get('/v1/config/skills', async (c) => {
71
- try {
72
- const projectRoot = c.req.query('project') || process.cwd();
73
- const cfg = await loadConfig(projectRoot);
74
- const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
75
- const discovered = sortSkillsByName(
76
- dedupeSkillsByName(await discoverSkills(projectRoot, repoRoot)),
77
- );
78
- const filtered = sortSkillsByName(
79
- filterDiscoveredSkills(discovered, cfg.skills),
80
- );
81
- return c.json({
82
- enabled: cfg.skills?.enabled !== false,
83
- totalCount: discovered.length,
84
- enabledCount: filtered.length,
85
- items: mapSkillsWithEnabled(discovered, cfg),
86
- });
87
- } catch (error) {
88
- logger.error('Failed to get skills config', error);
89
- const errorResponse = serializeError(error);
90
- return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
91
- }
92
- });
148
+ openApiRoute(
149
+ app,
150
+ {
151
+ method: 'get',
152
+ path: '/v1/config/skills',
153
+ tags: ['config'],
154
+ operationId: 'getSkillsConfig',
155
+ summary: 'Get skills enable/disable config and counts',
156
+ parameters: [
157
+ {
158
+ in: 'query',
159
+ name: 'project',
160
+ required: false,
161
+ schema: {
162
+ type: 'string',
163
+ },
164
+ description:
165
+ 'Project root override (defaults to current working directory).',
166
+ },
167
+ ],
168
+ responses: {
169
+ '200': {
170
+ description: 'OK',
171
+ content: {
172
+ 'application/json': {
173
+ schema: {
174
+ type: 'object',
175
+ properties: {
176
+ enabled: {
177
+ type: 'boolean',
178
+ },
179
+ totalCount: {
180
+ type: 'number',
181
+ },
182
+ enabledCount: {
183
+ type: 'number',
184
+ },
185
+ items: {
186
+ type: 'array',
187
+ items: {
188
+ type: 'object',
189
+ properties: {
190
+ name: {
191
+ type: 'string',
192
+ },
193
+ description: {
194
+ type: 'string',
195
+ },
196
+ scope: {
197
+ type: 'string',
198
+ },
199
+ path: {
200
+ type: 'string',
201
+ },
202
+ enabled: {
203
+ type: 'boolean',
204
+ },
205
+ },
206
+ required: [
207
+ 'name',
208
+ 'description',
209
+ 'scope',
210
+ 'path',
211
+ 'enabled',
212
+ ],
213
+ },
214
+ },
215
+ },
216
+ required: ['enabled', 'totalCount', 'enabledCount', 'items'],
217
+ },
218
+ },
219
+ },
220
+ },
221
+ '500': {
222
+ description: 'Bad Request',
223
+ content: {
224
+ 'application/json': {
225
+ schema: {
226
+ type: 'object',
227
+ properties: {
228
+ error: {
229
+ type: 'string',
230
+ },
231
+ },
232
+ required: ['error'],
233
+ },
234
+ },
235
+ },
236
+ },
237
+ },
238
+ },
239
+ async (c) => {
240
+ try {
241
+ const projectRoot = c.req.query('project') || process.cwd();
242
+ const cfg = await loadConfig(projectRoot);
243
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
244
+ const discovered = sortSkillsByName(
245
+ dedupeSkillsByName(await discoverSkills(projectRoot, repoRoot)),
246
+ );
247
+ const filtered = sortSkillsByName(
248
+ filterDiscoveredSkills(discovered, cfg.skills),
249
+ );
250
+ return c.json({
251
+ enabled: cfg.skills?.enabled !== false,
252
+ totalCount: discovered.length,
253
+ enabledCount: filtered.length,
254
+ items: mapSkillsWithEnabled(discovered, cfg),
255
+ });
256
+ } catch (error) {
257
+ logger.error('Failed to get skills config', error);
258
+ const errorResponse = serializeError(error);
259
+ return c.json(
260
+ errorResponse,
261
+ (errorResponse.error.status || 500) as 500,
262
+ );
263
+ }
264
+ },
265
+ );
93
266
 
94
- app.put('/v1/config/skills', async (c) => {
95
- try {
96
- const projectRoot = c.req.query('project') || process.cwd();
97
- const body = await c.req.json<{
98
- enabled?: boolean;
99
- items?: Record<string, { enabled?: boolean }>;
100
- }>();
101
- await writeSkillSettings(
102
- 'global',
267
+ openApiRoute(
268
+ app,
269
+ {
270
+ method: 'put',
271
+ path: '/v1/config/skills',
272
+ tags: ['config'],
273
+ operationId: 'updateSkillsConfig',
274
+ summary: 'Update skills enable/disable config',
275
+ parameters: [
103
276
  {
104
- ...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
105
- ...(body.items ? { items: body.items } : {}),
106
- },
107
- projectRoot,
108
- );
109
- const cfg = await loadConfig(projectRoot);
110
- const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
111
- const discovered = sortSkillsByName(
112
- dedupeSkillsByName(await discoverSkills(projectRoot, repoRoot)),
113
- );
114
- const filtered = sortSkillsByName(
115
- filterDiscoveredSkills(discovered, cfg.skills),
116
- );
117
- return c.json({
118
- success: true,
119
- enabled: cfg.skills?.enabled !== false,
120
- totalCount: discovered.length,
121
- enabledCount: filtered.length,
122
- items: mapSkillsWithEnabled(discovered, cfg),
123
- });
124
- } catch (error) {
125
- logger.error('Failed to update skills config', error);
126
- const errorResponse = serializeError(error);
127
- return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
128
- }
129
- });
277
+ in: 'query',
278
+ name: 'project',
279
+ required: false,
280
+ schema: {
281
+ type: 'string',
282
+ },
283
+ description:
284
+ 'Project root override (defaults to current working directory).',
285
+ },
286
+ ],
287
+ requestBody: {
288
+ required: true,
289
+ content: {
290
+ 'application/json': {
291
+ schema: {
292
+ type: 'object',
293
+ properties: {
294
+ enabled: {
295
+ type: 'boolean',
296
+ },
297
+ items: {
298
+ type: 'object',
299
+ additionalProperties: {
300
+ type: 'object',
301
+ properties: {
302
+ enabled: {
303
+ type: 'boolean',
304
+ },
305
+ },
306
+ },
307
+ },
308
+ },
309
+ },
310
+ },
311
+ },
312
+ },
313
+ responses: {
314
+ '200': {
315
+ description: 'OK',
316
+ content: {
317
+ 'application/json': {
318
+ schema: {
319
+ type: 'object',
320
+ properties: {
321
+ success: {
322
+ type: 'boolean',
323
+ },
324
+ enabled: {
325
+ type: 'boolean',
326
+ },
327
+ totalCount: {
328
+ type: 'number',
329
+ },
330
+ enabledCount: {
331
+ type: 'number',
332
+ },
333
+ items: {
334
+ type: 'array',
335
+ items: {
336
+ type: 'object',
337
+ properties: {
338
+ name: {
339
+ type: 'string',
340
+ },
341
+ description: {
342
+ type: 'string',
343
+ },
344
+ scope: {
345
+ type: 'string',
346
+ },
347
+ path: {
348
+ type: 'string',
349
+ },
350
+ enabled: {
351
+ type: 'boolean',
352
+ },
353
+ },
354
+ required: [
355
+ 'name',
356
+ 'description',
357
+ 'scope',
358
+ 'path',
359
+ 'enabled',
360
+ ],
361
+ },
362
+ },
363
+ },
364
+ required: [
365
+ 'success',
366
+ 'enabled',
367
+ 'totalCount',
368
+ 'enabledCount',
369
+ 'items',
370
+ ],
371
+ },
372
+ },
373
+ },
374
+ },
375
+ '500': {
376
+ description: 'Bad Request',
377
+ content: {
378
+ 'application/json': {
379
+ schema: {
380
+ type: 'object',
381
+ properties: {
382
+ error: {
383
+ type: 'string',
384
+ },
385
+ },
386
+ required: ['error'],
387
+ },
388
+ },
389
+ },
390
+ },
391
+ },
392
+ },
393
+ async (c) => {
394
+ try {
395
+ const projectRoot = c.req.query('project') || process.cwd();
396
+ const body = await c.req.json<{
397
+ enabled?: boolean;
398
+ items?: Record<string, { enabled?: boolean }>;
399
+ }>();
400
+ await writeSkillSettings(
401
+ 'global',
402
+ {
403
+ ...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
404
+ ...(body.items ? { items: body.items } : {}),
405
+ },
406
+ projectRoot,
407
+ );
408
+ const cfg = await loadConfig(projectRoot);
409
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
410
+ const discovered = sortSkillsByName(
411
+ dedupeSkillsByName(await discoverSkills(projectRoot, repoRoot)),
412
+ );
413
+ const filtered = sortSkillsByName(
414
+ filterDiscoveredSkills(discovered, cfg.skills),
415
+ );
416
+ return c.json({
417
+ success: true,
418
+ enabled: cfg.skills?.enabled !== false,
419
+ totalCount: discovered.length,
420
+ enabledCount: filtered.length,
421
+ items: mapSkillsWithEnabled(discovered, cfg),
422
+ });
423
+ } catch (error) {
424
+ logger.error('Failed to update skills config', error);
425
+ const errorResponse = serializeError(error);
426
+ return c.json(
427
+ errorResponse,
428
+ (errorResponse.error.status || 500) as 500,
429
+ );
430
+ }
431
+ },
432
+ );
130
433
 
131
- app.get('/v1/skills/:name', async (c) => {
132
- try {
133
- const name = c.req.param('name');
134
- const projectRoot = c.req.query('project') || process.cwd();
135
- const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
136
- await discoverSkills(projectRoot, repoRoot);
434
+ openApiRoute(
435
+ app,
436
+ {
437
+ method: 'get',
438
+ path: '/v1/skills/{name}',
439
+ tags: ['config'],
440
+ operationId: 'getSkill',
441
+ summary: 'Get a skill by name',
442
+ parameters: [
443
+ {
444
+ in: 'path',
445
+ name: 'name',
446
+ required: true,
447
+ schema: {
448
+ type: 'string',
449
+ },
450
+ },
451
+ {
452
+ in: 'query',
453
+ name: 'project',
454
+ required: false,
455
+ schema: {
456
+ type: 'string',
457
+ },
458
+ description:
459
+ 'Project root override (defaults to current working directory).',
460
+ },
461
+ ],
462
+ responses: {
463
+ '200': {
464
+ description: 'OK',
465
+ content: {
466
+ 'application/json': {
467
+ schema: {
468
+ type: 'object',
469
+ properties: {
470
+ name: {
471
+ type: 'string',
472
+ },
473
+ description: {
474
+ type: 'string',
475
+ },
476
+ license: {
477
+ type: 'string',
478
+ nullable: true,
479
+ },
480
+ compatibility: {
481
+ type: 'string',
482
+ nullable: true,
483
+ },
484
+ metadata: {
485
+ type: 'object',
486
+ nullable: true,
487
+ },
488
+ allowedTools: {
489
+ type: 'array',
490
+ items: {
491
+ type: 'string',
492
+ },
493
+ nullable: true,
494
+ },
495
+ path: {
496
+ type: 'string',
497
+ },
498
+ scope: {
499
+ type: 'string',
500
+ },
501
+ content: {
502
+ type: 'string',
503
+ },
504
+ },
505
+ required: ['name', 'description', 'path', 'scope', 'content'],
506
+ },
507
+ },
508
+ },
509
+ },
510
+ '404': {
511
+ description: 'Bad Request',
512
+ content: {
513
+ 'application/json': {
514
+ schema: {
515
+ type: 'object',
516
+ properties: {
517
+ error: {
518
+ type: 'string',
519
+ },
520
+ },
521
+ required: ['error'],
522
+ },
523
+ },
524
+ },
525
+ },
526
+ '500': {
527
+ description: 'Bad Request',
528
+ content: {
529
+ 'application/json': {
530
+ schema: {
531
+ type: 'object',
532
+ properties: {
533
+ error: {
534
+ type: 'string',
535
+ },
536
+ },
537
+ required: ['error'],
538
+ },
539
+ },
540
+ },
541
+ },
542
+ },
543
+ },
544
+ async (c) => {
545
+ try {
546
+ const name = c.req.param('name');
547
+ const projectRoot = c.req.query('project') || process.cwd();
548
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
549
+ await discoverSkills(projectRoot, repoRoot);
550
+
551
+ const skill = await loadSkill(name);
552
+ if (!skill) {
553
+ return c.json({ error: `Skill '${name}' not found` }, 404);
554
+ }
137
555
 
138
- const skill = await loadSkill(name);
139
- if (!skill) {
140
- return c.json({ error: `Skill '${name}' not found` }, 404);
556
+ return c.json({
557
+ name: skill.metadata.name,
558
+ description: skill.metadata.description,
559
+ license: skill.metadata.license ?? null,
560
+ compatibility: skill.metadata.compatibility ?? null,
561
+ metadata: skill.metadata.metadata ?? null,
562
+ allowedTools: skill.metadata.allowedTools ?? null,
563
+ path: skill.path,
564
+ scope: skill.scope,
565
+ content: skill.content,
566
+ });
567
+ } catch (error) {
568
+ logger.error('Failed to load skill', error);
569
+ const errorResponse = serializeError(error);
570
+ return c.json(
571
+ errorResponse,
572
+ (errorResponse.error.status || 500) as 500,
573
+ );
141
574
  }
575
+ },
576
+ );
142
577
 
143
- return c.json({
144
- name: skill.metadata.name,
145
- description: skill.metadata.description,
146
- license: skill.metadata.license ?? null,
147
- compatibility: skill.metadata.compatibility ?? null,
148
- metadata: skill.metadata.metadata ?? null,
149
- allowedTools: skill.metadata.allowedTools ?? null,
150
- path: skill.path,
151
- scope: skill.scope,
152
- content: skill.content,
153
- });
154
- } catch (error) {
155
- logger.error('Failed to load skill', error);
156
- const errorResponse = serializeError(error);
157
- return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
158
- }
159
- });
578
+ openApiRoute(
579
+ app,
580
+ {
581
+ method: 'get',
582
+ path: '/v1/skills/{name}/files',
583
+ tags: ['config'],
584
+ operationId: 'listSkillFiles',
585
+ summary: 'List files in a skill directory',
586
+ parameters: [
587
+ {
588
+ in: 'path',
589
+ name: 'name',
590
+ required: true,
591
+ schema: {
592
+ type: 'string',
593
+ },
594
+ },
595
+ {
596
+ in: 'query',
597
+ name: 'project',
598
+ required: false,
599
+ schema: {
600
+ type: 'string',
601
+ },
602
+ description:
603
+ 'Project root override (defaults to current working directory).',
604
+ },
605
+ ],
606
+ responses: {
607
+ '200': {
608
+ description: 'OK',
609
+ content: {
610
+ 'application/json': {
611
+ schema: {
612
+ type: 'object',
613
+ properties: {
614
+ files: {
615
+ type: 'array',
616
+ items: {
617
+ type: 'object',
618
+ properties: {
619
+ relativePath: {
620
+ type: 'string',
621
+ },
622
+ size: {
623
+ type: 'number',
624
+ },
625
+ },
626
+ required: ['relativePath', 'size'],
627
+ },
628
+ },
629
+ },
630
+ required: ['files'],
631
+ },
632
+ },
633
+ },
634
+ },
635
+ '500': {
636
+ description: 'Bad Request',
637
+ content: {
638
+ 'application/json': {
639
+ schema: {
640
+ type: 'object',
641
+ properties: {
642
+ error: {
643
+ type: 'string',
644
+ },
645
+ },
646
+ required: ['error'],
647
+ },
648
+ },
649
+ },
650
+ },
651
+ },
652
+ },
653
+ async (c) => {
654
+ try {
655
+ const name = c.req.param('name');
656
+ const projectRoot = c.req.query('project') || process.cwd();
657
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
658
+ await discoverSkills(projectRoot, repoRoot);
160
659
 
161
- app.get('/v1/skills/:name/files', async (c) => {
162
- try {
163
- const name = c.req.param('name');
164
- const projectRoot = c.req.query('project') || process.cwd();
165
- const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
166
- await discoverSkills(projectRoot, repoRoot);
167
-
168
- const files = await discoverSkillFiles(name);
169
- return c.json({ files });
170
- } catch (error) {
171
- logger.error('Failed to list skill files', error);
172
- const errorResponse = serializeError(error);
173
- return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
174
- }
175
- });
660
+ const files = await discoverSkillFiles(name);
661
+ return c.json({ files });
662
+ } catch (error) {
663
+ logger.error('Failed to list skill files', error);
664
+ const errorResponse = serializeError(error);
665
+ return c.json(
666
+ errorResponse,
667
+ (errorResponse.error.status || 500) as 500,
668
+ );
669
+ }
670
+ },
671
+ );
176
672
 
177
- app.get('/v1/skills/:name/files/*', async (c) => {
178
- try {
179
- const name = c.req.param('name');
180
- const filePath = c.req.path.replace(`/v1/skills/${name}/files/`, '');
181
- const projectRoot = c.req.query('project') || process.cwd();
182
- const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
183
- await discoverSkills(projectRoot, repoRoot);
673
+ openApiRoute(
674
+ app,
675
+ {
676
+ method: 'get',
677
+ path: '/v1/skills/{name}/files/{filePath}',
678
+ tags: ['config'],
679
+ operationId: 'getSkillFile',
680
+ summary: 'Read a specific file from a skill directory',
681
+ parameters: [
682
+ {
683
+ in: 'path',
684
+ name: 'name',
685
+ required: true,
686
+ schema: {
687
+ type: 'string',
688
+ },
689
+ },
690
+ {
691
+ in: 'path',
692
+ name: 'filePath',
693
+ required: true,
694
+ schema: {
695
+ type: 'string',
696
+ },
697
+ },
698
+ {
699
+ in: 'query',
700
+ name: 'project',
701
+ required: false,
702
+ schema: {
703
+ type: 'string',
704
+ },
705
+ description:
706
+ 'Project root override (defaults to current working directory).',
707
+ },
708
+ ],
709
+ responses: {
710
+ '200': {
711
+ description: 'OK',
712
+ content: {
713
+ 'application/json': {
714
+ schema: {
715
+ type: 'object',
716
+ properties: {
717
+ content: {
718
+ type: 'string',
719
+ },
720
+ path: {
721
+ type: 'string',
722
+ },
723
+ },
724
+ required: ['content', 'path'],
725
+ },
726
+ },
727
+ },
728
+ },
729
+ '404': {
730
+ description: 'Bad Request',
731
+ content: {
732
+ 'application/json': {
733
+ schema: {
734
+ type: 'object',
735
+ properties: {
736
+ error: {
737
+ type: 'string',
738
+ },
739
+ },
740
+ required: ['error'],
741
+ },
742
+ },
743
+ },
744
+ },
745
+ '500': {
746
+ description: 'Bad Request',
747
+ content: {
748
+ 'application/json': {
749
+ schema: {
750
+ type: 'object',
751
+ properties: {
752
+ error: {
753
+ type: 'string',
754
+ },
755
+ },
756
+ required: ['error'],
757
+ },
758
+ },
759
+ },
760
+ },
761
+ },
762
+ },
763
+ async (c) => {
764
+ try {
765
+ const name = c.req.param('name');
766
+ const filePath = c.req.path.replace(`/v1/skills/${name}/files/`, '');
767
+ const projectRoot = c.req.query('project') || process.cwd();
768
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
769
+ await discoverSkills(projectRoot, repoRoot);
184
770
 
185
- const result = await loadSkillFile(name, filePath);
186
- if (!result) {
771
+ const result = await loadSkillFile(name, filePath);
772
+ if (!result) {
773
+ return c.json(
774
+ { error: `File '${filePath}' not found in skill '${name}'` },
775
+ 404,
776
+ );
777
+ }
778
+ return c.json({ content: result.content, path: result.resolvedPath });
779
+ } catch (error) {
780
+ logger.error('Failed to load skill file', error);
781
+ const errorResponse = serializeError(error);
187
782
  return c.json(
188
- { error: `File '${filePath}' not found in skill '${name}'` },
189
- 404,
783
+ errorResponse,
784
+ (errorResponse.error.status || 500) as 500,
190
785
  );
191
786
  }
192
- return c.json({ content: result.content, path: result.resolvedPath });
193
- } catch (error) {
194
- logger.error('Failed to load skill file', error);
195
- const errorResponse = serializeError(error);
196
- return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
197
- }
198
- });
787
+ },
788
+ );
199
789
 
200
- app.post('/v1/skills/validate', async (c) => {
201
- try {
202
- const body = await c.req.json<{ content: string; path?: string }>();
203
- if (!body.content) {
204
- return c.json({ error: 'content is required' }, 400);
205
- }
790
+ openApiRoute(
791
+ app,
792
+ {
793
+ method: 'post',
794
+ path: '/v1/skills/validate',
795
+ tags: ['config'],
796
+ operationId: 'validateSkill',
797
+ summary: 'Validate a SKILL.md content',
798
+ requestBody: {
799
+ required: true,
800
+ content: {
801
+ 'application/json': {
802
+ schema: {
803
+ type: 'object',
804
+ properties: {
805
+ content: {
806
+ type: 'string',
807
+ },
808
+ path: {
809
+ type: 'string',
810
+ },
811
+ },
812
+ required: ['content'],
813
+ },
814
+ },
815
+ },
816
+ },
817
+ responses: {
818
+ '200': {
819
+ description: 'OK',
820
+ content: {
821
+ 'application/json': {
822
+ schema: {
823
+ type: 'object',
824
+ properties: {
825
+ valid: {
826
+ type: 'boolean',
827
+ },
828
+ name: {
829
+ type: 'string',
830
+ },
831
+ description: {
832
+ type: 'string',
833
+ },
834
+ license: {
835
+ type: 'string',
836
+ nullable: true,
837
+ },
838
+ error: {
839
+ type: 'string',
840
+ },
841
+ },
842
+ required: ['valid'],
843
+ },
844
+ },
845
+ },
846
+ },
847
+ },
848
+ },
849
+ async (c) => {
850
+ try {
851
+ const body = await c.req.json<{ content: string; path?: string }>();
852
+ if (!body.content) {
853
+ return c.json({ error: 'content is required' }, 400);
854
+ }
206
855
 
207
- const skillPath = body.path ?? 'SKILL.md';
208
- const skill = parseSkillFile(body.content, skillPath, 'cwd');
209
- return c.json({
210
- valid: true,
211
- name: skill.metadata.name,
212
- description: skill.metadata.description,
213
- license: skill.metadata.license ?? null,
214
- });
215
- } catch (error) {
216
- return c.json({
217
- valid: false,
218
- error: (error as Error).message,
219
- });
220
- }
221
- });
856
+ const skillPath = body.path ?? 'SKILL.md';
857
+ const skill = parseSkillFile(body.content, skillPath, 'cwd');
858
+ return c.json({
859
+ valid: true,
860
+ name: skill.metadata.name,
861
+ description: skill.metadata.description,
862
+ license: skill.metadata.license ?? null,
863
+ });
864
+ } catch (error) {
865
+ return c.json({
866
+ valid: false,
867
+ error: (error as Error).message,
868
+ });
869
+ }
870
+ },
871
+ );
222
872
 
223
- app.get('/v1/skills/validate-name/:name', async (c) => {
224
- const name = c.req.param('name');
225
- return c.json({ valid: validateSkillName(name) });
226
- });
873
+ openApiRoute(
874
+ app,
875
+ {
876
+ method: 'get',
877
+ path: '/v1/skills/validate-name/{name}',
878
+ tags: ['config'],
879
+ operationId: 'validateSkillName',
880
+ summary: 'Check if a skill name is valid',
881
+ parameters: [
882
+ {
883
+ in: 'path',
884
+ name: 'name',
885
+ required: true,
886
+ schema: {
887
+ type: 'string',
888
+ },
889
+ },
890
+ ],
891
+ responses: {
892
+ '200': {
893
+ description: 'OK',
894
+ content: {
895
+ 'application/json': {
896
+ schema: {
897
+ type: 'object',
898
+ properties: {
899
+ valid: {
900
+ type: 'boolean',
901
+ },
902
+ },
903
+ required: ['valid'],
904
+ },
905
+ },
906
+ },
907
+ },
908
+ },
909
+ },
910
+ async (c) => {
911
+ const name = c.req.param('name');
912
+ return c.json({ valid: validateSkillName(name) });
913
+ },
914
+ );
227
915
  }