@civilization/dsh-git-workbench 0.0.0-stage → 0.4.1
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/LICENSE +201 -0
- package/README.md +119 -2
- package/cordis.patch.yml +3 -0
- package/icon.svg +26 -0
- package/lib/client.js +2943 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1371 -0
- package/package.json +91 -4
- package/src/client/GitBody.module.css +1283 -0
- package/src/client/GitBody.tsx +1496 -0
- package/src/client/GitBranchPill.module.css +292 -0
- package/src/client/GitBranchPill.tsx +368 -0
- package/src/client/GitTitle.tsx +27 -0
- package/src/client/definition.tsx +63 -0
- package/src/client/git-helpers.ts +351 -0
- package/src/client/index.ts +221 -0
- package/src/client/locales.ts +183 -0
- package/src/css.d.ts +4 -0
- package/src/git-executor.ts +1025 -0
- package/src/index.ts +304 -0
- package/src/tool.ts +299 -0
- package/src/types.ts +187 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host entry for the Git Workbench & source control plugin.
|
|
3
|
+
* Provides RPC endpoints for the Client right-sidebar viewer, status bar, and model tools for Agents.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
7
|
+
import {
|
|
8
|
+
getGitAllBranches,
|
|
9
|
+
getGitCommitDetails,
|
|
10
|
+
getGitDiff,
|
|
11
|
+
getGitLog,
|
|
12
|
+
getGitStatus,
|
|
13
|
+
getGitTags,
|
|
14
|
+
gitCheckout,
|
|
15
|
+
gitCommit,
|
|
16
|
+
gitCreateTag,
|
|
17
|
+
gitDeleteTag,
|
|
18
|
+
gitDiscard,
|
|
19
|
+
gitFetch,
|
|
20
|
+
gitPull,
|
|
21
|
+
gitPush,
|
|
22
|
+
gitStage,
|
|
23
|
+
gitUnstage,
|
|
24
|
+
} from './git-executor.ts'
|
|
25
|
+
import { registerGitTool } from './tool.ts'
|
|
26
|
+
|
|
27
|
+
export type * from './types.ts'
|
|
28
|
+
|
|
29
|
+
/** Cordis plugin name */
|
|
30
|
+
export const name = '@civilization/dsh-git-workbench'
|
|
31
|
+
|
|
32
|
+
/** Required host services */
|
|
33
|
+
export const inject = ['webServer', 'sessions', 'sandboxPolicy']
|
|
34
|
+
|
|
35
|
+
interface HostServices {
|
|
36
|
+
webServer?: {
|
|
37
|
+
register: (route: unknown) => () => void
|
|
38
|
+
}
|
|
39
|
+
sessions?: {
|
|
40
|
+
get: (id: string) => { header?: { cwd?: string } } | undefined
|
|
41
|
+
}
|
|
42
|
+
sandboxPolicy?: {
|
|
43
|
+
workspaceRoot?: string
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Execute Git operations based on requested endpoint and payload.
|
|
49
|
+
*/
|
|
50
|
+
export async function executeGitEndpoint(
|
|
51
|
+
endpoint: string,
|
|
52
|
+
payload: unknown,
|
|
53
|
+
resolveTargetDir: (sessionId?: string) => string,
|
|
54
|
+
): Promise<{ ok: true; value: unknown } | { ok: false; error: { code: string; message: string; details: Record<string, unknown> } }> {
|
|
55
|
+
try {
|
|
56
|
+
const body = (typeof payload === 'object' && payload !== null ? payload : {}) as Record<string, unknown>
|
|
57
|
+
const sessionId = typeof body.sessionId === 'string' ? body.sessionId : undefined
|
|
58
|
+
const cwd = typeof body.path === 'string' ? body.path : resolveTargetDir(sessionId)
|
|
59
|
+
|
|
60
|
+
if (endpoint === 'status') {
|
|
61
|
+
const status = await getGitStatus(cwd)
|
|
62
|
+
return { ok: true, value: status }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (endpoint === 'log') {
|
|
66
|
+
const maxCount = typeof body.maxCount === 'number' ? body.maxCount : 50
|
|
67
|
+
const commits = await getGitLog(cwd, maxCount)
|
|
68
|
+
return { ok: true, value: commits }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (endpoint === 'commitDetails') {
|
|
72
|
+
const commit = typeof body.commit === 'string' ? body.commit : ''
|
|
73
|
+
const details = await getGitCommitDetails(cwd, commit)
|
|
74
|
+
return { ok: true, value: details }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (endpoint === 'diff') {
|
|
78
|
+
const path = typeof body.filePath === 'string' ? body.filePath : undefined
|
|
79
|
+
const staged = typeof body.staged === 'boolean' ? body.staged : false
|
|
80
|
+
const commit = typeof body.commit === 'string' ? body.commit : undefined
|
|
81
|
+
const diff = await getGitDiff(cwd, {
|
|
82
|
+
...path !== undefined ? { path } : {},
|
|
83
|
+
staged,
|
|
84
|
+
...commit !== undefined ? { commit } : {},
|
|
85
|
+
})
|
|
86
|
+
return { ok: true, value: diff }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (endpoint === 'branches') {
|
|
90
|
+
const branches = await getGitAllBranches(cwd)
|
|
91
|
+
return { ok: true, value: branches }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (endpoint === 'checkout') {
|
|
95
|
+
const branch = typeof body.branch === 'string' ? body.branch : ''
|
|
96
|
+
const createNew = typeof body.create === 'boolean' ? body.create : false
|
|
97
|
+
const result = await gitCheckout(cwd, branch, createNew)
|
|
98
|
+
return { ok: true, value: result }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (endpoint === 'stage') {
|
|
102
|
+
const paths = Array.isArray(body.paths)
|
|
103
|
+
? (body.paths as string[])
|
|
104
|
+
: body.paths === 'all'
|
|
105
|
+
? 'all'
|
|
106
|
+
: typeof body.path === 'string'
|
|
107
|
+
? [body.path]
|
|
108
|
+
: 'all'
|
|
109
|
+
const result = await gitStage(cwd, paths)
|
|
110
|
+
return { ok: true, value: result }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (endpoint === 'unstage') {
|
|
114
|
+
const paths = Array.isArray(body.paths)
|
|
115
|
+
? (body.paths as string[])
|
|
116
|
+
: body.paths === 'all'
|
|
117
|
+
? 'all'
|
|
118
|
+
: typeof body.path === 'string'
|
|
119
|
+
? [body.path]
|
|
120
|
+
: 'all'
|
|
121
|
+
const result = await gitUnstage(cwd, paths)
|
|
122
|
+
return { ok: true, value: result }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (endpoint === 'discard') {
|
|
126
|
+
const paths = Array.isArray(body.paths)
|
|
127
|
+
? (body.paths as string[])
|
|
128
|
+
: typeof body.path === 'string'
|
|
129
|
+
? [body.path]
|
|
130
|
+
: []
|
|
131
|
+
const result = await gitDiscard(cwd, paths)
|
|
132
|
+
return { ok: true, value: result }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (endpoint === 'commit') {
|
|
136
|
+
const message = typeof body.message === 'string' ? body.message : ''
|
|
137
|
+
const result = await gitCommit(cwd, message)
|
|
138
|
+
return { ok: true, value: result }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (endpoint === 'fetch') {
|
|
142
|
+
const result = await gitFetch(cwd)
|
|
143
|
+
return { ok: true, value: result }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (endpoint === 'pull') {
|
|
147
|
+
const result = await gitPull(cwd)
|
|
148
|
+
return { ok: true, value: result }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (endpoint === 'push') {
|
|
152
|
+
const result = await gitPush(cwd)
|
|
153
|
+
return { ok: true, value: result }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (endpoint === 'tags') {
|
|
157
|
+
const tags = await getGitTags(cwd)
|
|
158
|
+
return { ok: true, value: tags }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (endpoint === 'createTag') {
|
|
162
|
+
const name = typeof body.name === 'string' ? body.name : ''
|
|
163
|
+
const commit = typeof body.commit === 'string' ? body.commit : undefined
|
|
164
|
+
const message = typeof body.message === 'string' ? body.message : undefined
|
|
165
|
+
const result = await gitCreateTag(cwd, name, commit, message)
|
|
166
|
+
return { ok: true, value: result }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (endpoint === 'deleteTag') {
|
|
170
|
+
const name = typeof body.name === 'string' ? body.name : ''
|
|
171
|
+
const result = await gitDeleteTag(cwd, name)
|
|
172
|
+
return { ok: true, value: result }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: {
|
|
178
|
+
code: 'git/not_found',
|
|
179
|
+
message: `Unknown git RPC endpoint: ${endpoint}`,
|
|
180
|
+
details: {},
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
error: {
|
|
187
|
+
code: 'git/error',
|
|
188
|
+
message: error instanceof Error ? error.message : String(error),
|
|
189
|
+
details: {},
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Apply the Git host plugin.
|
|
197
|
+
* Sets up direct WebServer HTTP route handlers under `/git/*` and registers the model tool.
|
|
198
|
+
* @param ctx - Context carrying the webServer, sessions, and sandboxPolicy services.
|
|
199
|
+
*/
|
|
200
|
+
export function apply(ctx: Context): void {
|
|
201
|
+
const host = ctx as any as HostServices
|
|
202
|
+
|
|
203
|
+
const resolveTargetDir = (sessionId?: string): string => {
|
|
204
|
+
try {
|
|
205
|
+
if (sessionId) {
|
|
206
|
+
const liveCwd = host.sessions?.get?.(sessionId)?.header?.cwd
|
|
207
|
+
if (liveCwd) return liveCwd
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const workspaceRoot = host.sandboxPolicy?.workspaceRoot
|
|
213
|
+
if (workspaceRoot) return workspaceRoot
|
|
214
|
+
} catch {}
|
|
215
|
+
|
|
216
|
+
return process.cwd()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 1. Direct WebServer route registration (Standard, robust DSH plugin route pattern)
|
|
220
|
+
const webServer = (ctx as any).webServer
|
|
221
|
+
if (webServer?.register) {
|
|
222
|
+
ctx.effect(() => webServer.register({
|
|
223
|
+
kind: 'prefix',
|
|
224
|
+
path: '/git',
|
|
225
|
+
handler: async (req: any, res: any) => {
|
|
226
|
+
try {
|
|
227
|
+
if (req.method === 'OPTIONS') {
|
|
228
|
+
res.writeHead(204, {
|
|
229
|
+
'Access-Control-Allow-Origin': '*',
|
|
230
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
231
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
232
|
+
})
|
|
233
|
+
return res.end()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (req.method !== 'POST') {
|
|
237
|
+
res.writeHead(405, { 'Content-Type': 'application/json' })
|
|
238
|
+
return res.end(JSON.stringify({ ok: false, error: { message: 'Method Not Allowed' } }))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const pathname = new URL(req.url ?? '/', 'http://localhost').pathname
|
|
242
|
+
const match = pathname.match(/^\/git\/([a-zA-Z0-9_-]+)/)
|
|
243
|
+
const endpoint = match ? match[1] : undefined
|
|
244
|
+
|
|
245
|
+
if (!endpoint) {
|
|
246
|
+
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
247
|
+
return res.end(JSON.stringify({ ok: false, error: { message: 'Not Found' } }))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let rawBody = ''
|
|
251
|
+
req.on('data', (chunk: Buffer) => { rawBody += chunk.toString('utf8') })
|
|
252
|
+
req.on('end', async () => {
|
|
253
|
+
try {
|
|
254
|
+
let body: any = {}
|
|
255
|
+
try {
|
|
256
|
+
if (rawBody.trim()) body = JSON.parse(rawBody)
|
|
257
|
+
} catch {
|
|
258
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
259
|
+
return res.end(JSON.stringify({ ok: false, error: { message: 'Invalid JSON body' } }))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const rpcId = body?.type === 'client-request' ? body.rpcId : undefined
|
|
263
|
+
const payload = body?.type === 'client-request' ? (body.payload ?? {}) : body
|
|
264
|
+
|
|
265
|
+
const result = await executeGitEndpoint(endpoint, payload, resolveTargetDir)
|
|
266
|
+
|
|
267
|
+
res.writeHead(200, {
|
|
268
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
269
|
+
'Access-Control-Allow-Origin': '*',
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
if (rpcId !== undefined) {
|
|
273
|
+
res.end(JSON.stringify({
|
|
274
|
+
type: 'server-response',
|
|
275
|
+
rpcId,
|
|
276
|
+
result,
|
|
277
|
+
}))
|
|
278
|
+
} else {
|
|
279
|
+
res.end(JSON.stringify(result))
|
|
280
|
+
}
|
|
281
|
+
} catch (err: any) {
|
|
282
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
283
|
+
res.end(JSON.stringify({
|
|
284
|
+
ok: false,
|
|
285
|
+
error: { code: 'git/internal_error', message: err?.message ?? String(err) },
|
|
286
|
+
}))
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
} catch (err: any) {
|
|
290
|
+
try {
|
|
291
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
292
|
+
res.end(JSON.stringify({
|
|
293
|
+
ok: false,
|
|
294
|
+
error: { code: 'git/internal_error', message: err?.message ?? String(err) },
|
|
295
|
+
}))
|
|
296
|
+
} catch {}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
}), 'git-workbench: /git routes')
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 2. Register the agent tool
|
|
303
|
+
registerGitTool(ctx, resolveTargetDir)
|
|
304
|
+
}
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent model tool for inspecting Git repository status, commits, and diffs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
6
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
7
|
+
|
|
8
|
+
export type JsonValue =
|
|
9
|
+
| string
|
|
10
|
+
| number
|
|
11
|
+
| boolean
|
|
12
|
+
| null
|
|
13
|
+
| { [key: string]: JsonValue }
|
|
14
|
+
| JsonValue[]
|
|
15
|
+
import {
|
|
16
|
+
getGitAllBranches,
|
|
17
|
+
getGitCommitDetails,
|
|
18
|
+
getGitDiff,
|
|
19
|
+
getGitLog,
|
|
20
|
+
getGitStatus,
|
|
21
|
+
getGitTags,
|
|
22
|
+
gitCheckout,
|
|
23
|
+
gitCreateTag,
|
|
24
|
+
gitDeleteTag,
|
|
25
|
+
} from './git-executor.ts'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register git model tools (git_view and git_checkout) in the tools service.
|
|
29
|
+
* @param ctx - Context carrying the tools service.
|
|
30
|
+
* @param resolveDir - Resolver for the current agent or session working directory.
|
|
31
|
+
*/
|
|
32
|
+
export function registerGitTool(ctx: Context, resolveDir: (sessionId?: string) => string): void {
|
|
33
|
+
ctx.inject(['tools'], (scope) => {
|
|
34
|
+
// 1. Tool for inspecting git status, DAG history, diff, and branches
|
|
35
|
+
scope.effect(() => scope.tools.register(defineTool({
|
|
36
|
+
name: 'git_view',
|
|
37
|
+
description: 'Inspect the current project Git repository status, changed files, DAG commit tree, branch list, or code diffs.',
|
|
38
|
+
parameters: {
|
|
39
|
+
action: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
enum: ['status', 'log', 'diff', 'branches', 'commit', 'tags'],
|
|
42
|
+
description: 'The Git query action: "status" (changes and branch info), "log" (commits with DAG topology rails), "diff" (code diffs), "branches" (branch list), "commit" (commit details), or "tags" (all repository tags).',
|
|
43
|
+
},
|
|
44
|
+
path: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Optional file path filter for diff or status inspection.',
|
|
47
|
+
},
|
|
48
|
+
staged: {
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
description: 'Whether to check staged changes when viewing diff (defaults to false for working tree changes).',
|
|
51
|
+
},
|
|
52
|
+
maxCount: {
|
|
53
|
+
type: 'number',
|
|
54
|
+
description: 'Maximum number of commits to retrieve when action is "log" (defaults to 30).',
|
|
55
|
+
},
|
|
56
|
+
commit: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Specific commit hash to inspect diff or details for.',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
output: {
|
|
62
|
+
schema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
additionalProperties: true,
|
|
65
|
+
},
|
|
66
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
67
|
+
},
|
|
68
|
+
async execute(args, exec): Promise<Record<string, JsonValue>> {
|
|
69
|
+
const cwd = resolveDir(exec.agent?.session.id)
|
|
70
|
+
const action = args.action ?? 'status'
|
|
71
|
+
|
|
72
|
+
switch (action) {
|
|
73
|
+
case 'log': {
|
|
74
|
+
const rawCommits = await getGitLog(cwd, args.maxCount ?? 30)
|
|
75
|
+
const commits = rawCommits.map(c => ({
|
|
76
|
+
hash: c.hash,
|
|
77
|
+
shortHash: c.shortHash,
|
|
78
|
+
author: c.author,
|
|
79
|
+
authorEmail: c.authorEmail,
|
|
80
|
+
relativeDate: c.relativeDate,
|
|
81
|
+
date: c.date,
|
|
82
|
+
subject: c.subject,
|
|
83
|
+
parents: c.parents ? [...c.parents] : [],
|
|
84
|
+
refs: c.refs ? [...c.refs] : [],
|
|
85
|
+
...c.lane !== undefined ? { lane: c.lane } : {},
|
|
86
|
+
...c.laneColor !== undefined ? { laneColor: c.laneColor } : {},
|
|
87
|
+
}))
|
|
88
|
+
return { action: 'log', count: commits.length, commits }
|
|
89
|
+
}
|
|
90
|
+
case 'commit': {
|
|
91
|
+
if (!args.commit) {
|
|
92
|
+
return { action: 'commit', error: 'Missing commit parameter' }
|
|
93
|
+
}
|
|
94
|
+
const details = await getGitCommitDetails(cwd, args.commit)
|
|
95
|
+
if (!details) return { action: 'commit', found: false }
|
|
96
|
+
return {
|
|
97
|
+
action: 'commit',
|
|
98
|
+
found: true,
|
|
99
|
+
commit: {
|
|
100
|
+
hash: details.commit.hash,
|
|
101
|
+
shortHash: details.commit.shortHash,
|
|
102
|
+
author: details.commit.author,
|
|
103
|
+
date: details.commit.date,
|
|
104
|
+
subject: details.commit.subject,
|
|
105
|
+
},
|
|
106
|
+
files: details.files.map(f => ({
|
|
107
|
+
path: f.path,
|
|
108
|
+
status: f.status,
|
|
109
|
+
additions: f.additions,
|
|
110
|
+
deletions: f.deletions,
|
|
111
|
+
})),
|
|
112
|
+
totalAdditions: details.totalAdditions,
|
|
113
|
+
totalDeletions: details.totalDeletions,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
case 'diff': {
|
|
117
|
+
const diff = await getGitDiff(cwd, {
|
|
118
|
+
...args.path !== undefined ? { path: args.path } : {},
|
|
119
|
+
...args.staged !== undefined ? { staged: args.staged } : {},
|
|
120
|
+
...args.commit !== undefined ? { commit: args.commit } : {},
|
|
121
|
+
})
|
|
122
|
+
const files = diff.files.map(f => ({ path: f.path, additions: f.additions, deletions: f.deletions }))
|
|
123
|
+
return { action: 'diff', files, rawDiff: diff.rawDiff }
|
|
124
|
+
}
|
|
125
|
+
case 'branches': {
|
|
126
|
+
const summary = await getGitAllBranches(cwd)
|
|
127
|
+
return {
|
|
128
|
+
action: 'branches',
|
|
129
|
+
...summary.current !== undefined ? { current: summary.current } : {},
|
|
130
|
+
local: summary.local.map(b => ({
|
|
131
|
+
name: b.name,
|
|
132
|
+
isCurrent: b.isCurrent,
|
|
133
|
+
...b.upstream !== undefined ? { upstream: b.upstream } : {},
|
|
134
|
+
...b.ahead !== undefined ? { ahead: b.ahead } : {},
|
|
135
|
+
...b.behind !== undefined ? { behind: b.behind } : {},
|
|
136
|
+
})),
|
|
137
|
+
remote: summary.remote.map(b => ({
|
|
138
|
+
name: b.name,
|
|
139
|
+
isCurrent: b.isCurrent,
|
|
140
|
+
})),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
case 'tags': {
|
|
144
|
+
const rawTags = await getGitTags(cwd)
|
|
145
|
+
return {
|
|
146
|
+
action: 'tags',
|
|
147
|
+
total: rawTags.length,
|
|
148
|
+
tags: rawTags.map(t => ({
|
|
149
|
+
name: t.name,
|
|
150
|
+
commitHash: t.commitHash,
|
|
151
|
+
shortHash: t.shortHash,
|
|
152
|
+
date: t.date,
|
|
153
|
+
relativeDate: t.relativeDate,
|
|
154
|
+
message: t.message,
|
|
155
|
+
author: t.author,
|
|
156
|
+
isAnnotated: t.isAnnotated,
|
|
157
|
+
})),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
case 'status':
|
|
161
|
+
default: {
|
|
162
|
+
const status = await getGitStatus(cwd)
|
|
163
|
+
return {
|
|
164
|
+
action: 'status',
|
|
165
|
+
isRepo: status.isRepo,
|
|
166
|
+
...status.rootPath !== undefined ? { rootPath: status.rootPath } : {},
|
|
167
|
+
...status.branch !== undefined ? { branch: status.branch } : {},
|
|
168
|
+
...status.upstream !== undefined ? { upstream: status.upstream } : {},
|
|
169
|
+
ahead: status.ahead,
|
|
170
|
+
behind: status.behind,
|
|
171
|
+
staged: status.staged.map(f => ({ path: f.path, status: f.status, staged: f.staged })),
|
|
172
|
+
unstaged: status.unstaged.map(f => ({ path: f.path, status: f.status, staged: f.staged })),
|
|
173
|
+
untracked: status.untracked.map(f => ({ path: f.path, status: f.status, staged: f.staged })),
|
|
174
|
+
totalChanges: status.totalChanges,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
presentCall: args => ({ card: 'generic', title: `git_view (${args.action ?? 'status'})`, rawInput: args }),
|
|
180
|
+
})), 'experimental-git: register git_view tool')
|
|
181
|
+
|
|
182
|
+
// 2. Dedicated tool for checking out or creating branches
|
|
183
|
+
scope.effect(() => scope.tools.register(defineTool({
|
|
184
|
+
name: 'git_checkout',
|
|
185
|
+
description: 'Switch branch or create and checkout a new branch in the current project Git repository.',
|
|
186
|
+
parameters: {
|
|
187
|
+
branch: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
required: true,
|
|
190
|
+
description: 'The target branch name to switch to (e.g. "feature/auth", "dev", "master").',
|
|
191
|
+
},
|
|
192
|
+
create: {
|
|
193
|
+
type: 'boolean',
|
|
194
|
+
description: 'Whether to create a new branch from current HEAD and check it out (-b). Defaults to false.',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
output: {
|
|
198
|
+
schema: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
additionalProperties: true,
|
|
201
|
+
},
|
|
202
|
+
render: (_args, val) => {
|
|
203
|
+
const value = val as { success?: boolean; branch?: string; message?: string }
|
|
204
|
+
return [{
|
|
205
|
+
type: 'text',
|
|
206
|
+
text: value.success
|
|
207
|
+
? `Successfully checked out branch '${value.branch ?? ''}'.`
|
|
208
|
+
: `Failed to checkout branch '${value.branch ?? ''}': ${value.message ?? ''}`,
|
|
209
|
+
}]
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
async execute(args, exec): Promise<Record<string, JsonValue>> {
|
|
214
|
+
const cwd = resolveDir(exec.agent?.session.id)
|
|
215
|
+
const branch = args.branch.trim()
|
|
216
|
+
const createNew = args.create ?? false
|
|
217
|
+
const result = await gitCheckout(cwd, branch, createNew)
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
success: result.success,
|
|
221
|
+
branch,
|
|
222
|
+
message: result.message ?? '',
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
presentCall: args => ({
|
|
226
|
+
card: 'generic',
|
|
227
|
+
title: `git_checkout ${args.create ? '-b ' : ''}${args.branch}`,
|
|
228
|
+
rawInput: args,
|
|
229
|
+
}),
|
|
230
|
+
})), 'experimental-git: register git_checkout tool')
|
|
231
|
+
|
|
232
|
+
// 3. Dedicated tool for creating or deleting git tags
|
|
233
|
+
scope.effect(() => scope.tools.register(defineTool({
|
|
234
|
+
name: 'git_tag',
|
|
235
|
+
description: 'Create or delete a Git tag in the current project repository.',
|
|
236
|
+
parameters: {
|
|
237
|
+
name: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
required: true,
|
|
240
|
+
description: 'The tag name (e.g. "v1.0.0", "release-20261020").',
|
|
241
|
+
},
|
|
242
|
+
action: {
|
|
243
|
+
type: 'string',
|
|
244
|
+
enum: ['create', 'delete'],
|
|
245
|
+
description: 'Tag operation: "create" (default) or "delete".',
|
|
246
|
+
},
|
|
247
|
+
commit: {
|
|
248
|
+
type: 'string',
|
|
249
|
+
description: 'Target commit hash to place the tag on when creating (defaults to current HEAD).',
|
|
250
|
+
},
|
|
251
|
+
message: {
|
|
252
|
+
type: 'string',
|
|
253
|
+
description: 'Optional annotation message for an annotated tag (git tag -a -m).',
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
output: {
|
|
257
|
+
schema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
additionalProperties: true,
|
|
260
|
+
},
|
|
261
|
+
render: (_args, val) => {
|
|
262
|
+
const value = val as { success?: boolean; name?: string; message?: string }
|
|
263
|
+
return [{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: value.message ?? (value.success ? 'Success' : 'Failed'),
|
|
266
|
+
}]
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
async execute(args, exec): Promise<Record<string, JsonValue>> {
|
|
270
|
+
const cwd = resolveDir(exec.agent?.session.id)
|
|
271
|
+
const name = args.name.trim()
|
|
272
|
+
const action = args.action ?? 'create'
|
|
273
|
+
|
|
274
|
+
if (action === 'delete') {
|
|
275
|
+
const result = await gitDeleteTag(cwd, name)
|
|
276
|
+
return {
|
|
277
|
+
success: result.success,
|
|
278
|
+
name,
|
|
279
|
+
action: 'delete',
|
|
280
|
+
message: result.message ?? '',
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const result = await gitCreateTag(cwd, name, args.commit, args.message)
|
|
285
|
+
return {
|
|
286
|
+
success: result.success,
|
|
287
|
+
name,
|
|
288
|
+
action: 'create',
|
|
289
|
+
message: result.message ?? '',
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
presentCall: args => ({
|
|
293
|
+
card: 'generic',
|
|
294
|
+
title: `git_tag ${args.action ?? 'create'} ${args.name}`,
|
|
295
|
+
rawInput: args,
|
|
296
|
+
}),
|
|
297
|
+
})), 'experimental-git: register git_tag tool')
|
|
298
|
+
})
|
|
299
|
+
}
|