@agi-cli/server 0.1.81 → 0.1.82

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.
@@ -0,0 +1,75 @@
1
+ import type { Hono } from 'hono';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { gitStatusSchema } from './schemas.ts';
5
+ import {
6
+ validateAndGetGitRoot,
7
+ getAheadBehind,
8
+ getCurrentBranch,
9
+ } from './utils.ts';
10
+
11
+ const execFileAsync = promisify(execFile);
12
+
13
+ export function registerBranchRoute(app: Hono) {
14
+ app.get('/v1/git/branch', async (c) => {
15
+ try {
16
+ const query = gitStatusSchema.parse({
17
+ project: c.req.query('project'),
18
+ });
19
+
20
+ const requestedPath = query.project || process.cwd();
21
+
22
+ const validation = await validateAndGetGitRoot(requestedPath);
23
+ if ('error' in validation) {
24
+ return c.json(
25
+ { status: 'error', error: validation.error, code: validation.code },
26
+ 400,
27
+ );
28
+ }
29
+
30
+ const { gitRoot } = validation;
31
+
32
+ const branch = await getCurrentBranch(gitRoot);
33
+
34
+ const { ahead, behind } = await getAheadBehind(gitRoot);
35
+
36
+ try {
37
+ const { stdout: remotes } = await execFileAsync('git', ['remote'], {
38
+ cwd: gitRoot,
39
+ });
40
+ const remoteList = remotes.trim().split('\n').filter(Boolean);
41
+
42
+ return c.json({
43
+ status: 'ok',
44
+ data: {
45
+ branch,
46
+ ahead,
47
+ behind,
48
+ remotes: remoteList,
49
+ },
50
+ });
51
+ } catch {
52
+ return c.json({
53
+ status: 'ok',
54
+ data: {
55
+ branch,
56
+ ahead,
57
+ behind,
58
+ remotes: [],
59
+ },
60
+ });
61
+ }
62
+ } catch (error) {
63
+ return c.json(
64
+ {
65
+ status: 'error',
66
+ error:
67
+ error instanceof Error
68
+ ? error.message
69
+ : 'Failed to get branch info',
70
+ },
71
+ 500,
72
+ );
73
+ }
74
+ });
75
+ }
@@ -0,0 +1,159 @@
1
+ import type { Hono } from 'hono';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { generateText } from 'ai';
5
+ import type { ProviderId } from '@agi-cli/sdk';
6
+ import { loadConfig, getAuth } from '@agi-cli/sdk';
7
+ import { gitCommitSchema, gitGenerateCommitMessageSchema } from './schemas.ts';
8
+ import { validateAndGetGitRoot, parseGitStatus } from './utils.ts';
9
+ import { resolveModel } from '../../runtime/provider.ts';
10
+ import { getProviderSpoofPrompt } from '../../runtime/prompt.ts';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ export function registerCommitRoutes(app: Hono) {
15
+ app.post('/v1/git/commit', async (c) => {
16
+ try {
17
+ const body = await c.req.json();
18
+ const { message, project } = gitCommitSchema.parse(body);
19
+
20
+ const requestedPath = project || process.cwd();
21
+
22
+ const validation = await validateAndGetGitRoot(requestedPath);
23
+ if ('error' in validation) {
24
+ return c.json(
25
+ { status: 'error', error: validation.error, code: validation.code },
26
+ 400,
27
+ );
28
+ }
29
+
30
+ const { gitRoot } = validation;
31
+
32
+ const { stdout } = await execFileAsync('git', ['commit', '-m', message], {
33
+ cwd: gitRoot,
34
+ });
35
+
36
+ return c.json({
37
+ status: 'ok',
38
+ data: {
39
+ message: stdout.trim(),
40
+ },
41
+ });
42
+ } catch (error) {
43
+ return c.json(
44
+ {
45
+ status: 'error',
46
+ error: error instanceof Error ? error.message : 'Failed to commit',
47
+ },
48
+ 500,
49
+ );
50
+ }
51
+ });
52
+
53
+ app.post('/v1/git/generate-commit-message', async (c) => {
54
+ try {
55
+ const body = await c.req.json();
56
+ const { project } = gitGenerateCommitMessageSchema.parse(body);
57
+
58
+ const requestedPath = project || process.cwd();
59
+
60
+ const validation = await validateAndGetGitRoot(requestedPath);
61
+ if ('error' in validation) {
62
+ return c.json(
63
+ { status: 'error', error: validation.error, code: validation.code },
64
+ 400,
65
+ );
66
+ }
67
+
68
+ const { gitRoot } = validation;
69
+
70
+ const { stdout: diff } = await execFileAsync(
71
+ 'git',
72
+ ['diff', '--cached'],
73
+ {
74
+ cwd: gitRoot,
75
+ },
76
+ );
77
+
78
+ if (!diff.trim()) {
79
+ return c.json(
80
+ {
81
+ status: 'error',
82
+ error: 'No staged changes to generate message from',
83
+ },
84
+ 400,
85
+ );
86
+ }
87
+
88
+ const { stdout: statusOutput } = await execFileAsync(
89
+ 'git',
90
+ ['status', '--porcelain=v2'],
91
+ { cwd: gitRoot },
92
+ );
93
+ const { staged } = parseGitStatus(statusOutput, gitRoot);
94
+ const fileList = staged.map((f) => `${f.status}: ${f.path}`).join('\n');
95
+
96
+ const config = await loadConfig();
97
+
98
+ const provider = (config.defaults?.provider || 'anthropic') as ProviderId;
99
+ const modelId = config.defaults?.model || 'claude-3-5-sonnet-20241022';
100
+
101
+ const auth = await getAuth(provider, config.projectRoot);
102
+ const needsSpoof = auth?.type === 'oauth';
103
+ const spoofPrompt = needsSpoof
104
+ ? getProviderSpoofPrompt(provider)
105
+ : undefined;
106
+
107
+ const model = await resolveModel(provider, modelId, config);
108
+
109
+ const userPrompt = `Generate a concise, conventional commit message for these git changes.
110
+
111
+ Staged files:
112
+ ${fileList}
113
+
114
+ Diff (first 2000 chars):
115
+ ${diff.slice(0, 2000)}
116
+
117
+ Guidelines:
118
+ - Use conventional commits format (feat:, fix:, docs:, etc.)
119
+ - Keep the first line under 72 characters
120
+ - Be specific but concise
121
+ - Focus on what changed and why, not how
122
+ - Do not include any markdown formatting or code blocks
123
+ - Return ONLY the commit message text, nothing else
124
+
125
+ Commit message:`;
126
+
127
+ const systemPrompt = spoofPrompt
128
+ ? spoofPrompt
129
+ : 'You are a helpful assistant that generates git commit messages.';
130
+
131
+ const { text } = await generateText({
132
+ model,
133
+ system: systemPrompt,
134
+ prompt: userPrompt,
135
+ maxTokens: 200,
136
+ });
137
+
138
+ const message = text.trim();
139
+
140
+ return c.json({
141
+ status: 'ok',
142
+ data: {
143
+ message,
144
+ },
145
+ });
146
+ } catch (error) {
147
+ return c.json(
148
+ {
149
+ status: 'error',
150
+ error:
151
+ error instanceof Error
152
+ ? error.message
153
+ : 'Failed to generate commit message',
154
+ },
155
+ 500,
156
+ );
157
+ }
158
+ });
159
+ }
@@ -0,0 +1,137 @@
1
+ import type { Hono } from 'hono';
2
+ import { execFile } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { promisify } from 'node:util';
6
+ import { gitDiffSchema } from './schemas.ts';
7
+ import {
8
+ validateAndGetGitRoot,
9
+ checkIfNewFile,
10
+ inferLanguage,
11
+ summarizeDiff,
12
+ } from './utils.ts';
13
+
14
+ const execFileAsync = promisify(execFile);
15
+
16
+ export function registerDiffRoute(app: Hono) {
17
+ app.get('/v1/git/diff', async (c) => {
18
+ try {
19
+ const query = gitDiffSchema.parse({
20
+ project: c.req.query('project'),
21
+ file: c.req.query('file'),
22
+ staged: c.req.query('staged'),
23
+ });
24
+
25
+ const requestedPath = query.project || process.cwd();
26
+
27
+ const validation = await validateAndGetGitRoot(requestedPath);
28
+ if ('error' in validation) {
29
+ return c.json(
30
+ { status: 'error', error: validation.error, code: validation.code },
31
+ 400,
32
+ );
33
+ }
34
+
35
+ const { gitRoot } = validation;
36
+ const absPath = join(gitRoot, query.file);
37
+
38
+ const isNewFile = await checkIfNewFile(gitRoot, query.file);
39
+
40
+ if (isNewFile) {
41
+ try {
42
+ const content = await readFile(absPath, 'utf-8');
43
+ const lineCount = content.split('\n').length;
44
+ const language = inferLanguage(query.file);
45
+
46
+ return c.json({
47
+ status: 'ok',
48
+ data: {
49
+ file: query.file,
50
+ absPath,
51
+ diff: '',
52
+ content,
53
+ isNewFile: true,
54
+ isBinary: false,
55
+ insertions: lineCount,
56
+ deletions: 0,
57
+ language,
58
+ staged: !!query.staged,
59
+ },
60
+ });
61
+ } catch (error) {
62
+ return c.json(
63
+ {
64
+ status: 'error',
65
+ error:
66
+ error instanceof Error ? error.message : 'Failed to read file',
67
+ },
68
+ 500,
69
+ );
70
+ }
71
+ }
72
+
73
+ const diffArgs = query.staged
74
+ ? ['diff', '--cached', '--', query.file]
75
+ : ['diff', '--', query.file];
76
+ const numstatArgs = query.staged
77
+ ? ['diff', '--cached', '--numstat', '--', query.file]
78
+ : ['diff', '--numstat', '--', query.file];
79
+
80
+ const [{ stdout: diffOutput }, { stdout: numstatOutput }] =
81
+ await Promise.all([
82
+ execFileAsync('git', diffArgs, { cwd: gitRoot }),
83
+ execFileAsync('git', numstatArgs, { cwd: gitRoot }),
84
+ ]);
85
+
86
+ let insertions = 0;
87
+ let deletions = 0;
88
+ let binary = false;
89
+
90
+ const numstatLine = numstatOutput.trim().split('\n').find(Boolean);
91
+ if (numstatLine) {
92
+ const [rawInsertions, rawDeletions] = numstatLine.split('\t');
93
+ if (rawInsertions === '-' || rawDeletions === '-') {
94
+ binary = true;
95
+ } else {
96
+ insertions = Number.parseInt(rawInsertions, 10) || 0;
97
+ deletions = Number.parseInt(rawDeletions, 10) || 0;
98
+ }
99
+ }
100
+
101
+ const diffText = diffOutput ?? '';
102
+ if (!binary) {
103
+ const summary = summarizeDiff(diffText);
104
+ binary = summary.binary;
105
+ if (insertions === 0 && deletions === 0) {
106
+ insertions = summary.insertions;
107
+ deletions = summary.deletions;
108
+ }
109
+ }
110
+
111
+ const language = inferLanguage(query.file);
112
+
113
+ return c.json({
114
+ status: 'ok',
115
+ data: {
116
+ file: query.file,
117
+ absPath,
118
+ diff: diffText,
119
+ isNewFile: false,
120
+ isBinary: binary,
121
+ insertions,
122
+ deletions,
123
+ language,
124
+ staged: !!query.staged,
125
+ },
126
+ });
127
+ } catch (error) {
128
+ return c.json(
129
+ {
130
+ status: 'error',
131
+ error: error instanceof Error ? error.message : 'Failed to get diff',
132
+ },
133
+ 500,
134
+ );
135
+ }
136
+ });
137
+ }
@@ -0,0 +1,18 @@
1
+ import type { Hono } from 'hono';
2
+ import { registerStatusRoute } from './status.ts';
3
+ import { registerBranchRoute } from './branch.ts';
4
+ import { registerDiffRoute } from './diff.ts';
5
+ import { registerStagingRoutes } from './staging.ts';
6
+ import { registerCommitRoutes } from './commit.ts';
7
+ import { registerPushRoute } from './push.ts';
8
+
9
+ export type { GitFile } from './types.ts';
10
+
11
+ export function registerGitRoutes(app: Hono) {
12
+ registerStatusRoute(app);
13
+ registerBranchRoute(app);
14
+ registerDiffRoute(app);
15
+ registerStagingRoutes(app);
16
+ registerCommitRoutes(app);
17
+ registerPushRoute(app);
18
+ }
@@ -0,0 +1,160 @@
1
+ import type { Hono } from 'hono';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { gitPushSchema } from './schemas.ts';
5
+ import { validateAndGetGitRoot, getCurrentBranch } from './utils.ts';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ export function registerPushRoute(app: Hono) {
10
+ app.post('/v1/git/push', async (c) => {
11
+ try {
12
+ let body = {};
13
+ try {
14
+ body = await c.req.json();
15
+ } catch (jsonError) {
16
+ console.warn(
17
+ 'Failed to parse JSON body for git push, using empty object:',
18
+ jsonError,
19
+ );
20
+ }
21
+
22
+ const { project } = gitPushSchema.parse(body);
23
+
24
+ const requestedPath = project || process.cwd();
25
+
26
+ const validation = await validateAndGetGitRoot(requestedPath);
27
+ if ('error' in validation) {
28
+ return c.json(
29
+ { status: 'error', error: validation.error, code: validation.code },
30
+ 400,
31
+ );
32
+ }
33
+
34
+ const { gitRoot } = validation;
35
+
36
+ try {
37
+ const { stdout: remotes } = await execFileAsync('git', ['remote'], {
38
+ cwd: gitRoot,
39
+ });
40
+ if (!remotes.trim()) {
41
+ return c.json(
42
+ { status: 'error', error: 'No remote repository configured' },
43
+ 400,
44
+ );
45
+ }
46
+ } catch {
47
+ return c.json(
48
+ { status: 'error', error: 'No remote repository configured' },
49
+ 400,
50
+ );
51
+ }
52
+
53
+ const branch = await getCurrentBranch(gitRoot);
54
+ let hasUpstream = false;
55
+ try {
56
+ await execFileAsync(
57
+ 'git',
58
+ ['rev-parse', '--abbrev-ref', '@{upstream}'],
59
+ {
60
+ cwd: gitRoot,
61
+ },
62
+ );
63
+ hasUpstream = true;
64
+ } catch {}
65
+
66
+ try {
67
+ let pushOutput: string;
68
+ let pushError: string;
69
+
70
+ if (hasUpstream) {
71
+ const result = await execFileAsync('git', ['push'], { cwd: gitRoot });
72
+ pushOutput = result.stdout;
73
+ pushError = result.stderr;
74
+ } else {
75
+ const result = await execFileAsync(
76
+ 'git',
77
+ ['push', '--set-upstream', 'origin', branch],
78
+ { cwd: gitRoot },
79
+ );
80
+ pushOutput = result.stdout;
81
+ pushError = result.stderr;
82
+ }
83
+
84
+ return c.json({
85
+ status: 'ok',
86
+ data: {
87
+ output: pushOutput.trim() || pushError.trim(),
88
+ },
89
+ });
90
+ } catch (pushErr: unknown) {
91
+ const error = pushErr as {
92
+ message?: string;
93
+ stderr?: string;
94
+ code?: number;
95
+ };
96
+ const errorMessage = error.stderr || error.message || 'Failed to push';
97
+
98
+ if (
99
+ errorMessage.includes('failed to push') ||
100
+ errorMessage.includes('rejected')
101
+ ) {
102
+ return c.json(
103
+ {
104
+ status: 'error',
105
+ error: 'Push rejected. Try pulling changes first with: git pull',
106
+ details: errorMessage,
107
+ },
108
+ 400,
109
+ );
110
+ }
111
+
112
+ if (
113
+ errorMessage.includes('Permission denied') ||
114
+ errorMessage.includes('authentication') ||
115
+ errorMessage.includes('could not read')
116
+ ) {
117
+ return c.json(
118
+ {
119
+ status: 'error',
120
+ error: 'Authentication failed. Check your git credentials',
121
+ details: errorMessage,
122
+ },
123
+ 401,
124
+ );
125
+ }
126
+
127
+ if (
128
+ errorMessage.includes('Could not resolve host') ||
129
+ errorMessage.includes('network')
130
+ ) {
131
+ return c.json(
132
+ {
133
+ status: 'error',
134
+ error: 'Network error. Check your internet connection',
135
+ details: errorMessage,
136
+ },
137
+ 503,
138
+ );
139
+ }
140
+
141
+ return c.json(
142
+ {
143
+ status: 'error',
144
+ error: 'Failed to push commits',
145
+ details: errorMessage,
146
+ },
147
+ 500,
148
+ );
149
+ }
150
+ } catch (error) {
151
+ return c.json(
152
+ {
153
+ status: 'error',
154
+ error: error instanceof Error ? error.message : 'Failed to push',
155
+ },
156
+ 500,
157
+ );
158
+ }
159
+ });
160
+ }
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+
3
+ export const gitStatusSchema = z.object({
4
+ project: z.string().optional(),
5
+ });
6
+
7
+ export const gitDiffSchema = z.object({
8
+ project: z.string().optional(),
9
+ file: z.string(),
10
+ staged: z
11
+ .string()
12
+ .optional()
13
+ .transform((val) => val === 'true'),
14
+ });
15
+
16
+ export const gitStageSchema = z.object({
17
+ project: z.string().optional(),
18
+ files: z.array(z.string()),
19
+ });
20
+
21
+ export const gitUnstageSchema = z.object({
22
+ project: z.string().optional(),
23
+ files: z.array(z.string()),
24
+ });
25
+
26
+ export const gitRestoreSchema = z.object({
27
+ project: z.string().optional(),
28
+ files: z.array(z.string()),
29
+ });
30
+
31
+ export const gitDeleteSchema = z.object({
32
+ project: z.string().optional(),
33
+ files: z.array(z.string()),
34
+ });
35
+
36
+ export const gitCommitSchema = z.object({
37
+ project: z.string().optional(),
38
+ message: z.string().min(1),
39
+ });
40
+
41
+ export const gitGenerateCommitMessageSchema = z.object({
42
+ project: z.string().optional(),
43
+ });
44
+
45
+ export const gitPushSchema = z.object({
46
+ project: z.string().optional(),
47
+ });