@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,221 @@
1
+ import type { Context } from 'hono';
2
+ import {
3
+ discoverSkillFiles,
4
+ discoverSkills,
5
+ filterDiscoveredSkills,
6
+ findGitRoot,
7
+ loadConfig,
8
+ loadSkill,
9
+ loadSkillFile,
10
+ logger,
11
+ parseSkillFile,
12
+ validateSkillName,
13
+ writeSkillSettings,
14
+ } from '@ottocode/sdk';
15
+ import { serializeError } from '../../runtime/errors/api-error.ts';
16
+
17
+ function dedupeSkillsByName<T extends { name: string }>(skills: T[]): T[] {
18
+ const seen = new Set<string>();
19
+ return skills.filter((skill) => {
20
+ const key = skill.name.trim();
21
+ if (!key || seen.has(key)) return false;
22
+ seen.add(key);
23
+ return true;
24
+ });
25
+ }
26
+
27
+ function sortSkillsByName<T extends { name: string }>(skills: T[]): T[] {
28
+ return [...skills].sort((a, b) => a.name.localeCompare(b.name));
29
+ }
30
+
31
+ function mapSkillsWithEnabled(
32
+ discovered: Array<{
33
+ name: string;
34
+ description: string;
35
+ scope: string;
36
+ path: string;
37
+ }>,
38
+ cfg: Awaited<ReturnType<typeof loadConfig>>,
39
+ ) {
40
+ return discovered.map((skill) => ({
41
+ name: skill.name,
42
+ description: skill.description,
43
+ scope: skill.scope,
44
+ path: skill.path,
45
+ enabled: cfg.skills?.items?.[skill.name]?.enabled !== false,
46
+ }));
47
+ }
48
+
49
+ async function loadProjectSkills(projectRoot: string) {
50
+ const cfg = await loadConfig(projectRoot);
51
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
52
+ const discovered = sortSkillsByName(
53
+ dedupeSkillsByName(await discoverSkills(projectRoot, repoRoot)),
54
+ );
55
+ const filtered = sortSkillsByName(
56
+ filterDiscoveredSkills(discovered, cfg.skills),
57
+ );
58
+ return { cfg, discovered, filtered };
59
+ }
60
+
61
+ async function ensureSkillsDiscovered(projectRoot: string): Promise<void> {
62
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
63
+ await discoverSkills(projectRoot, repoRoot);
64
+ }
65
+
66
+ function projectRootFromQuery(c: Context): string {
67
+ return c.req.query('project') || process.cwd();
68
+ }
69
+
70
+ function jsonError(c: Context, message: string, error: unknown) {
71
+ logger.error(message, error);
72
+ const errorResponse = serializeError(error);
73
+ return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
74
+ }
75
+
76
+ export async function listSkills(c: Context) {
77
+ try {
78
+ const projectRoot = projectRootFromQuery(c);
79
+ const cfg = await loadConfig(projectRoot);
80
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
81
+ const discovered = sortSkillsByName(
82
+ await discoverSkills(projectRoot, repoRoot),
83
+ );
84
+ const filtered = filterDiscoveredSkills(discovered, cfg.skills);
85
+ const unique = sortSkillsByName(dedupeSkillsByName(filtered));
86
+ return c.json({
87
+ skills: mapSkillsWithEnabled(unique, cfg),
88
+ });
89
+ } catch (error) {
90
+ return jsonError(c, 'Failed to list skills', error);
91
+ }
92
+ }
93
+
94
+ export async function getSkillsConfig(c: Context) {
95
+ try {
96
+ const { cfg, discovered, filtered } = await loadProjectSkills(
97
+ projectRootFromQuery(c),
98
+ );
99
+ return c.json({
100
+ enabled: cfg.skills?.enabled !== false,
101
+ totalCount: discovered.length,
102
+ enabledCount: filtered.length,
103
+ items: mapSkillsWithEnabled(discovered, cfg),
104
+ });
105
+ } catch (error) {
106
+ return jsonError(c, 'Failed to get skills config', error);
107
+ }
108
+ }
109
+
110
+ export async function updateSkillsConfig(c: Context) {
111
+ try {
112
+ const projectRoot = projectRootFromQuery(c);
113
+ const body = await c.req.json<{
114
+ enabled?: boolean;
115
+ items?: Record<string, { enabled?: boolean }>;
116
+ }>();
117
+ await writeSkillSettings(
118
+ 'global',
119
+ {
120
+ ...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
121
+ ...(body.items ? { items: body.items } : {}),
122
+ },
123
+ projectRoot,
124
+ );
125
+
126
+ const { cfg, discovered, filtered } = await loadProjectSkills(projectRoot);
127
+ return c.json({
128
+ success: true,
129
+ enabled: cfg.skills?.enabled !== false,
130
+ totalCount: discovered.length,
131
+ enabledCount: filtered.length,
132
+ items: mapSkillsWithEnabled(discovered, cfg),
133
+ });
134
+ } catch (error) {
135
+ return jsonError(c, 'Failed to update skills config', error);
136
+ }
137
+ }
138
+
139
+ export async function getSkill(c: Context) {
140
+ try {
141
+ const name = c.req.param('name');
142
+ await ensureSkillsDiscovered(projectRootFromQuery(c));
143
+
144
+ const skill = await loadSkill(name);
145
+ if (!skill) {
146
+ return c.json({ error: `Skill '${name}' not found` }, 404);
147
+ }
148
+
149
+ return c.json({
150
+ name: skill.metadata.name,
151
+ description: skill.metadata.description,
152
+ license: skill.metadata.license ?? null,
153
+ compatibility: skill.metadata.compatibility ?? null,
154
+ metadata: skill.metadata.metadata ?? null,
155
+ allowedTools: skill.metadata.allowedTools ?? null,
156
+ path: skill.path,
157
+ scope: skill.scope,
158
+ content: skill.content,
159
+ });
160
+ } catch (error) {
161
+ return jsonError(c, 'Failed to load skill', error);
162
+ }
163
+ }
164
+
165
+ export async function listSkillFiles(c: Context) {
166
+ try {
167
+ const name = c.req.param('name');
168
+ await ensureSkillsDiscovered(projectRootFromQuery(c));
169
+ const files = await discoverSkillFiles(name);
170
+ return c.json({ files });
171
+ } catch (error) {
172
+ return jsonError(c, 'Failed to list skill files', error);
173
+ }
174
+ }
175
+
176
+ export async function getSkillFile(c: Context) {
177
+ try {
178
+ const name = c.req.param('name');
179
+ const filePath = c.req.path.replace(`/v1/skills/${name}/files/`, '');
180
+ await ensureSkillsDiscovered(projectRootFromQuery(c));
181
+
182
+ const result = await loadSkillFile(name, filePath);
183
+ if (!result) {
184
+ return c.json(
185
+ { error: `File '${filePath}' not found in skill '${name}'` },
186
+ 404,
187
+ );
188
+ }
189
+ return c.json({ content: result.content, path: result.resolvedPath });
190
+ } catch (error) {
191
+ return jsonError(c, 'Failed to load skill file', error);
192
+ }
193
+ }
194
+
195
+ export async function validateSkill(c: Context) {
196
+ try {
197
+ const body = await c.req.json<{ content: string; path?: string }>();
198
+ if (!body.content) {
199
+ return c.json({ error: 'content is required' }, 400);
200
+ }
201
+
202
+ const skillPath = body.path ?? 'SKILL.md';
203
+ const skill = parseSkillFile(body.content, skillPath, 'cwd');
204
+ return c.json({
205
+ valid: true,
206
+ name: skill.metadata.name,
207
+ description: skill.metadata.description,
208
+ license: skill.metadata.license ?? null,
209
+ });
210
+ } catch (error) {
211
+ return c.json({
212
+ valid: false,
213
+ error: (error as Error).message,
214
+ });
215
+ }
216
+ }
217
+
218
+ export function validateSkillNameRoute(c: Context) {
219
+ const name = c.req.param('name');
220
+ return c.json({ valid: validateSkillName(name) });
221
+ }
@@ -0,0 +1,309 @@
1
+ const projectQueryParameter = {
2
+ in: 'query',
3
+ name: 'project',
4
+ required: false,
5
+ schema: { type: 'string' },
6
+ description: 'Project root override (defaults to current working directory).',
7
+ };
8
+
9
+ const skillNamePathParameter = {
10
+ in: 'path',
11
+ name: 'name',
12
+ required: true,
13
+ schema: { type: 'string' },
14
+ };
15
+
16
+ const errorResponse = {
17
+ description: 'Bad Request',
18
+ content: {
19
+ 'application/json': {
20
+ schema: {
21
+ type: 'object',
22
+ properties: {
23
+ error: { type: 'string' },
24
+ },
25
+ required: ['error'],
26
+ },
27
+ },
28
+ },
29
+ };
30
+
31
+ const skillListItemSchema = {
32
+ type: 'object',
33
+ properties: {
34
+ name: { type: 'string' },
35
+ description: { type: 'string' },
36
+ scope: { type: 'string' },
37
+ path: { type: 'string' },
38
+ },
39
+ required: ['name', 'description', 'scope', 'path'],
40
+ };
41
+
42
+ const skillConfigItemSchema = {
43
+ type: 'object',
44
+ properties: {
45
+ name: { type: 'string' },
46
+ description: { type: 'string' },
47
+ scope: { type: 'string' },
48
+ path: { type: 'string' },
49
+ enabled: { type: 'boolean' },
50
+ },
51
+ required: ['name', 'description', 'scope', 'path', 'enabled'],
52
+ };
53
+
54
+ const skillConfigSchema = {
55
+ type: 'object',
56
+ properties: {
57
+ enabled: { type: 'boolean' },
58
+ totalCount: { type: 'number' },
59
+ enabledCount: { type: 'number' },
60
+ items: {
61
+ type: 'array',
62
+ items: skillConfigItemSchema,
63
+ },
64
+ },
65
+ required: ['enabled', 'totalCount', 'enabledCount', 'items'],
66
+ };
67
+
68
+ const skillConfigUpdateSchema = {
69
+ type: 'object',
70
+ properties: {
71
+ enabled: { type: 'boolean' },
72
+ items: {
73
+ type: 'object',
74
+ additionalProperties: {
75
+ type: 'object',
76
+ properties: {
77
+ enabled: { type: 'boolean' },
78
+ },
79
+ },
80
+ },
81
+ },
82
+ };
83
+
84
+ const skillConfigUpdateResponseSchema = {
85
+ type: 'object',
86
+ properties: {
87
+ success: { type: 'boolean' },
88
+ ...skillConfigSchema.properties,
89
+ },
90
+ required: ['success', 'enabled', 'totalCount', 'enabledCount', 'items'],
91
+ };
92
+
93
+ const skillDetailSchema = {
94
+ type: 'object',
95
+ properties: {
96
+ name: { type: 'string' },
97
+ description: { type: 'string' },
98
+ license: { type: 'string', nullable: true },
99
+ compatibility: { type: 'string', nullable: true },
100
+ metadata: { type: 'object', nullable: true },
101
+ allowedTools: {
102
+ type: 'array',
103
+ items: { type: 'string' },
104
+ nullable: true,
105
+ },
106
+ path: { type: 'string' },
107
+ scope: { type: 'string' },
108
+ content: { type: 'string' },
109
+ },
110
+ required: ['name', 'description', 'path', 'scope', 'content'],
111
+ };
112
+
113
+ const skillFileListSchema = {
114
+ type: 'object',
115
+ properties: {
116
+ files: {
117
+ type: 'array',
118
+ items: {
119
+ type: 'object',
120
+ properties: {
121
+ relativePath: { type: 'string' },
122
+ size: { type: 'number' },
123
+ },
124
+ required: ['relativePath', 'size'],
125
+ },
126
+ },
127
+ },
128
+ required: ['files'],
129
+ };
130
+
131
+ const skillFileSchema = {
132
+ type: 'object',
133
+ properties: {
134
+ content: { type: 'string' },
135
+ path: { type: 'string' },
136
+ },
137
+ required: ['content', 'path'],
138
+ };
139
+
140
+ const validateSkillRequestSchema = {
141
+ type: 'object',
142
+ properties: {
143
+ content: { type: 'string' },
144
+ path: { type: 'string' },
145
+ },
146
+ required: ['content'],
147
+ };
148
+
149
+ const validateSkillResponseSchema = {
150
+ type: 'object',
151
+ properties: {
152
+ valid: { type: 'boolean' },
153
+ name: { type: 'string' },
154
+ description: { type: 'string' },
155
+ license: { type: 'string', nullable: true },
156
+ error: { type: 'string' },
157
+ },
158
+ required: ['valid'],
159
+ };
160
+
161
+ const validateNameResponseSchema = {
162
+ type: 'object',
163
+ properties: {
164
+ valid: { type: 'boolean' },
165
+ },
166
+ required: ['valid'],
167
+ };
168
+
169
+ function jsonResponse(schema: object, description = 'OK') {
170
+ return {
171
+ description,
172
+ content: {
173
+ 'application/json': { schema },
174
+ },
175
+ };
176
+ }
177
+
178
+ export const listSkillsSpec = {
179
+ method: 'get',
180
+ path: '/v1/skills',
181
+ tags: ['config'],
182
+ operationId: 'listSkills',
183
+ summary: 'List discovered skills',
184
+ parameters: [projectQueryParameter],
185
+ responses: {
186
+ '200': jsonResponse({
187
+ type: 'object',
188
+ properties: {
189
+ skills: {
190
+ type: 'array',
191
+ items: skillListItemSchema,
192
+ },
193
+ },
194
+ required: ['skills'],
195
+ }),
196
+ '500': errorResponse,
197
+ },
198
+ };
199
+
200
+ export const getSkillsConfigSpec = {
201
+ method: 'get',
202
+ path: '/v1/config/skills',
203
+ tags: ['config'],
204
+ operationId: 'getSkillsConfig',
205
+ summary: 'Get skills enable/disable config and counts',
206
+ parameters: [projectQueryParameter],
207
+ responses: {
208
+ '200': jsonResponse(skillConfigSchema),
209
+ '500': errorResponse,
210
+ },
211
+ };
212
+
213
+ export const updateSkillsConfigSpec = {
214
+ method: 'put',
215
+ path: '/v1/config/skills',
216
+ tags: ['config'],
217
+ operationId: 'updateSkillsConfig',
218
+ summary: 'Update skills enable/disable config',
219
+ parameters: [projectQueryParameter],
220
+ requestBody: {
221
+ required: true,
222
+ content: {
223
+ 'application/json': { schema: skillConfigUpdateSchema },
224
+ },
225
+ },
226
+ responses: {
227
+ '200': jsonResponse(skillConfigUpdateResponseSchema),
228
+ '500': errorResponse,
229
+ },
230
+ };
231
+
232
+ export const getSkillSpec = {
233
+ method: 'get',
234
+ path: '/v1/skills/{name}',
235
+ tags: ['config'],
236
+ operationId: 'getSkill',
237
+ summary: 'Get a skill by name',
238
+ parameters: [skillNamePathParameter, projectQueryParameter],
239
+ responses: {
240
+ '200': jsonResponse(skillDetailSchema),
241
+ '404': errorResponse,
242
+ '500': errorResponse,
243
+ },
244
+ };
245
+
246
+ export const listSkillFilesSpec = {
247
+ method: 'get',
248
+ path: '/v1/skills/{name}/files',
249
+ tags: ['config'],
250
+ operationId: 'listSkillFiles',
251
+ summary: 'List files in a skill directory',
252
+ parameters: [skillNamePathParameter, projectQueryParameter],
253
+ responses: {
254
+ '200': jsonResponse(skillFileListSchema),
255
+ '500': errorResponse,
256
+ },
257
+ };
258
+
259
+ export const getSkillFileSpec = {
260
+ method: 'get',
261
+ path: '/v1/skills/{name}/files/{filePath}',
262
+ tags: ['config'],
263
+ operationId: 'getSkillFile',
264
+ summary: 'Read a specific file from a skill directory',
265
+ parameters: [
266
+ skillNamePathParameter,
267
+ {
268
+ in: 'path',
269
+ name: 'filePath',
270
+ required: true,
271
+ schema: { type: 'string' },
272
+ },
273
+ projectQueryParameter,
274
+ ],
275
+ responses: {
276
+ '200': jsonResponse(skillFileSchema),
277
+ '404': errorResponse,
278
+ '500': errorResponse,
279
+ },
280
+ };
281
+
282
+ export const validateSkillSpec = {
283
+ method: 'post',
284
+ path: '/v1/skills/validate',
285
+ tags: ['config'],
286
+ operationId: 'validateSkill',
287
+ summary: 'Validate a SKILL.md content',
288
+ requestBody: {
289
+ required: true,
290
+ content: {
291
+ 'application/json': { schema: validateSkillRequestSchema },
292
+ },
293
+ },
294
+ responses: {
295
+ '200': jsonResponse(validateSkillResponseSchema),
296
+ },
297
+ };
298
+
299
+ export const validateSkillNameSpec = {
300
+ method: 'get',
301
+ path: '/v1/skills/validate-name/{name}',
302
+ tags: ['config'],
303
+ operationId: 'validateSkillName',
304
+ summary: 'Check if a skill name is valid',
305
+ parameters: [skillNamePathParameter],
306
+ responses: {
307
+ '200': jsonResponse(validateNameResponseSchema),
308
+ },
309
+ };