@ottocode/server 0.1.260 → 0.1.262

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
@@ -15,6 +15,7 @@ import {
15
15
  logger,
16
16
  } from '@ottocode/sdk';
17
17
  import { serializeError } from '../runtime/errors/api-error.ts';
18
+ import { openApiRoute } from '../openapi/route.ts';
18
19
 
19
20
  async function fileExists(path: string | null): Promise<boolean> {
20
21
  if (!path) return false;
@@ -46,172 +47,405 @@ async function listDir(dir: string | null): Promise<string[]> {
46
47
  }
47
48
 
48
49
  export function registerDoctorRoutes(app: Hono) {
49
- app.get('/v1/doctor', async (c) => {
50
- try {
51
- const projectRoot = c.req.query('project') || process.cwd();
52
- const { cfg, auth } = await readConfig(projectRoot);
53
- const configuredProviders = getConfiguredProviderIds(cfg, {
54
- includeDisabled: true,
55
- });
56
-
57
- const providers = await Promise.all(
58
- configuredProviders.map(async (id) => {
59
- const ok = await isProviderAuthorized(cfg, id);
60
- const envVar = getConfiguredProviderEnvVar(cfg, id) ?? null;
61
- const envConfigured = envVar ? !!process.env[envVar] : false;
62
-
63
- const globalAuthPath = getSecureAuthPath();
64
- let hasGlobalAuth = false;
65
- if (globalAuthPath) {
66
- const contents =
67
- await readJsonSafe<Record<string, unknown>>(globalAuthPath);
68
- hasGlobalAuth = Boolean(contents?.[id]);
69
- }
50
+ openApiRoute(
51
+ app,
52
+ {
53
+ method: 'get',
54
+ path: '/v1/doctor',
55
+ tags: ['config'],
56
+ operationId: 'runDoctor',
57
+ summary: 'Run diagnostics on the current configuration',
58
+ parameters: [
59
+ {
60
+ in: 'query',
61
+ name: 'project',
62
+ required: false,
63
+ schema: {
64
+ type: 'string',
65
+ },
66
+ description:
67
+ 'Project root override (defaults to current working directory).',
68
+ },
69
+ ],
70
+ responses: {
71
+ '200': {
72
+ description: 'OK',
73
+ content: {
74
+ 'application/json': {
75
+ schema: {
76
+ type: 'object',
77
+ properties: {
78
+ providers: {
79
+ type: 'array',
80
+ items: {
81
+ type: 'object',
82
+ properties: {
83
+ id: {
84
+ type: 'string',
85
+ },
86
+ ok: {
87
+ type: 'boolean',
88
+ },
89
+ configured: {
90
+ type: 'boolean',
91
+ },
92
+ sources: {
93
+ type: 'array',
94
+ items: {
95
+ type: 'string',
96
+ },
97
+ },
98
+ },
99
+ required: ['id', 'ok', 'configured', 'sources'],
100
+ },
101
+ },
102
+ defaults: {
103
+ type: 'object',
104
+ properties: {
105
+ agent: {
106
+ type: 'string',
107
+ },
108
+ provider: {
109
+ type: 'string',
110
+ },
111
+ model: {
112
+ type: 'string',
113
+ },
114
+ providerAuthorized: {
115
+ type: 'boolean',
116
+ },
117
+ },
118
+ required: [
119
+ 'agent',
120
+ 'provider',
121
+ 'model',
122
+ 'providerAuthorized',
123
+ ],
124
+ },
125
+ agents: {
126
+ type: 'object',
127
+ properties: {
128
+ globalPath: {
129
+ type: 'string',
130
+ nullable: true,
131
+ },
132
+ localPath: {
133
+ type: 'string',
134
+ nullable: true,
135
+ },
136
+ globalNames: {
137
+ type: 'array',
138
+ items: {
139
+ type: 'string',
140
+ },
141
+ },
142
+ localNames: {
143
+ type: 'array',
144
+ items: {
145
+ type: 'string',
146
+ },
147
+ },
148
+ },
149
+ required: [
150
+ 'globalPath',
151
+ 'localPath',
152
+ 'globalNames',
153
+ 'localNames',
154
+ ],
155
+ },
156
+ tools: {
157
+ type: 'object',
158
+ properties: {
159
+ defaultNames: {
160
+ type: 'array',
161
+ items: {
162
+ type: 'string',
163
+ },
164
+ },
165
+ globalPath: {
166
+ type: 'string',
167
+ nullable: true,
168
+ },
169
+ globalNames: {
170
+ type: 'array',
171
+ items: {
172
+ type: 'string',
173
+ },
174
+ },
175
+ localPath: {
176
+ type: 'string',
177
+ nullable: true,
178
+ },
179
+ localNames: {
180
+ type: 'array',
181
+ items: {
182
+ type: 'string',
183
+ },
184
+ },
185
+ effectiveNames: {
186
+ type: 'array',
187
+ items: {
188
+ type: 'string',
189
+ },
190
+ },
191
+ },
192
+ required: [
193
+ 'defaultNames',
194
+ 'globalNames',
195
+ 'localNames',
196
+ 'effectiveNames',
197
+ ],
198
+ },
199
+ commands: {
200
+ type: 'object',
201
+ properties: {
202
+ globalPath: {
203
+ type: 'string',
204
+ nullable: true,
205
+ },
206
+ globalNames: {
207
+ type: 'array',
208
+ items: {
209
+ type: 'string',
210
+ },
211
+ },
212
+ localPath: {
213
+ type: 'string',
214
+ nullable: true,
215
+ },
216
+ localNames: {
217
+ type: 'array',
218
+ items: {
219
+ type: 'string',
220
+ },
221
+ },
222
+ },
223
+ required: ['globalNames', 'localNames'],
224
+ },
225
+ issues: {
226
+ type: 'array',
227
+ items: {
228
+ type: 'string',
229
+ },
230
+ },
231
+ suggestions: {
232
+ type: 'array',
233
+ items: {
234
+ type: 'string',
235
+ },
236
+ },
237
+ globalAuthPath: {
238
+ type: 'string',
239
+ nullable: true,
240
+ },
241
+ },
242
+ required: [
243
+ 'providers',
244
+ 'defaults',
245
+ 'agents',
246
+ 'tools',
247
+ 'commands',
248
+ 'issues',
249
+ 'suggestions',
250
+ ],
251
+ },
252
+ },
253
+ },
254
+ },
255
+ '500': {
256
+ description: 'Bad Request',
257
+ content: {
258
+ 'application/json': {
259
+ schema: {
260
+ type: 'object',
261
+ properties: {
262
+ error: {
263
+ type: 'string',
264
+ },
265
+ },
266
+ required: ['error'],
267
+ },
268
+ },
269
+ },
270
+ },
271
+ },
272
+ },
273
+ async (c) => {
274
+ try {
275
+ const projectRoot = c.req.query('project') || process.cwd();
276
+ const { cfg, auth } = await readConfig(projectRoot);
277
+ const configuredProviders = getConfiguredProviderIds(cfg, {
278
+ includeDisabled: true,
279
+ });
280
+
281
+ const providers = await Promise.all(
282
+ configuredProviders.map(async (id) => {
283
+ const ok = await isProviderAuthorized(cfg, id);
284
+ const envVar = getConfiguredProviderEnvVar(cfg, id) ?? null;
285
+ const envConfigured = envVar ? !!process.env[envVar] : false;
286
+
287
+ const globalAuthPath = getSecureAuthPath();
288
+ let hasGlobalAuth = false;
289
+ if (globalAuthPath) {
290
+ const contents =
291
+ await readJsonSafe<Record<string, unknown>>(globalAuthPath);
292
+ hasGlobalAuth = Boolean(contents?.[id]);
293
+ }
294
+
295
+ const authInfo = auth?.[id];
296
+ const hasStoredSecret = (() => {
297
+ if (!authInfo) return false;
298
+ if (authInfo.type === 'api')
299
+ return Boolean((authInfo as { key?: string }).key);
300
+ if (authInfo.type === 'wallet')
301
+ return Boolean((authInfo as { secret?: string }).secret);
302
+ if (authInfo.type === 'oauth')
303
+ return Boolean(
304
+ (authInfo as { access?: string; refresh?: string }).access ||
305
+ (authInfo as { access?: string; refresh?: string }).refresh,
306
+ );
307
+ return false;
308
+ })();
309
+
310
+ const sources: string[] = [];
311
+ if (envConfigured && envVar) sources.push(`env:${envVar}`);
312
+ if (hasGlobalAuth) sources.push('auth.json');
313
+
314
+ const configured =
315
+ envConfigured ||
316
+ hasGlobalAuth ||
317
+ cfg.defaults.provider === id ||
318
+ hasStoredSecret ||
319
+ Boolean(getConfiguredProviderApiKey(cfg, id));
70
320
 
71
- const authInfo = auth?.[id];
72
- const hasStoredSecret = (() => {
73
- if (!authInfo) return false;
74
- if (authInfo.type === 'api')
75
- return Boolean((authInfo as { key?: string }).key);
76
- if (authInfo.type === 'wallet')
77
- return Boolean((authInfo as { secret?: string }).secret);
78
- if (authInfo.type === 'oauth')
79
- return Boolean(
80
- (authInfo as { access?: string; refresh?: string }).access ||
81
- (authInfo as { access?: string; refresh?: string }).refresh,
82
- );
83
- return false;
84
- })();
85
-
86
- const sources: string[] = [];
87
- if (envConfigured && envVar) sources.push(`env:${envVar}`);
88
- if (hasGlobalAuth) sources.push('auth.json');
89
-
90
- const configured =
91
- envConfigured ||
92
- hasGlobalAuth ||
93
- cfg.defaults.provider === id ||
94
- hasStoredSecret ||
95
- Boolean(getConfiguredProviderApiKey(cfg, id));
96
-
97
- return { id, ok, configured, sources };
98
- }),
99
- );
100
-
101
- const defaults = {
102
- agent: cfg.defaults.agent,
103
- provider: cfg.defaults.provider,
104
- model: cfg.defaults.model,
105
- providerAuthorized: await isProviderAuthorized(
106
- cfg,
107
- cfg.defaults.provider,
108
- ),
109
- };
110
-
111
- const globalAgentsPath = getGlobalAgentsJsonPath();
112
- const localAgentsPath = `${projectRoot}/.otto/agents.json`;
113
- const globalAgents =
114
- (await readJsonSafe<Record<string, unknown>>(globalAgentsPath)) ?? {};
115
- const localAgents =
116
- (await readJsonSafe<Record<string, unknown>>(localAgentsPath)) ?? {};
117
-
118
- const agents = {
119
- globalPath: (await fileExists(globalAgentsPath))
120
- ? globalAgentsPath
121
- : null,
122
- localPath: (await fileExists(localAgentsPath)) ? localAgentsPath : null,
123
- globalNames: Object.keys(globalAgents).sort(),
124
- localNames: Object.keys(localAgents).sort(),
125
- };
126
-
127
- const defaultToolNames = Array.from(
128
- new Set([
129
- ...buildFsTools(projectRoot).map((t) => t.name),
130
- ...buildGitTools(projectRoot).map((t) => t.name),
131
- 'finish',
132
- ]),
133
- ).sort();
134
-
135
- const globalToolsDir = getGlobalToolsDir();
136
- const localToolsDir = `${projectRoot}/.otto/tools`;
137
- const globalToolNames = await listDir(globalToolsDir);
138
- const localToolNames = await listDir(localToolsDir);
139
-
140
- const tools = {
141
- defaultNames: defaultToolNames,
142
- globalPath: globalToolNames.length ? globalToolsDir : null,
143
- globalNames: globalToolNames.sort(),
144
- localPath: localToolNames.length ? localToolsDir : null,
145
- localNames: localToolNames.sort(),
146
- effectiveNames: Array.from(
147
- new Set([...defaultToolNames, ...globalToolNames, ...localToolNames]),
148
- ).sort(),
149
- };
150
-
151
- const globalCommandsDir = getGlobalCommandsDir();
152
- const localCommandsDir = `${projectRoot}/.otto/commands`;
153
- const globalCommandFiles = await listDir(globalCommandsDir);
154
- const localCommandFiles = await listDir(localCommandsDir);
155
-
156
- const commands = {
157
- globalPath: globalCommandFiles.length ? globalCommandsDir : null,
158
- globalNames: globalCommandFiles
159
- .filter((f) => f.endsWith('.json'))
160
- .map((f) => f.replace(/\.json$/, ''))
161
- .sort(),
162
- localPath: localCommandFiles.length ? localCommandsDir : null,
163
- localNames: localCommandFiles
164
- .filter((f) => f.endsWith('.json'))
165
- .map((f) => f.replace(/\.json$/, ''))
166
- .sort(),
167
- };
168
-
169
- const issues: string[] = [];
170
- if (!defaults.providerAuthorized) {
171
- issues.push(
172
- `Default provider '${defaults.provider}' is not authorized`,
321
+ return { id, ok, configured, sources };
322
+ }),
173
323
  );
174
- }
175
- for (const [scope, entries] of [
176
- ['global', globalAgents],
177
- ['local', localAgents],
178
- ] as const) {
179
- for (const [name, entry] of Object.entries(entries)) {
180
- if (
181
- entry &&
182
- typeof entry === 'object' &&
183
- Object.hasOwn(entry, 'tools') &&
184
- !Array.isArray((entry as { tools?: unknown }).tools)
185
- ) {
186
- issues.push(`${scope}:${name} tools field must be an array`);
324
+
325
+ const defaults = {
326
+ agent: cfg.defaults.agent,
327
+ provider: cfg.defaults.provider,
328
+ model: cfg.defaults.model,
329
+ providerAuthorized: await isProviderAuthorized(
330
+ cfg,
331
+ cfg.defaults.provider,
332
+ ),
333
+ };
334
+
335
+ const globalAgentsPath = getGlobalAgentsJsonPath();
336
+ const localAgentsPath = `${projectRoot}/.otto/agents.json`;
337
+ const globalAgents =
338
+ (await readJsonSafe<Record<string, unknown>>(globalAgentsPath)) ?? {};
339
+ const localAgents =
340
+ (await readJsonSafe<Record<string, unknown>>(localAgentsPath)) ?? {};
341
+
342
+ const agents = {
343
+ globalPath: (await fileExists(globalAgentsPath))
344
+ ? globalAgentsPath
345
+ : null,
346
+ localPath: (await fileExists(localAgentsPath))
347
+ ? localAgentsPath
348
+ : null,
349
+ globalNames: Object.keys(globalAgents).sort(),
350
+ localNames: Object.keys(localAgents).sort(),
351
+ };
352
+
353
+ const defaultToolNames = Array.from(
354
+ new Set([
355
+ ...buildFsTools(projectRoot).map((t) => t.name),
356
+ ...buildGitTools(projectRoot).map((t) => t.name),
357
+ 'finish',
358
+ ]),
359
+ ).sort();
360
+
361
+ const globalToolsDir = getGlobalToolsDir();
362
+ const localToolsDir = `${projectRoot}/.otto/tools`;
363
+ const globalToolNames = await listDir(globalToolsDir);
364
+ const localToolNames = await listDir(localToolsDir);
365
+
366
+ const tools = {
367
+ defaultNames: defaultToolNames,
368
+ globalPath: globalToolNames.length ? globalToolsDir : null,
369
+ globalNames: globalToolNames.sort(),
370
+ localPath: localToolNames.length ? localToolsDir : null,
371
+ localNames: localToolNames.sort(),
372
+ effectiveNames: Array.from(
373
+ new Set([
374
+ ...defaultToolNames,
375
+ ...globalToolNames,
376
+ ...localToolNames,
377
+ ]),
378
+ ).sort(),
379
+ };
380
+
381
+ const globalCommandsDir = getGlobalCommandsDir();
382
+ const localCommandsDir = `${projectRoot}/.otto/commands`;
383
+ const globalCommandFiles = await listDir(globalCommandsDir);
384
+ const localCommandFiles = await listDir(localCommandsDir);
385
+
386
+ const commands = {
387
+ globalPath: globalCommandFiles.length ? globalCommandsDir : null,
388
+ globalNames: globalCommandFiles
389
+ .filter((f) => f.endsWith('.json'))
390
+ .map((f) => f.replace(/\.json$/, ''))
391
+ .sort(),
392
+ localPath: localCommandFiles.length ? localCommandsDir : null,
393
+ localNames: localCommandFiles
394
+ .filter((f) => f.endsWith('.json'))
395
+ .map((f) => f.replace(/\.json$/, ''))
396
+ .sort(),
397
+ };
398
+
399
+ const issues: string[] = [];
400
+ if (!defaults.providerAuthorized) {
401
+ issues.push(
402
+ `Default provider '${defaults.provider}' is not authorized`,
403
+ );
404
+ }
405
+ for (const [scope, entries] of [
406
+ ['global', globalAgents],
407
+ ['local', localAgents],
408
+ ] as const) {
409
+ for (const [name, entry] of Object.entries(entries)) {
410
+ if (
411
+ entry &&
412
+ typeof entry === 'object' &&
413
+ Object.hasOwn(entry, 'tools') &&
414
+ !Array.isArray((entry as { tools?: unknown }).tools)
415
+ ) {
416
+ issues.push(`${scope}:${name} tools field must be an array`);
417
+ }
187
418
  }
188
419
  }
189
- }
190
420
 
191
- const suggestions: string[] = [];
192
- if (!defaults.providerAuthorized) {
193
- suggestions.push(
194
- `Run: otto auth login ${defaults.provider} — or switch defaults with: otto models`,
421
+ const suggestions: string[] = [];
422
+ if (!defaults.providerAuthorized) {
423
+ suggestions.push(
424
+ `Run: otto auth login ${defaults.provider} — or switch defaults with: otto models`,
425
+ );
426
+ }
427
+ if (issues.length) {
428
+ suggestions.push('Review agents.json fields.');
429
+ }
430
+
431
+ return c.json({
432
+ providers,
433
+ defaults,
434
+ agents,
435
+ tools,
436
+ commands,
437
+ issues,
438
+ suggestions,
439
+ globalAuthPath: getSecureAuthPath(),
440
+ });
441
+ } catch (error) {
442
+ logger.error('Failed to run doctor', error);
443
+ const errorResponse = serializeError(error);
444
+ return c.json(
445
+ errorResponse,
446
+ (errorResponse.error.status || 500) as 500,
195
447
  );
196
448
  }
197
- if (issues.length) {
198
- suggestions.push('Review agents.json fields.');
199
- }
200
-
201
- return c.json({
202
- providers,
203
- defaults,
204
- agents,
205
- tools,
206
- commands,
207
- issues,
208
- suggestions,
209
- globalAuthPath: getSecureAuthPath(),
210
- });
211
- } catch (error) {
212
- logger.error('Failed to run doctor', error);
213
- const errorResponse = serializeError(error);
214
- return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
215
- }
216
- });
449
+ },
450
+ );
217
451
  }