@ottocode/server 0.1.193 → 0.1.194
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.194",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@ottocode/sdk": "0.1.
|
|
33
|
-
"@ottocode/database": "0.1.
|
|
32
|
+
"@ottocode/sdk": "0.1.194",
|
|
33
|
+
"@ottocode/database": "0.1.194",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { registerSessionApprovalRoute } from './routes/session-approval.ts';
|
|
|
20
20
|
import { registerSetuRoutes } from './routes/setu.ts';
|
|
21
21
|
import { registerAuthRoutes } from './routes/auth.ts';
|
|
22
22
|
import { registerTunnelRoutes } from './routes/tunnel.ts';
|
|
23
|
+
import { registerProviderUsageRoutes } from './routes/provider-usage.ts';
|
|
23
24
|
import type { AgentConfigEntry } from './runtime/agent/registry.ts';
|
|
24
25
|
|
|
25
26
|
const globalTerminalManager = new TerminalManager();
|
|
@@ -76,6 +77,7 @@ function initApp() {
|
|
|
76
77
|
registerSetuRoutes(app);
|
|
77
78
|
registerAuthRoutes(app);
|
|
78
79
|
registerTunnelRoutes(app);
|
|
80
|
+
registerProviderUsageRoutes(app);
|
|
79
81
|
|
|
80
82
|
return app;
|
|
81
83
|
}
|
|
@@ -148,6 +150,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
|
148
150
|
registerSetuRoutes(honoApp);
|
|
149
151
|
registerAuthRoutes(honoApp);
|
|
150
152
|
registerTunnelRoutes(honoApp);
|
|
153
|
+
registerProviderUsageRoutes(honoApp);
|
|
151
154
|
|
|
152
155
|
return honoApp;
|
|
153
156
|
}
|
|
@@ -248,6 +251,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
248
251
|
registerSetuRoutes(honoApp);
|
|
249
252
|
registerAuthRoutes(honoApp);
|
|
250
253
|
registerTunnelRoutes(honoApp);
|
|
254
|
+
registerProviderUsageRoutes(honoApp);
|
|
251
255
|
|
|
252
256
|
return honoApp;
|
|
253
257
|
}
|
package/src/openapi/paths/git.ts
CHANGED
|
@@ -350,6 +350,52 @@ export const gitPaths = {
|
|
|
350
350
|
},
|
|
351
351
|
},
|
|
352
352
|
},
|
|
353
|
+
'/v1/git/pull': {
|
|
354
|
+
post: {
|
|
355
|
+
tags: ['git'],
|
|
356
|
+
operationId: 'pullChanges',
|
|
357
|
+
summary: 'Pull changes from remote',
|
|
358
|
+
description: 'Pulls changes from the configured remote repository',
|
|
359
|
+
requestBody: {
|
|
360
|
+
required: false,
|
|
361
|
+
content: {
|
|
362
|
+
'application/json': {
|
|
363
|
+
schema: {
|
|
364
|
+
type: 'object',
|
|
365
|
+
properties: {
|
|
366
|
+
project: { type: 'string' },
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
responses: {
|
|
373
|
+
200: {
|
|
374
|
+
description: 'OK',
|
|
375
|
+
content: {
|
|
376
|
+
'application/json': {
|
|
377
|
+
schema: {
|
|
378
|
+
type: 'object',
|
|
379
|
+
properties: {
|
|
380
|
+
status: { type: 'string', enum: ['ok'] },
|
|
381
|
+
data: {
|
|
382
|
+
type: 'object',
|
|
383
|
+
properties: {
|
|
384
|
+
output: { type: 'string' },
|
|
385
|
+
},
|
|
386
|
+
required: ['output'],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
required: ['status', 'data'],
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
400: gitErrorResponse(),
|
|
395
|
+
500: gitErrorResponse(),
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
353
399
|
'/v1/git/restore': {
|
|
354
400
|
post: {
|
|
355
401
|
tags: ['git'],
|
package/src/routes/git/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { registerDiffRoute } from './diff.ts';
|
|
|
5
5
|
import { registerStagingRoutes } from './staging.ts';
|
|
6
6
|
import { registerCommitRoutes } from './commit.ts';
|
|
7
7
|
import { registerPushRoute } from './push.ts';
|
|
8
|
+
import { registerPullRoute } from './pull.ts';
|
|
8
9
|
|
|
9
10
|
export type { GitFile } from './types.ts';
|
|
10
11
|
|
|
@@ -15,4 +16,5 @@ export function registerGitRoutes(app: Hono) {
|
|
|
15
16
|
registerStagingRoutes(app);
|
|
16
17
|
registerCommitRoutes(app);
|
|
17
18
|
registerPushRoute(app);
|
|
19
|
+
registerPullRoute(app);
|
|
18
20
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { gitPullSchema } from './schemas.ts';
|
|
5
|
+
import { validateAndGetGitRoot } from './utils.ts';
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
export function registerPullRoute(app: Hono) {
|
|
10
|
+
app.post('/v1/git/pull', async (c) => {
|
|
11
|
+
try {
|
|
12
|
+
let body = {};
|
|
13
|
+
try {
|
|
14
|
+
body = await c.req.json();
|
|
15
|
+
} catch {
|
|
16
|
+
body = {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { project } = gitPullSchema.parse(body);
|
|
20
|
+
|
|
21
|
+
const requestedPath = project || process.cwd();
|
|
22
|
+
|
|
23
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
24
|
+
if ('error' in validation) {
|
|
25
|
+
return c.json(
|
|
26
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
27
|
+
400,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { gitRoot } = validation;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = await execFileAsync('git', ['pull'], {
|
|
35
|
+
cwd: gitRoot,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return c.json({
|
|
39
|
+
status: 'ok',
|
|
40
|
+
data: {
|
|
41
|
+
output: result.stdout.trim() || result.stderr.trim(),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
} catch (pullErr: unknown) {
|
|
45
|
+
const error = pullErr as {
|
|
46
|
+
message?: string;
|
|
47
|
+
stderr?: string;
|
|
48
|
+
};
|
|
49
|
+
const errorMessage = error.stderr || error.message || 'Failed to pull';
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
errorMessage.includes('CONFLICT') ||
|
|
53
|
+
errorMessage.includes('merge conflict')
|
|
54
|
+
) {
|
|
55
|
+
return c.json(
|
|
56
|
+
{
|
|
57
|
+
status: 'error',
|
|
58
|
+
error: 'Merge conflicts detected. Resolve conflicts manually',
|
|
59
|
+
details: errorMessage,
|
|
60
|
+
},
|
|
61
|
+
400,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
errorMessage.includes('Permission denied') ||
|
|
67
|
+
errorMessage.includes('authentication')
|
|
68
|
+
) {
|
|
69
|
+
return c.json(
|
|
70
|
+
{
|
|
71
|
+
status: 'error',
|
|
72
|
+
error: 'Authentication failed. Check your git credentials',
|
|
73
|
+
details: errorMessage,
|
|
74
|
+
},
|
|
75
|
+
401,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return c.json(
|
|
80
|
+
{
|
|
81
|
+
status: 'error',
|
|
82
|
+
error: 'Failed to pull changes',
|
|
83
|
+
details: errorMessage,
|
|
84
|
+
},
|
|
85
|
+
500,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return c.json(
|
|
90
|
+
{
|
|
91
|
+
status: 'error',
|
|
92
|
+
error: error instanceof Error ? error.message : 'Failed to pull',
|
|
93
|
+
},
|
|
94
|
+
500,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import {
|
|
3
|
+
getAuth,
|
|
4
|
+
refreshToken,
|
|
5
|
+
refreshOpenAIToken,
|
|
6
|
+
type ProviderId,
|
|
7
|
+
} from '@ottocode/sdk';
|
|
8
|
+
import { logger } from '@ottocode/sdk';
|
|
9
|
+
import { setAuth } from '@ottocode/sdk';
|
|
10
|
+
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
11
|
+
import type { OAuth } from '@ottocode/sdk';
|
|
12
|
+
|
|
13
|
+
async function ensureValidOAuth(
|
|
14
|
+
provider: ProviderId,
|
|
15
|
+
): Promise<{ access: string; oauth: OAuth } | null> {
|
|
16
|
+
const projectRoot = process.cwd();
|
|
17
|
+
const auth = await getAuth(provider, projectRoot);
|
|
18
|
+
if (!auth || auth.type !== 'oauth') return null;
|
|
19
|
+
|
|
20
|
+
if (auth.access && auth.expires > Date.now()) {
|
|
21
|
+
return { access: auth.access, oauth: auth };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const refreshFn = provider === 'openai' ? refreshOpenAIToken : refreshToken;
|
|
26
|
+
const newTokens = await refreshFn(auth.refresh);
|
|
27
|
+
const updated: OAuth = {
|
|
28
|
+
...auth,
|
|
29
|
+
access: newTokens.access,
|
|
30
|
+
refresh: newTokens.refresh,
|
|
31
|
+
expires: newTokens.expires,
|
|
32
|
+
};
|
|
33
|
+
await setAuth(provider, updated, projectRoot, 'global');
|
|
34
|
+
return { access: updated.access, oauth: updated };
|
|
35
|
+
} catch {
|
|
36
|
+
return { access: auth.access, oauth: auth };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchAnthropicUsage(access: string) {
|
|
41
|
+
const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${access}`,
|
|
44
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
45
|
+
Accept: 'application/json',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Anthropic usage API returned ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = (await response.json()) as {
|
|
54
|
+
five_hour?: { utilization: number; resets_at: string | null };
|
|
55
|
+
seven_day?: { utilization: number; resets_at: string | null };
|
|
56
|
+
seven_day_sonnet?: { utilization: number; resets_at: string | null };
|
|
57
|
+
extra_usage?: {
|
|
58
|
+
is_enabled: boolean;
|
|
59
|
+
monthly_limit: number;
|
|
60
|
+
used_credits: number;
|
|
61
|
+
utilization: number | null;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
provider: 'anthropic' as const,
|
|
67
|
+
primaryWindow: data.five_hour
|
|
68
|
+
? {
|
|
69
|
+
usedPercent: data.five_hour.utilization,
|
|
70
|
+
windowSeconds: 18000,
|
|
71
|
+
resetsAt: data.five_hour.resets_at,
|
|
72
|
+
}
|
|
73
|
+
: null,
|
|
74
|
+
secondaryWindow: data.seven_day
|
|
75
|
+
? {
|
|
76
|
+
usedPercent: data.seven_day.utilization,
|
|
77
|
+
windowSeconds: 604800,
|
|
78
|
+
resetsAt: data.seven_day.resets_at,
|
|
79
|
+
}
|
|
80
|
+
: null,
|
|
81
|
+
sonnetWindow: data.seven_day_sonnet
|
|
82
|
+
? {
|
|
83
|
+
usedPercent: data.seven_day_sonnet.utilization,
|
|
84
|
+
resetsAt: data.seven_day_sonnet.resets_at,
|
|
85
|
+
}
|
|
86
|
+
: null,
|
|
87
|
+
extraUsage: data.extra_usage ?? null,
|
|
88
|
+
limitReached: (data.five_hour?.utilization ?? 0) >= 100,
|
|
89
|
+
raw: data,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchOpenAIUsage(access: string, accountId?: string) {
|
|
94
|
+
const headers: Record<string, string> = {
|
|
95
|
+
Authorization: `Bearer ${access}`,
|
|
96
|
+
Accept: '*/*',
|
|
97
|
+
};
|
|
98
|
+
if (accountId) {
|
|
99
|
+
headers['ChatGPT-Account-Id'] = accountId;
|
|
100
|
+
}
|
|
101
|
+
const response = await fetch('https://chatgpt.com/backend-api/wham/usage', {
|
|
102
|
+
headers,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`OpenAI usage API returned ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const data = (await response.json()) as {
|
|
110
|
+
plan_type?: string;
|
|
111
|
+
rate_limit?: {
|
|
112
|
+
allowed: boolean;
|
|
113
|
+
limit_reached: boolean;
|
|
114
|
+
primary_window?: {
|
|
115
|
+
used_percent: number;
|
|
116
|
+
limit_window_seconds: number;
|
|
117
|
+
reset_after_seconds: number;
|
|
118
|
+
reset_at: number;
|
|
119
|
+
};
|
|
120
|
+
secondary_window?: {
|
|
121
|
+
used_percent: number;
|
|
122
|
+
limit_window_seconds: number;
|
|
123
|
+
reset_after_seconds: number;
|
|
124
|
+
reset_at: number;
|
|
125
|
+
} | null;
|
|
126
|
+
};
|
|
127
|
+
credits?: {
|
|
128
|
+
has_credits: boolean;
|
|
129
|
+
balance: number | null;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const rl = data.rate_limit;
|
|
134
|
+
return {
|
|
135
|
+
provider: 'openai' as const,
|
|
136
|
+
planType: data.plan_type ?? null,
|
|
137
|
+
primaryWindow: rl?.primary_window
|
|
138
|
+
? {
|
|
139
|
+
usedPercent: rl.primary_window.used_percent,
|
|
140
|
+
windowSeconds: rl.primary_window.limit_window_seconds,
|
|
141
|
+
resetsAt: new Date(rl.primary_window.reset_at * 1000).toISOString(),
|
|
142
|
+
resetAfterSeconds: rl.primary_window.reset_after_seconds,
|
|
143
|
+
}
|
|
144
|
+
: null,
|
|
145
|
+
secondaryWindow: rl?.secondary_window
|
|
146
|
+
? {
|
|
147
|
+
usedPercent: rl.secondary_window.used_percent,
|
|
148
|
+
windowSeconds: rl.secondary_window.limit_window_seconds,
|
|
149
|
+
resetsAt: new Date(rl.secondary_window.reset_at * 1000).toISOString(),
|
|
150
|
+
resetAfterSeconds: rl.secondary_window.reset_after_seconds,
|
|
151
|
+
}
|
|
152
|
+
: null,
|
|
153
|
+
credits: data.credits ?? null,
|
|
154
|
+
limitReached: rl?.limit_reached ?? false,
|
|
155
|
+
raw: data,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function registerProviderUsageRoutes(app: Hono) {
|
|
160
|
+
app.get('/v1/provider-usage/:provider', async (c) => {
|
|
161
|
+
try {
|
|
162
|
+
const provider = c.req.param('provider') as ProviderId;
|
|
163
|
+
|
|
164
|
+
if (provider !== 'anthropic' && provider !== 'openai') {
|
|
165
|
+
return c.json(
|
|
166
|
+
{ error: { message: 'Usage not supported for this provider' } },
|
|
167
|
+
400,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const tokenResult = await ensureValidOAuth(provider);
|
|
172
|
+
if (!tokenResult) {
|
|
173
|
+
return c.json(
|
|
174
|
+
{
|
|
175
|
+
error: {
|
|
176
|
+
message: `No OAuth credentials for ${provider}. Usage is only available for OAuth-authenticated providers.`,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
404,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const usage =
|
|
184
|
+
provider === 'anthropic'
|
|
185
|
+
? await fetchAnthropicUsage(tokenResult.access)
|
|
186
|
+
: await fetchOpenAIUsage(
|
|
187
|
+
tokenResult.access,
|
|
188
|
+
tokenResult.oauth.accountId,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return c.json(usage);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.error('Failed to fetch provider usage', error);
|
|
194
|
+
const errorResponse = serializeError(error);
|
|
195
|
+
const status = (errorResponse.error.status || 500) as 500;
|
|
196
|
+
return c.json(errorResponse, status);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -307,7 +307,11 @@ async function runAssistant(opts: RunOpts) {
|
|
|
307
307
|
|
|
308
308
|
const wasTruncated = streamFinishReason === 'length';
|
|
309
309
|
|
|
310
|
-
const shouldContinue =
|
|
310
|
+
const shouldContinue =
|
|
311
|
+
opts.provider === 'openai' &&
|
|
312
|
+
isOpenAIOAuth &&
|
|
313
|
+
!_finishObserved &&
|
|
314
|
+
(wasTruncated || fs);
|
|
311
315
|
|
|
312
316
|
if (shouldContinue) {
|
|
313
317
|
debugLog(
|