@ottocode/server 0.1.264 → 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 (74) 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/ask/service.ts +1 -0
  64. package/src/runtime/provider/custom.ts +73 -0
  65. package/src/runtime/provider/index.ts +6 -85
  66. package/src/runtime/provider/reasoning-builders.ts +280 -0
  67. package/src/runtime/provider/reasoning.ts +68 -264
  68. package/src/runtime/provider/xai.ts +8 -0
  69. package/src/tools/adapter/events.ts +116 -0
  70. package/src/tools/adapter/execution.ts +160 -0
  71. package/src/tools/adapter/pending.ts +37 -0
  72. package/src/tools/adapter/persistence.ts +166 -0
  73. package/src/tools/adapter/results.ts +97 -0
  74. package/src/tools/adapter.ts +124 -451
@@ -0,0 +1,189 @@
1
+ import type { Hono } from 'hono';
2
+ import {
3
+ catalog,
4
+ isBuiltInProviderId,
5
+ removeAuth,
6
+ setAuth,
7
+ type ProviderId,
8
+ } from '@ottocode/sdk';
9
+ import { logger } from '@ottocode/sdk';
10
+ import { openApiRoute } from '../../openapi/route.ts';
11
+ import { serializeError } from '../../runtime/errors/api-error.ts';
12
+
13
+ export function registerAuthProviderRoutes(app: Hono) {
14
+ openApiRoute(
15
+ app,
16
+ {
17
+ method: 'post',
18
+ path: '/v1/auth/{provider}',
19
+ tags: ['auth'],
20
+ operationId: 'addProviderApiKey',
21
+ summary: 'Add API key for a provider',
22
+ parameters: [
23
+ {
24
+ in: 'path',
25
+ name: 'provider',
26
+ required: true,
27
+ schema: {
28
+ type: 'string',
29
+ },
30
+ },
31
+ ],
32
+ requestBody: {
33
+ required: true,
34
+ content: {
35
+ 'application/json': {
36
+ schema: {
37
+ type: 'object',
38
+ properties: {
39
+ apiKey: {
40
+ type: 'string',
41
+ },
42
+ },
43
+ required: ['apiKey'],
44
+ },
45
+ },
46
+ },
47
+ },
48
+ responses: {
49
+ '200': {
50
+ description: 'OK',
51
+ content: {
52
+ 'application/json': {
53
+ schema: {
54
+ type: 'object',
55
+ properties: {
56
+ success: {
57
+ type: 'boolean',
58
+ },
59
+ provider: {
60
+ type: 'string',
61
+ },
62
+ },
63
+ required: ['success', 'provider'],
64
+ },
65
+ },
66
+ },
67
+ },
68
+ '400': {
69
+ description: 'Bad Request',
70
+ content: {
71
+ 'application/json': {
72
+ schema: {
73
+ type: 'object',
74
+ properties: {
75
+ error: {
76
+ type: 'string',
77
+ },
78
+ },
79
+ required: ['error'],
80
+ },
81
+ },
82
+ },
83
+ },
84
+ },
85
+ },
86
+ async (c) => {
87
+ try {
88
+ const provider = c.req.param('provider') as ProviderId;
89
+ const { apiKey } = await c.req.json<{ apiKey: string }>();
90
+
91
+ if (!isBuiltInProviderId(provider) || !catalog[provider]) {
92
+ return c.json({ error: 'Unknown provider' }, 400);
93
+ }
94
+
95
+ if (!apiKey) {
96
+ return c.json({ error: 'API key required' }, 400);
97
+ }
98
+
99
+ await setAuth(
100
+ provider,
101
+ { type: 'api', key: apiKey },
102
+ undefined,
103
+ 'global',
104
+ );
105
+
106
+ return c.json({ success: true, provider });
107
+ } catch (error) {
108
+ logger.error('Failed to add provider', error);
109
+ const errorResponse = serializeError(error);
110
+ return c.json(errorResponse, errorResponse.error.status || 500);
111
+ }
112
+ },
113
+ );
114
+
115
+ openApiRoute(
116
+ app,
117
+ {
118
+ method: 'delete',
119
+ path: '/v1/auth/{provider}',
120
+ tags: ['auth'],
121
+ operationId: 'removeProvider',
122
+ summary: 'Remove auth for a provider',
123
+ parameters: [
124
+ {
125
+ in: 'path',
126
+ name: 'provider',
127
+ required: true,
128
+ schema: {
129
+ type: 'string',
130
+ },
131
+ },
132
+ ],
133
+ responses: {
134
+ '200': {
135
+ description: 'OK',
136
+ content: {
137
+ 'application/json': {
138
+ schema: {
139
+ type: 'object',
140
+ properties: {
141
+ success: {
142
+ type: 'boolean',
143
+ },
144
+ provider: {
145
+ type: 'string',
146
+ },
147
+ },
148
+ required: ['success', 'provider'],
149
+ },
150
+ },
151
+ },
152
+ },
153
+ '400': {
154
+ description: 'Bad Request',
155
+ content: {
156
+ 'application/json': {
157
+ schema: {
158
+ type: 'object',
159
+ properties: {
160
+ error: {
161
+ type: 'string',
162
+ },
163
+ },
164
+ required: ['error'],
165
+ },
166
+ },
167
+ },
168
+ },
169
+ },
170
+ },
171
+ async (c) => {
172
+ try {
173
+ const provider = c.req.param('provider') as ProviderId;
174
+
175
+ if (!isBuiltInProviderId(provider) || !catalog[provider]) {
176
+ return c.json({ error: 'Unknown provider' }, 400);
177
+ }
178
+
179
+ await removeAuth(provider, undefined, 'global');
180
+
181
+ return c.json({ success: true, provider });
182
+ } catch (error) {
183
+ logger.error('Failed to remove provider', error);
184
+ const errorResponse = serializeError(error);
185
+ return c.json(errorResponse, errorResponse.error.status || 500);
186
+ }
187
+ },
188
+ );
189
+ }
@@ -0,0 +1,167 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models';
4
+ const GH_CAPABILITY_CACHE_TTL_MS = 60 * 1000;
5
+
6
+ let ghCapabilityCache: {
7
+ expiresAt: number;
8
+ value: { available: boolean; authenticated: boolean; reason?: string };
9
+ } = {
10
+ expiresAt: 0,
11
+ value: {
12
+ available: false,
13
+ authenticated: false,
14
+ reason: 'Not checked yet',
15
+ },
16
+ };
17
+
18
+ export function getGhImportCapability() {
19
+ if (ghCapabilityCache.expiresAt > Date.now()) return ghCapabilityCache.value;
20
+
21
+ const version = spawnSync('gh', ['--version'], {
22
+ encoding: 'utf8',
23
+ stdio: ['ignore', 'pipe', 'pipe'],
24
+ });
25
+ if (version.status !== 0) {
26
+ ghCapabilityCache = {
27
+ expiresAt: Date.now() + GH_CAPABILITY_CACHE_TTL_MS,
28
+ value: {
29
+ available: false,
30
+ authenticated: false,
31
+ reason: 'GitHub CLI (gh) is not installed',
32
+ },
33
+ };
34
+ return ghCapabilityCache.value;
35
+ }
36
+
37
+ const authStatus = spawnSync('gh', ['auth', 'status', '-h', 'github.com'], {
38
+ encoding: 'utf8',
39
+ stdio: ['ignore', 'pipe', 'pipe'],
40
+ });
41
+ if (authStatus.status !== 0) {
42
+ ghCapabilityCache = {
43
+ expiresAt: Date.now() + GH_CAPABILITY_CACHE_TTL_MS,
44
+ value: {
45
+ available: true,
46
+ authenticated: false,
47
+ reason: 'Run `gh auth login` first',
48
+ },
49
+ };
50
+ return ghCapabilityCache.value;
51
+ }
52
+
53
+ ghCapabilityCache = {
54
+ expiresAt: Date.now() + GH_CAPABILITY_CACHE_TTL_MS,
55
+ value: {
56
+ available: true,
57
+ authenticated: true,
58
+ },
59
+ };
60
+ return ghCapabilityCache.value;
61
+ }
62
+
63
+ export function parseErrorMessageFromBody(text: string): string | undefined {
64
+ if (!text) return undefined;
65
+ try {
66
+ const parsed = JSON.parse(text) as {
67
+ message?: string;
68
+ error?: { message?: string };
69
+ };
70
+ return parsed.error?.message ?? parsed.message;
71
+ } catch {
72
+ return undefined;
73
+ }
74
+ }
75
+
76
+ export async function fetchCopilotModels(token: string): Promise<
77
+ | {
78
+ ok: true;
79
+ models: Set<string>;
80
+ }
81
+ | {
82
+ ok: false;
83
+ status: number;
84
+ message: string;
85
+ }
86
+ > {
87
+ try {
88
+ const response = await fetch(COPILOT_MODELS_URL, {
89
+ headers: {
90
+ Authorization: `Bearer ${token}`,
91
+ 'Openai-Intent': 'conversation-edits',
92
+ 'User-Agent': 'ottocode',
93
+ },
94
+ });
95
+ const text = await response.text();
96
+ if (!response.ok) {
97
+ return {
98
+ ok: false,
99
+ status: response.status,
100
+ message:
101
+ parseErrorMessageFromBody(text) ||
102
+ `Copilot models endpoint returned ${response.status}`,
103
+ };
104
+ }
105
+
106
+ const payload = JSON.parse(text) as {
107
+ data?: Array<{ id?: string }>;
108
+ };
109
+ const models = new Set(
110
+ (payload.data ?? [])
111
+ .map((item) => item.id)
112
+ .filter((id): id is string => Boolean(id)),
113
+ );
114
+ return { ok: true, models };
115
+ } catch (error) {
116
+ const message =
117
+ error instanceof Error ? error.message : 'Failed to fetch Copilot models';
118
+ return { ok: false, status: 0, message };
119
+ }
120
+ }
121
+
122
+ export async function detectOAuthOrgRestriction(token: string): Promise<{
123
+ restricted: boolean;
124
+ org?: string;
125
+ message?: string;
126
+ }> {
127
+ try {
128
+ const orgsResponse = await fetch('https://api.github.com/user/orgs', {
129
+ headers: {
130
+ Authorization: `Bearer ${token}`,
131
+ 'User-Agent': 'ottocode',
132
+ Accept: 'application/vnd.github+json',
133
+ },
134
+ });
135
+ if (!orgsResponse.ok) {
136
+ return { restricted: false };
137
+ }
138
+
139
+ const orgs = (await orgsResponse.json()) as Array<{ login?: string }>;
140
+ for (const org of orgs) {
141
+ if (!org.login) continue;
142
+ const membershipResponse = await fetch(
143
+ `https://api.github.com/user/memberships/orgs/${org.login}`,
144
+ {
145
+ headers: {
146
+ Authorization: `Bearer ${token}`,
147
+ 'User-Agent': 'ottocode',
148
+ Accept: 'application/vnd.github+json',
149
+ },
150
+ },
151
+ );
152
+ if (membershipResponse.status !== 403) continue;
153
+
154
+ const bodyText = await membershipResponse.text();
155
+ const message = parseErrorMessageFromBody(bodyText) || bodyText;
156
+ if (message.includes('enabled OAuth App access restrictions')) {
157
+ return {
158
+ restricted: true,
159
+ org: org.login,
160
+ message,
161
+ };
162
+ }
163
+ }
164
+ } catch {}
165
+
166
+ return { restricted: false };
167
+ }
@@ -0,0 +1,23 @@
1
+ export const oauthVerifiers = new Map<
2
+ string,
3
+ { verifier: string; provider: string; createdAt: number; callbackUrl: string }
4
+ >();
5
+
6
+ export const copilotDeviceSessions = new Map<
7
+ string,
8
+ { deviceCode: string; interval: number; provider: string; createdAt: number }
9
+ >();
10
+
11
+ setInterval(() => {
12
+ const now = Date.now();
13
+ for (const [key, value] of oauthVerifiers.entries()) {
14
+ if (now - value.createdAt > 10 * 60 * 1000) {
15
+ oauthVerifiers.delete(key);
16
+ }
17
+ }
18
+ for (const [key, value] of copilotDeviceSessions.entries()) {
19
+ if (now - value.createdAt > 10 * 60 * 1000) {
20
+ copilotDeviceSessions.delete(key);
21
+ }
22
+ }
23
+ }, 60 * 1000);
@@ -0,0 +1,203 @@
1
+ import type { Hono } from 'hono';
2
+ import {
3
+ catalog,
4
+ getAllAuth,
5
+ getOnboardingComplete,
6
+ getOttoRouterWallet,
7
+ loadConfig,
8
+ type ProviderId,
9
+ } from '@ottocode/sdk';
10
+ import { logger } from '@ottocode/sdk';
11
+ import { openApiRoute } from '../../openapi/route.ts';
12
+ import { serializeError } from '../../runtime/errors/api-error.ts';
13
+ import { getProviderDetails } from '../config/utils.ts';
14
+ import { getGhImportCapability } from './service.ts';
15
+
16
+ export function registerAuthStatusRoutes(app: Hono) {
17
+ openApiRoute(
18
+ app,
19
+ {
20
+ method: 'get',
21
+ path: '/v1/auth/status',
22
+ tags: ['auth'],
23
+ operationId: 'getAuthStatus',
24
+ summary: 'Get auth status for all providers',
25
+ responses: {
26
+ '200': {
27
+ description: 'OK',
28
+ content: {
29
+ 'application/json': {
30
+ schema: {
31
+ type: 'object',
32
+ properties: {
33
+ onboardingComplete: {
34
+ type: 'boolean',
35
+ },
36
+ ottorouter: {
37
+ type: 'object',
38
+ properties: {
39
+ configured: {
40
+ type: 'boolean',
41
+ },
42
+ publicKey: {
43
+ type: 'string',
44
+ },
45
+ },
46
+ required: ['configured'],
47
+ },
48
+ providers: {
49
+ type: 'object',
50
+ additionalProperties: {
51
+ type: 'object',
52
+ properties: {
53
+ configured: {
54
+ type: 'boolean',
55
+ },
56
+ type: {
57
+ type: 'string',
58
+ enum: ['api', 'oauth', 'wallet'],
59
+ },
60
+ label: {
61
+ type: 'string',
62
+ },
63
+ supportsOAuth: {
64
+ type: 'boolean',
65
+ },
66
+ supportsToken: {
67
+ type: 'boolean',
68
+ },
69
+ supportsGhImport: {
70
+ type: 'boolean',
71
+ },
72
+ modelCount: {
73
+ type: 'integer',
74
+ },
75
+ costRange: {
76
+ type: 'object',
77
+ nullable: true,
78
+ properties: {
79
+ min: {
80
+ type: 'number',
81
+ },
82
+ max: {
83
+ type: 'number',
84
+ },
85
+ },
86
+ required: ['min', 'max'],
87
+ },
88
+ },
89
+ required: [
90
+ 'configured',
91
+ 'label',
92
+ 'supportsOAuth',
93
+ 'modelCount',
94
+ ],
95
+ },
96
+ },
97
+ defaults: {
98
+ type: 'object',
99
+ properties: {
100
+ agent: {
101
+ type: 'string',
102
+ },
103
+ provider: {
104
+ type: 'string',
105
+ },
106
+ model: {
107
+ type: 'string',
108
+ },
109
+ },
110
+ },
111
+ },
112
+ required: ['onboardingComplete', 'ottorouter', 'providers'],
113
+ },
114
+ },
115
+ },
116
+ },
117
+ },
118
+ },
119
+ async (c) => {
120
+ try {
121
+ const projectRoot = process.cwd();
122
+ const auth = await getAllAuth(projectRoot);
123
+ const cfg = await loadConfig(projectRoot);
124
+ const onboardingComplete = await getOnboardingComplete(projectRoot);
125
+ const ottorouterWallet = await getOttoRouterWallet(projectRoot);
126
+ const ghImportCapability = getGhImportCapability();
127
+
128
+ const providers: Record<
129
+ string,
130
+ {
131
+ configured: boolean;
132
+ type?: 'api' | 'oauth' | 'wallet';
133
+ label: string;
134
+ supportsOAuth: boolean;
135
+ supportsToken?: boolean;
136
+ supportsGhImport?: boolean;
137
+ custom?: boolean;
138
+ modelCount: number;
139
+ costRange?: { min: number; max: number };
140
+ }
141
+ > = {};
142
+
143
+ for (const [id, entry] of Object.entries(catalog)) {
144
+ const providerAuth = auth[id as ProviderId];
145
+ const models = entry.models || [];
146
+ const costs = models
147
+ .map((m) => m.cost?.input)
148
+ .filter((c): c is number => c !== undefined);
149
+
150
+ providers[id] = {
151
+ configured: !!providerAuth,
152
+ type: providerAuth?.type,
153
+ label: entry.label || id,
154
+ supportsOAuth:
155
+ id === 'anthropic' || id === 'openai' || id === 'copilot',
156
+ supportsToken: id === 'copilot',
157
+ supportsGhImport:
158
+ id === 'copilot' ? ghImportCapability.available : false,
159
+ modelCount: models.length,
160
+ costRange:
161
+ costs.length > 0
162
+ ? {
163
+ min: Math.min(...costs),
164
+ max: Math.max(...costs),
165
+ }
166
+ : undefined,
167
+ };
168
+ }
169
+
170
+ const providerDetails = await getProviderDetails(undefined, cfg);
171
+ for (const detail of providerDetails) {
172
+ if (!detail.custom || providers[detail.id]) continue;
173
+ providers[detail.id] = {
174
+ configured: detail.authorized,
175
+ type: detail.authType,
176
+ label: detail.label,
177
+ supportsOAuth: false,
178
+ custom: true,
179
+ modelCount: detail.modelCount,
180
+ };
181
+ }
182
+
183
+ return c.json({
184
+ onboardingComplete,
185
+ ottorouter: ottorouterWallet
186
+ ? {
187
+ configured: true,
188
+ publicKey: ottorouterWallet.publicKey,
189
+ }
190
+ : {
191
+ configured: false,
192
+ },
193
+ providers,
194
+ defaults: cfg.defaults,
195
+ });
196
+ } catch (error) {
197
+ logger.error('Failed to get auth status', error);
198
+ const errorResponse = serializeError(error);
199
+ return c.json(errorResponse, errorResponse.error.status || 500);
200
+ }
201
+ },
202
+ );
203
+ }