@ottocode/server 0.1.265 → 0.1.267

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/prompt/builder.ts +5 -1
  64. package/src/runtime/prompt/capabilities.ts +13 -8
  65. package/src/runtime/provider/custom.ts +73 -0
  66. package/src/runtime/provider/index.ts +2 -85
  67. package/src/runtime/provider/reasoning-builders.ts +280 -0
  68. package/src/runtime/provider/reasoning.ts +67 -264
  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,207 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { getDb } from '@ottocode/database';
4
+ import { sessions } from '@ottocode/database/schema';
5
+ import {
6
+ appendCoAuthorTrailer,
7
+ getAuth,
8
+ getFastModelForAuth,
9
+ getProviderDefinition,
10
+ loadConfig,
11
+ type ProviderId,
12
+ } from '@ottocode/sdk';
13
+ import { generateText, streamText } from 'ai';
14
+ import { eq } from 'drizzle-orm';
15
+ import type { Context } from 'hono';
16
+ import {
17
+ detectOAuth,
18
+ adaptSimpleCall,
19
+ } from '../../runtime/provider/oauth-adapter.ts';
20
+ import { resolveModel } from '../../runtime/provider/index.ts';
21
+ import { gitCommitSchema, gitGenerateCommitMessageSchema } from './schemas.ts';
22
+ import { parseGitStatus, validateAndGetGitRoot } from './utils.ts';
23
+
24
+ const execFileAsync = promisify(execFile);
25
+
26
+ export async function handleCommitChanges(c: Context) {
27
+ try {
28
+ const body = await c.req.json();
29
+ const { message, project } = gitCommitSchema.parse(body);
30
+ const requestedPath = project || process.cwd();
31
+
32
+ const validation = await validateAndGetGitRoot(requestedPath);
33
+ if ('error' in validation) {
34
+ return c.json(
35
+ { status: 'error', error: validation.error, code: validation.code },
36
+ 400,
37
+ );
38
+ }
39
+
40
+ const fullMessage = appendCoAuthorTrailer(message);
41
+ const { stdout } = await execFileAsync(
42
+ 'git',
43
+ ['commit', '-m', fullMessage],
44
+ {
45
+ cwd: validation.gitRoot,
46
+ },
47
+ );
48
+
49
+ return c.json({
50
+ status: 'ok',
51
+ data: {
52
+ message: stdout.trim(),
53
+ },
54
+ });
55
+ } catch (error) {
56
+ return c.json(
57
+ {
58
+ status: 'error',
59
+ error: error instanceof Error ? error.message : 'Failed to commit',
60
+ },
61
+ 500,
62
+ );
63
+ }
64
+ }
65
+
66
+ async function getSessionProviderModel(sessionId?: string) {
67
+ if (!sessionId) return {};
68
+ const db = await getDb();
69
+ const [session] = await db
70
+ .select({ provider: sessions.provider, model: sessions.model })
71
+ .from(sessions)
72
+ .where(eq(sessions.id, sessionId));
73
+ return {
74
+ provider: session?.provider as ProviderId | undefined,
75
+ model: session?.model ?? undefined,
76
+ };
77
+ }
78
+
79
+ function buildCommitPrompt(fileList: string, diff: string): string {
80
+ return `Generate a commit message for these git changes.
81
+
82
+ Staged files:
83
+ ${fileList}
84
+
85
+ Diff (first 4000 chars):
86
+ ${diff.slice(0, 4000)}
87
+
88
+ Guidelines:
89
+ - CAREFULLY READ the diff above - describe what ACTUALLY changed
90
+ - Use conventional commits format: type(scope): description
91
+ - First line under 72 characters
92
+ - Add a blank line, then 2-4 short bullet points
93
+ - Each bullet describes ONE specific change you see in the diff
94
+ - Be ACCURATE - don't invent changes that aren't in the diff
95
+ - Keep bullets short (under 80 chars each)
96
+ - Do not include markdown code blocks or backticks
97
+ - Return ONLY the commit message text, nothing else
98
+
99
+ Example (for a diff that adds boolean returns to functions):
100
+ refactor(auth): return success status from login functions
101
+
102
+ - Add boolean return type to auth functions
103
+ - Return false on user cancellation or failure
104
+ - Check return value before proceeding with auth flow
105
+
106
+ Commit message:`;
107
+ }
108
+
109
+ export async function handleGenerateCommitMessage(c: Context) {
110
+ try {
111
+ const body = await c.req.json();
112
+ const { project, sessionId } = gitGenerateCommitMessageSchema.parse(body);
113
+ const requestedPath = project || process.cwd();
114
+
115
+ const validation = await validateAndGetGitRoot(requestedPath);
116
+ if ('error' in validation) {
117
+ return c.json(
118
+ { status: 'error', error: validation.error, code: validation.code },
119
+ 400,
120
+ );
121
+ }
122
+
123
+ const { stdout: diff } = await execFileAsync('git', ['diff', '--cached'], {
124
+ cwd: validation.gitRoot,
125
+ });
126
+
127
+ if (!diff.trim()) {
128
+ return c.json(
129
+ {
130
+ status: 'error',
131
+ error: 'No staged changes to generate message from',
132
+ },
133
+ 400,
134
+ );
135
+ }
136
+
137
+ const { stdout: statusOutput } = await execFileAsync(
138
+ 'git',
139
+ ['status', '--porcelain=v2'],
140
+ { cwd: validation.gitRoot },
141
+ );
142
+ const { staged } = parseGitStatus(statusOutput, validation.gitRoot);
143
+ const fileList = staged.map((f) => `${f.status}: ${f.path}`).join('\n');
144
+ const config = await loadConfig();
145
+ const session = await getSessionProviderModel(sessionId);
146
+ const provider =
147
+ session.provider ??
148
+ ((config.defaults?.provider || 'anthropic') as ProviderId);
149
+ const currentModel =
150
+ session.model ?? config.defaults?.model ?? 'claude-3-5-sonnet-20241022';
151
+ const auth = await getAuth(provider, config.projectRoot);
152
+ const oauth = detectOAuth(provider, auth);
153
+ const providerDefinition = getProviderDefinition(config, provider);
154
+ const modelId =
155
+ providerDefinition?.source === 'custom' ||
156
+ providerDefinition?.compatibility === 'ollama'
157
+ ? currentModel
158
+ : (getFastModelForAuth(provider, auth?.type) ?? currentModel);
159
+ const model = await resolveModel(provider, modelId, config);
160
+
161
+ const adapted = adaptSimpleCall(oauth, {
162
+ instructions:
163
+ 'You are a helpful assistant that generates accurate git commit messages based on the actual diff content.',
164
+ userContent: buildCommitPrompt(fileList, diff),
165
+ maxOutputTokens: 500,
166
+ });
167
+
168
+ if (adapted.forceStream) {
169
+ const result = streamText({
170
+ model,
171
+ system: adapted.system,
172
+ messages: adapted.messages,
173
+ providerOptions: adapted.providerOptions,
174
+ });
175
+ let text = '';
176
+ for await (const chunk of result.textStream) {
177
+ text += chunk;
178
+ }
179
+ return c.json({ status: 'ok', data: { message: text.trim() } });
180
+ }
181
+
182
+ const { text } = await generateText({
183
+ model,
184
+ system: adapted.system,
185
+ messages: adapted.messages,
186
+ maxOutputTokens: adapted.maxOutputTokens,
187
+ });
188
+
189
+ return c.json({
190
+ status: 'ok',
191
+ data: {
192
+ message: text.trim(),
193
+ },
194
+ });
195
+ } catch (error) {
196
+ return c.json(
197
+ {
198
+ status: 'error',
199
+ error:
200
+ error instanceof Error
201
+ ? error.message
202
+ : 'Failed to generate commit message',
203
+ },
204
+ 500,
205
+ );
206
+ }
207
+ }
@@ -1,28 +1,9 @@
1
1
  import type { Hono } from 'hono';
2
- import { execFile } from 'node:child_process';
3
- import { promisify } from 'node:util';
4
- import { generateText, streamText } from 'ai';
5
- import { eq } from 'drizzle-orm';
6
- import type { ProviderId } from '@ottocode/sdk';
7
- import {
8
- loadConfig,
9
- getAuth,
10
- getFastModelForAuth,
11
- getProviderDefinition,
12
- } from '@ottocode/sdk';
13
- import { getDb } from '@ottocode/database';
14
- import { sessions } from '@ottocode/database/schema';
15
- import { gitCommitSchema, gitGenerateCommitMessageSchema } from './schemas.ts';
16
- import { validateAndGetGitRoot, parseGitStatus } from './utils.ts';
17
- import { resolveModel } from '../../runtime/provider/index.ts';
18
- import {
19
- detectOAuth,
20
- adaptSimpleCall,
21
- } from '../../runtime/provider/oauth-adapter.ts';
22
- import { appendCoAuthorTrailer } from '@ottocode/sdk';
23
2
  import { openApiRoute } from '../../openapi/route.ts';
24
-
25
- const execFileAsync = promisify(execFile);
3
+ import {
4
+ handleCommitChanges,
5
+ handleGenerateCommitMessage,
6
+ } from './commit-service.ts';
26
7
 
27
8
  export function registerCommitRoutes(app: Hono) {
28
9
  openApiRoute(
@@ -122,48 +103,7 @@ export function registerCommitRoutes(app: Hono) {
122
103
  },
123
104
  },
124
105
  },
125
- async (c) => {
126
- try {
127
- const body = await c.req.json();
128
- const { message, project } = gitCommitSchema.parse(body);
129
-
130
- const requestedPath = project || process.cwd();
131
-
132
- const validation = await validateAndGetGitRoot(requestedPath);
133
- if ('error' in validation) {
134
- return c.json(
135
- { status: 'error', error: validation.error, code: validation.code },
136
- 400,
137
- );
138
- }
139
-
140
- const { gitRoot } = validation;
141
-
142
- const fullMessage = appendCoAuthorTrailer(message);
143
- const { stdout } = await execFileAsync(
144
- 'git',
145
- ['commit', '-m', fullMessage],
146
- {
147
- cwd: gitRoot,
148
- },
149
- );
150
-
151
- return c.json({
152
- status: 'ok',
153
- data: {
154
- message: stdout.trim(),
155
- },
156
- });
157
- } catch (error) {
158
- return c.json(
159
- {
160
- status: 'error',
161
- error: error instanceof Error ? error.message : 'Failed to commit',
162
- },
163
- 500,
164
- );
165
- }
166
- },
106
+ handleCommitChanges,
167
107
  );
168
108
 
169
109
  openApiRoute(
@@ -270,160 +210,6 @@ export function registerCommitRoutes(app: Hono) {
270
210
  },
271
211
  },
272
212
  },
273
- async (c) => {
274
- try {
275
- const body = await c.req.json();
276
- const { project, sessionId } =
277
- gitGenerateCommitMessageSchema.parse(body);
278
-
279
- const requestedPath = project || process.cwd();
280
-
281
- const validation = await validateAndGetGitRoot(requestedPath);
282
- if ('error' in validation) {
283
- return c.json(
284
- { status: 'error', error: validation.error, code: validation.code },
285
- 400,
286
- );
287
- }
288
-
289
- const { gitRoot } = validation;
290
-
291
- const { stdout: diff } = await execFileAsync(
292
- 'git',
293
- ['diff', '--cached'],
294
- {
295
- cwd: gitRoot,
296
- },
297
- );
298
-
299
- if (!diff.trim()) {
300
- return c.json(
301
- {
302
- status: 'error',
303
- error: 'No staged changes to generate message from',
304
- },
305
- 400,
306
- );
307
- }
308
-
309
- const { stdout: statusOutput } = await execFileAsync(
310
- 'git',
311
- ['status', '--porcelain=v2'],
312
- { cwd: gitRoot },
313
- );
314
- const { staged } = parseGitStatus(statusOutput, gitRoot);
315
- const fileList = staged.map((f) => `${f.status}: ${f.path}`).join('\n');
316
-
317
- const config = await loadConfig();
318
-
319
- let provider = (config.defaults?.provider || 'anthropic') as ProviderId;
320
- let currentModel =
321
- config.defaults?.model ?? 'claude-3-5-sonnet-20241022';
322
-
323
- if (sessionId) {
324
- const db = await getDb();
325
- const [session] = await db
326
- .select({ provider: sessions.provider, model: sessions.model })
327
- .from(sessions)
328
- .where(eq(sessions.id, sessionId));
329
- if (session?.provider) {
330
- provider = session.provider as ProviderId;
331
- }
332
- if (session?.model) {
333
- currentModel = session.model;
334
- }
335
- }
336
-
337
- const auth = await getAuth(provider, config.projectRoot);
338
- const oauth = detectOAuth(provider, auth);
339
- const providerDefinition = getProviderDefinition(config, provider);
340
-
341
- const modelId =
342
- providerDefinition?.source === 'custom' ||
343
- providerDefinition?.compatibility === 'ollama'
344
- ? currentModel
345
- : (getFastModelForAuth(provider, auth?.type) ?? currentModel);
346
- const model = await resolveModel(provider, modelId, config);
347
-
348
- const userPrompt = `Generate a commit message for these git changes.
349
-
350
- Staged files:
351
- ${fileList}
352
-
353
- Diff (first 4000 chars):
354
- ${diff.slice(0, 4000)}
355
-
356
- Guidelines:
357
- - CAREFULLY READ the diff above - describe what ACTUALLY changed
358
- - Use conventional commits format: type(scope): description
359
- - First line under 72 characters
360
- - Add a blank line, then 2-4 short bullet points
361
- - Each bullet describes ONE specific change you see in the diff
362
- - Be ACCURATE - don't invent changes that aren't in the diff
363
- - Keep bullets short (under 80 chars each)
364
- - Do not include markdown code blocks or backticks
365
- - Return ONLY the commit message text, nothing else
366
-
367
- Example (for a diff that adds boolean returns to functions):
368
- refactor(auth): return success status from login functions
369
-
370
- - Add boolean return type to auth functions
371
- - Return false on user cancellation or failure
372
- - Check return value before proceeding with auth flow
373
-
374
- Commit message:`;
375
-
376
- const commitInstructions =
377
- 'You are a helpful assistant that generates accurate git commit messages based on the actual diff content.';
378
-
379
- const adapted = adaptSimpleCall(oauth, {
380
- instructions: commitInstructions,
381
- userContent: userPrompt,
382
- maxOutputTokens: 500,
383
- });
384
-
385
- if (adapted.forceStream) {
386
- const result = streamText({
387
- model,
388
- system: adapted.system,
389
- messages: adapted.messages,
390
- providerOptions: adapted.providerOptions,
391
- });
392
- let text = '';
393
- for await (const chunk of result.textStream) {
394
- text += chunk;
395
- }
396
- const message = text.trim();
397
- return c.json({ status: 'ok', data: { message } });
398
- }
399
-
400
- const { text } = await generateText({
401
- model,
402
- system: adapted.system,
403
- messages: adapted.messages,
404
- maxOutputTokens: adapted.maxOutputTokens,
405
- });
406
-
407
- const message = text.trim();
408
-
409
- return c.json({
410
- status: 'ok',
411
- data: {
412
- message,
413
- },
414
- });
415
- } catch (error) {
416
- return c.json(
417
- {
418
- status: 'error',
419
- error:
420
- error instanceof Error
421
- ? error.message
422
- : 'Failed to generate commit message',
423
- },
424
- 500,
425
- );
426
- }
427
- },
213
+ handleGenerateCommitMessage,
428
214
  );
429
215
  }
@@ -0,0 +1,116 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import type { Context } from 'hono';
4
+ import { gitRemoteAddSchema, gitRemoteRemoveSchema } from './schemas.ts';
5
+ import { validateAndGetGitRoot } from './utils.ts';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ type GitRemote = { name: string; url: string; type: string };
10
+
11
+ function parseRemoteOutput(output: string): GitRemote[] {
12
+ const remotes: GitRemote[] = [];
13
+ const seen = new Set<string>();
14
+ for (const line of output.trim().split('\n').filter(Boolean)) {
15
+ const match = line.match(/^(\S+)\s+(\S+)\s+\((\w+)\)$/);
16
+ if (!match) continue;
17
+ const [, name, url, type] = match;
18
+ const key = `${name}:${type}`;
19
+ if (seen.has(key)) continue;
20
+ seen.add(key);
21
+ remotes.push({ name, url, type });
22
+ }
23
+ return remotes;
24
+ }
25
+
26
+ export async function handleGetGitRemotes(c: Context) {
27
+ try {
28
+ const project = c.req.query('project');
29
+ const requestedPath = project || process.cwd();
30
+
31
+ const validation = await validateAndGetGitRoot(requestedPath);
32
+ if ('error' in validation) {
33
+ return c.json(
34
+ { status: 'error', error: validation.error, code: validation.code },
35
+ 400,
36
+ );
37
+ }
38
+
39
+ const { stdout } = await execFileAsync('git', ['remote', '-v'], {
40
+ cwd: validation.gitRoot,
41
+ });
42
+
43
+ return c.json({
44
+ status: 'ok',
45
+ data: { remotes: parseRemoteOutput(stdout) },
46
+ });
47
+ } catch (error) {
48
+ return c.json(
49
+ {
50
+ status: 'error',
51
+ error:
52
+ error instanceof Error ? error.message : 'Failed to list remotes',
53
+ },
54
+ 500,
55
+ );
56
+ }
57
+ }
58
+
59
+ export async function handleAddGitRemote(c: Context) {
60
+ try {
61
+ const body = await c.req.json().catch(() => ({}));
62
+ const { project, name, url } = gitRemoteAddSchema.parse(body);
63
+ const requestedPath = project || process.cwd();
64
+
65
+ const validation = await validateAndGetGitRoot(requestedPath);
66
+ if ('error' in validation) {
67
+ return c.json(
68
+ { status: 'error', error: validation.error, code: validation.code },
69
+ 400,
70
+ );
71
+ }
72
+
73
+ await execFileAsync('git', ['remote', 'add', name, url], {
74
+ cwd: validation.gitRoot,
75
+ });
76
+
77
+ return c.json({
78
+ status: 'ok',
79
+ data: { name, url },
80
+ });
81
+ } catch (error) {
82
+ const message =
83
+ error instanceof Error ? error.message : 'Failed to add remote';
84
+ const status = message.includes('already exists') ? 400 : 500;
85
+ return c.json({ status: 'error', error: message }, status);
86
+ }
87
+ }
88
+
89
+ export async function handleRemoveGitRemote(c: Context) {
90
+ try {
91
+ const body = await c.req.json().catch(() => ({}));
92
+ const { project, name } = gitRemoteRemoveSchema.parse(body);
93
+ const requestedPath = project || process.cwd();
94
+
95
+ const validation = await validateAndGetGitRoot(requestedPath);
96
+ if ('error' in validation) {
97
+ return c.json(
98
+ { status: 'error', error: validation.error, code: validation.code },
99
+ 400,
100
+ );
101
+ }
102
+
103
+ await execFileAsync('git', ['remote', 'remove', name], {
104
+ cwd: validation.gitRoot,
105
+ });
106
+
107
+ return c.json({
108
+ status: 'ok',
109
+ data: { removed: name },
110
+ });
111
+ } catch (error) {
112
+ const message =
113
+ error instanceof Error ? error.message : 'Failed to remove remote';
114
+ return c.json({ status: 'error', error: message }, 500);
115
+ }
116
+ }
@@ -1,11 +1,10 @@
1
1
  import type { Hono } from 'hono';
2
- import { execFile } from 'node:child_process';
3
- import { promisify } from 'node:util';
4
- import { gitRemoteAddSchema, gitRemoteRemoveSchema } from './schemas.ts';
5
- import { validateAndGetGitRoot } from './utils.ts';
6
2
  import { openApiRoute } from '../../openapi/route.ts';
7
-
8
- const execFileAsync = promisify(execFile);
3
+ import {
4
+ handleAddGitRemote,
5
+ handleGetGitRemotes,
6
+ handleRemoveGitRemote,
7
+ } from './remote-service.ts';
9
8
 
10
9
  export function registerRemoteRoutes(app: Hono) {
11
10
  openApiRoute(
@@ -118,54 +117,7 @@ export function registerRemoteRoutes(app: Hono) {
118
117
  },
119
118
  },
120
119
  },
121
- async (c) => {
122
- try {
123
- const project = c.req.query('project');
124
- const requestedPath = project || process.cwd();
125
-
126
- const validation = await validateAndGetGitRoot(requestedPath);
127
- if ('error' in validation) {
128
- return c.json(
129
- { status: 'error', error: validation.error, code: validation.code },
130
- 400,
131
- );
132
- }
133
-
134
- const { gitRoot } = validation;
135
-
136
- const { stdout } = await execFileAsync('git', ['remote', '-v'], {
137
- cwd: gitRoot,
138
- });
139
-
140
- const remotes: { name: string; url: string; type: string }[] = [];
141
- const seen = new Set<string>();
142
- for (const line of stdout.trim().split('\n').filter(Boolean)) {
143
- const match = line.match(/^(\S+)\s+(\S+)\s+\((\w+)\)$/);
144
- if (match) {
145
- const key = `${match[1]}:${match[3]}`;
146
- if (!seen.has(key)) {
147
- seen.add(key);
148
- remotes.push({
149
- name: match[1],
150
- url: match[2],
151
- type: match[3],
152
- });
153
- }
154
- }
155
- }
156
-
157
- return c.json({ status: 'ok', data: { remotes } });
158
- } catch (error) {
159
- return c.json(
160
- {
161
- status: 'error',
162
- error:
163
- error instanceof Error ? error.message : 'Failed to list remotes',
164
- },
165
- 500,
166
- );
167
- }
168
- },
120
+ handleGetGitRemotes,
169
121
  );
170
122
 
171
123
  openApiRoute(
@@ -276,37 +228,7 @@ export function registerRemoteRoutes(app: Hono) {
276
228
  },
277
229
  },
278
230
  },
279
- async (c) => {
280
- try {
281
- const body = await c.req.json().catch(() => ({}));
282
- const { project, name, url } = gitRemoteAddSchema.parse(body);
283
- const requestedPath = project || process.cwd();
284
-
285
- const validation = await validateAndGetGitRoot(requestedPath);
286
- if ('error' in validation) {
287
- return c.json(
288
- { status: 'error', error: validation.error, code: validation.code },
289
- 400,
290
- );
291
- }
292
-
293
- const { gitRoot } = validation;
294
-
295
- await execFileAsync('git', ['remote', 'add', name, url], {
296
- cwd: gitRoot,
297
- });
298
-
299
- return c.json({
300
- status: 'ok',
301
- data: { name, url },
302
- });
303
- } catch (error) {
304
- const message =
305
- error instanceof Error ? error.message : 'Failed to add remote';
306
- const status = message.includes('already exists') ? 400 : 500;
307
- return c.json({ status: 'error', error: message }, status);
308
- }
309
- },
231
+ handleAddGitRemote,
310
232
  );
311
233
 
312
234
  openApiRoute(
@@ -388,35 +310,6 @@ export function registerRemoteRoutes(app: Hono) {
388
310
  },
389
311
  },
390
312
  },
391
- async (c) => {
392
- try {
393
- const body = await c.req.json().catch(() => ({}));
394
- const { project, name } = gitRemoteRemoveSchema.parse(body);
395
- const requestedPath = project || process.cwd();
396
-
397
- const validation = await validateAndGetGitRoot(requestedPath);
398
- if ('error' in validation) {
399
- return c.json(
400
- { status: 'error', error: validation.error, code: validation.code },
401
- 400,
402
- );
403
- }
404
-
405
- const { gitRoot } = validation;
406
-
407
- await execFileAsync('git', ['remote', 'remove', name], {
408
- cwd: gitRoot,
409
- });
410
-
411
- return c.json({
412
- status: 'ok',
413
- data: { removed: name },
414
- });
415
- } catch (error) {
416
- const message =
417
- error instanceof Error ? error.message : 'Failed to remove remote';
418
- return c.json({ status: 'error', error: message }, 500);
419
- }
420
- },
313
+ handleRemoveGitRemote,
421
314
  );
422
315
  }