agent-termo 0.0.0-stage → 1.0.0
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/.claude/agents/backend-architecture-designer.md +1250 -0
- package/.claude/agents/backend-expert.md +1731 -0
- package/.claude/agents/deployment-expert.md +1126 -0
- package/.claude/agents/dev-environment-initiator.md +788 -0
- package/.claude/agents/frontend-expert.md +1343 -0
- package/.claude/agents/frontend-planner.md +1044 -0
- package/.claude/agents/git-manager.md +811 -0
- package/.claude/agents/project-manager.md +843 -0
- package/.claude/agents/security-expert.md +835 -0
- package/.claude/agents/tech-stack-expert.md +1357 -0
- package/.claude/agents/test-agent.md +1372 -0
- package/.claude/agents/uiux-designer.md +955 -0
- package/.claude/commands/build.md +5 -0
- package/.claude/commands/deploy.md +3 -0
- package/.claude/commands/feature.md +3 -0
- package/.claude/commands/fix.md +3 -0
- package/.claude/skills/pdf-reader/.skillfish.json +10 -0
- package/.claude/skills/pdf-reader/SKILL.md +94 -0
- package/.claude/skills/pdf-reader/pdf-reader +203 -0
- package/README.md +169 -3
- package/dist/.claude/agents/backend-architecture-designer.md +1250 -0
- package/dist/.claude/agents/backend-expert.md +1731 -0
- package/dist/.claude/agents/deployment-expert.md +1126 -0
- package/dist/.claude/agents/dev-environment-initiator.md +788 -0
- package/dist/.claude/agents/frontend-expert.md +1343 -0
- package/dist/.claude/agents/frontend-planner.md +1044 -0
- package/dist/.claude/agents/git-manager.md +811 -0
- package/dist/.claude/agents/project-manager.md +843 -0
- package/dist/.claude/agents/security-expert.md +835 -0
- package/dist/.claude/agents/tech-stack-expert.md +1357 -0
- package/dist/.claude/agents/test-agent.md +1372 -0
- package/dist/.claude/agents/uiux-designer.md +955 -0
- package/dist/.claude/commands/build.md +5 -0
- package/dist/.claude/commands/deploy.md +3 -0
- package/dist/.claude/commands/feature.md +3 -0
- package/dist/.claude/commands/fix.md +3 -0
- package/dist/.claude/skills/pdf-reader/.skillfish.json +10 -0
- package/dist/.claude/skills/pdf-reader/SKILL.md +94 -0
- package/dist/.claude/skills/pdf-reader/pdf-reader +203 -0
- package/dist/cli.js +206 -0
- package/dist/cli.js.map +7 -0
- package/dist/main.js +173 -0
- package/dist/main.js.map +7 -0
- package/package.json +89 -5
- package/src/agents/index.ts +94 -0
- package/src/cli.ts +76 -0
- package/src/diff/index.ts +255 -0
- package/src/llm/client.ts +192 -0
- package/src/llm/tools.ts +206 -0
- package/src/main.ts +427 -0
- package/src/memory/index.ts +130 -0
- package/src/tools/index.ts +371 -0
- package/src/types/index.ts +151 -0
- package/src/ui/index.ts +519 -0
- package/src/utils/ansi.ts +134 -0
- package/src/utils/config.ts +75 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Agent-Termo Tool Executor — routes tool calls to implementations
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import { readFile as readFileAsync, writeFile as writeFileAsync } from 'node:fs/promises';
|
|
6
|
+
import { resolve, isAbsolute, relative } from 'node:path';
|
|
7
|
+
import { exec as execAsync } from 'node:child_process';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { default as globAsync } from 'glob';
|
|
10
|
+
import { simpleGit } from 'simple-git';
|
|
11
|
+
import { statSync, existsSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { getConfig } from '../utils/config.js';
|
|
13
|
+
import { computeDiff } from '../diff/index.js';
|
|
14
|
+
import type { ToolResult, ProposedFileChange } from '../types/index.js';
|
|
15
|
+
|
|
16
|
+
const exec = promisify(execAsync);
|
|
17
|
+
const globPromise = promisify(globAsync);
|
|
18
|
+
|
|
19
|
+
// ── Tool map ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const TOOL_MAP: Record<string, (...args: any[]) => Promise<ToolResult>> = {
|
|
22
|
+
'file:read': toolFileRead,
|
|
23
|
+
'file:write': toolFileWrite,
|
|
24
|
+
'file:edit': toolFileEdit,
|
|
25
|
+
'file:glob': toolFileGlob,
|
|
26
|
+
'grep': toolGrep,
|
|
27
|
+
'bash:execute': toolBash,
|
|
28
|
+
'git:status': toolGitStatus,
|
|
29
|
+
'git:commit': toolGitCommit,
|
|
30
|
+
'git:push': toolGitPush,
|
|
31
|
+
'git:log': toolGitLog,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ── Re-exported tool definitions for OpenAI schema ───────────────────────────
|
|
35
|
+
export const TOOL_DEFS = TOOL_MAP;
|
|
36
|
+
|
|
37
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export async function executeTool(
|
|
40
|
+
name: string,
|
|
41
|
+
args: Record<string, unknown>,
|
|
42
|
+
projectRoot: string,
|
|
43
|
+
): Promise<ToolResult> {
|
|
44
|
+
const start = Date.now();
|
|
45
|
+
const id = crypto.randomUUID();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const fn = TOOL_MAP[name];
|
|
49
|
+
if (!fn) {
|
|
50
|
+
return { id, name, success: false, error: 'Unknown tool: ' + name, durationMs: Date.now() - start };
|
|
51
|
+
}
|
|
52
|
+
return await fn(args, projectRoot);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
name,
|
|
57
|
+
success: false,
|
|
58
|
+
error: String(err instanceof Error ? err.message : err),
|
|
59
|
+
durationMs: Date.now() - start,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Path helpers ─────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function toProjectPath(path: string, projectRoot: string): string {
|
|
67
|
+
if (isAbsolute(path)) return path;
|
|
68
|
+
return resolve(projectRoot, path);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function withinProject(path: string, projectRoot: string): boolean {
|
|
72
|
+
try {
|
|
73
|
+
const resolved = toProjectPath(path, projectRoot);
|
|
74
|
+
const rel = relative(projectRoot, resolved);
|
|
75
|
+
return !rel.startsWith('..') && !isAbsolute(rel);
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Propose a file change (returns ProposedFileChange without writing) ───────
|
|
82
|
+
|
|
83
|
+
function proposeFileChange(
|
|
84
|
+
path: string,
|
|
85
|
+
proposedContent: string,
|
|
86
|
+
projectRoot: string,
|
|
87
|
+
changeType: 'write' | 'edit',
|
|
88
|
+
oldContent?: string,
|
|
89
|
+
): { change: ProposedFileChange; result: ToolResult } {
|
|
90
|
+
const fullPath = toProjectPath(path, projectRoot);
|
|
91
|
+
const originalContent = oldContent ?? '';
|
|
92
|
+
const isNewFile = !existsSync(fullPath);
|
|
93
|
+
const isDeletion = proposedContent === '' && changeType === 'write';
|
|
94
|
+
|
|
95
|
+
const baseContent = isNewFile ? '' : originalContent;
|
|
96
|
+
const diff = computeDiff(baseContent, proposedContent);
|
|
97
|
+
|
|
98
|
+
const change: ProposedFileChange = {
|
|
99
|
+
path,
|
|
100
|
+
originalContent,
|
|
101
|
+
proposedContent,
|
|
102
|
+
diff: { ...diff, file: path },
|
|
103
|
+
isNewFile,
|
|
104
|
+
isDeletion,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const lineCount = proposedContent.split('\n').length;
|
|
108
|
+
const summary = isDeletion
|
|
109
|
+
? `Delete ${path} (${lineCount} lines removed)`
|
|
110
|
+
: isNewFile
|
|
111
|
+
? `Create ${path} (${lineCount} lines)`
|
|
112
|
+
: `${changeType === 'edit' ? 'Edit' : 'Overwrite'} ${path} (${lineCount} lines, +${diff.additions}/-${diff.deletions})`;
|
|
113
|
+
|
|
114
|
+
const result: ToolResult = {
|
|
115
|
+
id: '',
|
|
116
|
+
name: `${changeType}:${path}`,
|
|
117
|
+
success: true,
|
|
118
|
+
output: JSON.stringify({
|
|
119
|
+
message: summary,
|
|
120
|
+
needsApproval: true,
|
|
121
|
+
proposedChange: change,
|
|
122
|
+
}),
|
|
123
|
+
durationMs: 0,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return { change, result };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Apply an approved change to disk ────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export async function applyFileChange(change: ProposedFileChange, projectRoot: string): Promise<ToolResult> {
|
|
132
|
+
const start = Date.now();
|
|
133
|
+
const fullPath = toProjectPath(change.path, projectRoot);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
if (change.isDeletion) {
|
|
137
|
+
if (existsSync(fullPath)) {
|
|
138
|
+
await writeFileAsync(fullPath, '');
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
const dir = resolve(fullPath, '..');
|
|
142
|
+
if (!existsSync(dir)) {
|
|
143
|
+
mkdirSync(dir, { recursive: true });
|
|
144
|
+
}
|
|
145
|
+
await writeFileAsync(fullPath, change.proposedContent, 'utf-8');
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
id: '',
|
|
149
|
+
name: 'file:write',
|
|
150
|
+
success: true,
|
|
151
|
+
output: `Applied: ${change.path}`,
|
|
152
|
+
durationMs: Date.now() - start,
|
|
153
|
+
};
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return {
|
|
156
|
+
id: '',
|
|
157
|
+
name: 'file:write',
|
|
158
|
+
success: false,
|
|
159
|
+
error: 'Cannot apply ' + change.path + ': ' + (err instanceof Error ? err.message : String(err)),
|
|
160
|
+
durationMs: Date.now() - start,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── File Tools ──────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
async function toolFileRead(args: Record<string, unknown>, projectRoot: string): Promise<ToolResult> {
|
|
168
|
+
const path = String(args.path ?? '');
|
|
169
|
+
if (!path) return { id: '', name: 'file:read', success: false, error: 'path is required', durationMs: 0 };
|
|
170
|
+
if (!withinProject(path, projectRoot)) {
|
|
171
|
+
return { id: '', name: 'file:read', success: false, error: 'Access denied: ' + path, durationMs: 0 };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const fullPath = toProjectPath(path, projectRoot);
|
|
175
|
+
const limit = Number(args.limit ?? 0);
|
|
176
|
+
const offset = Number(args.offset ?? 0);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const statResult = statSync(fullPath);
|
|
180
|
+
if (statResult.isDirectory()) {
|
|
181
|
+
return { id: '', name: 'file:read', success: false, error: 'Path "' + path + '" is a directory', durationMs: 0 };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const content = await readFileAsync(fullPath, 'utf-8');
|
|
185
|
+
const lines = content.split('\n');
|
|
186
|
+
const sliced = lines.slice(offset, limit > 0 ? offset + limit : undefined);
|
|
187
|
+
const summary = limit > 0 ? '[Showing lines ' + (offset + 1) + '-' + (offset + sliced.length) + ' of ' + lines.length + ']' : '';
|
|
188
|
+
return { id: '', name: 'file:read', success: true, output: (summary ? summary + '\n' : '') + sliced.join('\n'), durationMs: 0 };
|
|
189
|
+
} catch (err) {
|
|
190
|
+
return { id: '', name: 'file:read', success: false, error: 'Cannot read ' + path + ': ' + (err instanceof Error ? err.message : String(err)), durationMs: 0 };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function toolFileWrite(args: Record<string, unknown>, projectRoot: string): Promise<ToolResult> {
|
|
195
|
+
const path = String(args.path ?? '');
|
|
196
|
+
const content = String(args.content ?? '');
|
|
197
|
+
if (!path) return { id: '', name: 'file:write', success: false, error: 'path is required', durationMs: 0 };
|
|
198
|
+
if (!withinProject(path, projectRoot)) {
|
|
199
|
+
return { id: '', name: 'file:write', success: false, error: 'Access denied: ' + path, durationMs: 0 };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const { result } = proposeFileChange(path, content, projectRoot, 'write');
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function toolFileEdit(args: Record<string, unknown>, projectRoot: string): Promise<ToolResult> {
|
|
207
|
+
const path = String(args.path ?? '');
|
|
208
|
+
const oldString = String(args.old_string ?? '');
|
|
209
|
+
const newString = String(args.new_string ?? '');
|
|
210
|
+
if (!path || !oldString) return { id: '', name: 'file:edit', success: false, error: 'path and old_string are required', durationMs: 0 };
|
|
211
|
+
if (!withinProject(path, projectRoot)) {
|
|
212
|
+
return { id: '', name: 'file:edit', success: false, error: 'Access denied: ' + path, durationMs: 0 };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const fullPath = toProjectPath(path, projectRoot);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const existingContent = existsSync(fullPath) ? await readFileAsync(fullPath, 'utf-8') : '';
|
|
219
|
+
if (!existingContent.includes(oldString)) {
|
|
220
|
+
return { id: '', name: 'file:edit', success: false, error: 'old_string not found in ' + path + '. Check whitespace.', durationMs: 0 };
|
|
221
|
+
}
|
|
222
|
+
const newContent = existingContent.replace(oldString, newString);
|
|
223
|
+
const { result } = proposeFileChange(path, newContent, projectRoot, 'edit', existingContent);
|
|
224
|
+
return result;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return { id: '', name: 'file:edit', success: false, error: 'Cannot edit ' + path + ': ' + (err instanceof Error ? err.message : String(err)), durationMs: 0 };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function toolFileGlob(args: Record<string, unknown>, projectRoot: string): Promise<ToolResult> {
|
|
231
|
+
const globPattern = String(args.pattern ?? '');
|
|
232
|
+
if (!globPattern) return { id: '', name: 'file:glob', success: false, error: 'pattern is required', durationMs: 0 };
|
|
233
|
+
|
|
234
|
+
const cwd = args.cwd ? String(args.cwd) : projectRoot;
|
|
235
|
+
if (!withinProject(cwd, projectRoot)) {
|
|
236
|
+
return { id: '', name: 'file:glob', success: false, error: 'Access denied', durationMs: 0 };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const files = await globPromise(globPattern, { cwd, absolute: false });
|
|
241
|
+
return { id: '', name: 'file:glob', success: true, output: files.length > 0 ? files.join('\n') : '(no files matched)', durationMs: 0 };
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return { id: '', name: 'file:glob', success: false, error: 'glob error: ' + (err instanceof Error ? err.message : String(err)), durationMs: 0 };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function toolGrep(args: Record<string, unknown>, projectRoot: string): Promise<ToolResult> {
|
|
248
|
+
const pattern = String(args.pattern ?? '');
|
|
249
|
+
const path = args.path ? String(args.path) : '.';
|
|
250
|
+
const caseSensitive = args.case_sensitive !== false;
|
|
251
|
+
|
|
252
|
+
if (!pattern || !path) return { id: '', name: 'grep', success: false, error: 'pattern and path are required', durationMs: 0 };
|
|
253
|
+
|
|
254
|
+
if (isAbsolute(path) && !withinProject(path, projectRoot)) {
|
|
255
|
+
return { id: '', name: 'grep', success: false, error: 'Access denied', durationMs: 0 };
|
|
256
|
+
}
|
|
257
|
+
const fullPath = isAbsolute(path) ? path : resolve(projectRoot, path);
|
|
258
|
+
const flags = caseSensitive ? '' : '-i';
|
|
259
|
+
const includeFlag = args.glob ? ['--include=' + String(args.glob)] : [];
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const cmd = ['grep', flags, '-rn', '--color=never', pattern, fullPath, ...includeFlag].join(' ');
|
|
263
|
+
const { stdout, stderr } = await exec(cmd, { timeout: 30000 } as { timeout: number });
|
|
264
|
+
const results = (stdout || stderr).trim();
|
|
265
|
+
return { id: '', name: 'grep', success: true, output: results || '(no matches found)', durationMs: 0 };
|
|
266
|
+
} catch (err: unknown) {
|
|
267
|
+
const errorObj = err as { code?: string | number; message?: string; stdout?: string; stderr?: string };
|
|
268
|
+
if (errorObj.code === 1) return { id: '', name: 'grep', success: true, output: '(no matches found)', durationMs: 0 };
|
|
269
|
+
return { id: '', name: 'grep', success: false, error: 'grep error: ' + (err instanceof Error ? err.message : String(err)), durationMs: 0 };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Bash ─────────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
const DANGEROUS_PATTERNS = [
|
|
276
|
+
/rm\s+-rf\s+['"]?\//i,
|
|
277
|
+
/dd\s+if=/i,
|
|
278
|
+
/>\s*\/dev\/sda/i,
|
|
279
|
+
/mkfs\./i,
|
|
280
|
+
/:\s*\(\)\s*\{\s*:\|:&?\s*\};?\s*:/i,
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
async function toolBash(args: Record<string, unknown>, projectRoot: string): Promise<ToolResult> {
|
|
284
|
+
const command = String(args.command ?? '');
|
|
285
|
+
const cwd = args.cwd ? String(args.cwd) : projectRoot;
|
|
286
|
+
const timeout = Number(args.timeout ?? getConfig(projectRoot).bashTimeout);
|
|
287
|
+
|
|
288
|
+
if (!command) return { id: '', name: 'bash:execute', success: false, error: 'command is required', durationMs: 0 };
|
|
289
|
+
|
|
290
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
291
|
+
if (pattern.test(command)) {
|
|
292
|
+
return { id: '', name: 'bash:execute', success: false, error: 'Dangerous command blocked: ' + command.slice(0, 50), durationMs: 0 };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const { stdout, stderr } = await exec(command, { cwd, timeout } as { cwd: string; timeout: number });
|
|
298
|
+
const output = [stdout, stderr].filter(Boolean).join('\n');
|
|
299
|
+
return { id: '', name: 'bash:execute', success: true, output: output || '(no output)', durationMs: 0 };
|
|
300
|
+
} catch (err: unknown) {
|
|
301
|
+
const errorObj = err as { message?: string; stdout?: string; stderr?: string };
|
|
302
|
+
const combined = [errorObj.stdout, errorObj.stderr].filter(Boolean).join('\n');
|
|
303
|
+
return { id: '', name: 'bash:execute', success: false, error: combined.slice(0, 500) || errorObj.message, durationMs: 0 };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Git Tools ────────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
async function toolGitStatus(args: Record<string, unknown>, projectRoot: string): Promise<ToolResult> {
|
|
310
|
+
const cwd = args.cwd ? String(args.cwd) : projectRoot;
|
|
311
|
+
try {
|
|
312
|
+
const git = simpleGit(cwd);
|
|
313
|
+
const status = await git.status();
|
|
314
|
+
const tracking = status.tracking;
|
|
315
|
+
const lines = [
|
|
316
|
+
'Current branch: ' + (status.current ?? 'unknown'),
|
|
317
|
+
tracking ? 'tracking: ' + tracking : '',
|
|
318
|
+
'',
|
|
319
|
+
'Modified: ' + (status.modified.length > 0 ? status.modified.join(', ') : '(none)'),
|
|
320
|
+
'Created: ' + (status.created.length > 0 ? status.created.join(', ') : '(none)'),
|
|
321
|
+
'Deleted: ' + (status.deleted.length > 0 ? status.deleted.join(', ') : '(none)'),
|
|
322
|
+
'',
|
|
323
|
+
'Not staged: ' + (status.not_added.length > 0 ? status.not_added.join(', ') : '(none)'),
|
|
324
|
+
'',
|
|
325
|
+
'Untracked: ' + (status.files.length > 0 ? status.files.join(', ') : '(none)'),
|
|
326
|
+
].filter(Boolean).join('\n');
|
|
327
|
+
return { id: '', name: 'git:status', success: true, output: lines, durationMs: 0 };
|
|
328
|
+
} catch (err) {
|
|
329
|
+
return { id: '', name: 'git:status', success: false, error: err instanceof Error ? err.message : String(err), durationMs: 0 };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function toolGitCommit(args: Record<string, unknown>, projectRoot: string): Promise<ToolResult> {
|
|
334
|
+
const message = String(args.message ?? '');
|
|
335
|
+
const cwd = args.cwd ? String(args.cwd) : projectRoot;
|
|
336
|
+
if (!message) return { id: '', name: 'git:commit', success: false, error: 'message is required', durationMs: 0 };
|
|
337
|
+
try {
|
|
338
|
+
const git = simpleGit(cwd);
|
|
339
|
+
await git.add('.');
|
|
340
|
+
const result = await git.commit(message);
|
|
341
|
+
return { id: '', name: 'git:commit', success: true, output: 'Committed ' + result.commit + ' — ' + message, durationMs: 0 };
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return { id: '', name: 'git:commit', success: false, error: err instanceof Error ? err.message : String(err), durationMs: 0 };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function toolGitPush(args: Record<string, unknown>, projectRoot: string): Promise<ToolResult> {
|
|
348
|
+
const cwd = args.cwd ? String(args.cwd) : projectRoot;
|
|
349
|
+
try {
|
|
350
|
+
const git = simpleGit(cwd);
|
|
351
|
+
await git.push();
|
|
352
|
+
return { id: '', name: 'git:push', success: true, output: 'Pushed to remote.', durationMs: 0 };
|
|
353
|
+
} catch (err) {
|
|
354
|
+
return { id: '', name: 'git:push', success: false, error: err instanceof Error ? err.message : String(err), durationMs: 0 };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function toolGitLog(args: Record<string, unknown>, projectRoot: string): Promise<ToolResult> {
|
|
359
|
+
const cwd = args.cwd ? String(args.cwd) : projectRoot;
|
|
360
|
+
const limit = Number(args.limit ?? 20);
|
|
361
|
+
try {
|
|
362
|
+
const git = simpleGit(cwd);
|
|
363
|
+
const log = await git.log({ maxCount: limit });
|
|
364
|
+
const lines = log.all
|
|
365
|
+
.map((c: { hash: string; date: string; message: string; author_name: string }) => c.hash.slice(0, 7) + ' | ' + c.date.slice(0, 10) + ' | ' + c.message + ' (' + c.author_name + ')')
|
|
366
|
+
.join('\n');
|
|
367
|
+
return { id: '', name: 'git:log', success: true, output: lines, durationMs: 0 };
|
|
368
|
+
} catch (err) {
|
|
369
|
+
return { id: '', name: 'git:log', success: false, error: err instanceof Error ? err.message : String(err), durationMs: 0 };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Agent-Termo Type Definitions
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
export type Role = 'system' | 'user' | 'assistant' | 'tool';
|
|
6
|
+
|
|
7
|
+
export interface Message {
|
|
8
|
+
role: Role;
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ConversationMessage extends Message {
|
|
13
|
+
id: string;
|
|
14
|
+
timestamp: Date;
|
|
15
|
+
agent?: string; // which agent produced this (for display badge)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ToolCall {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
arguments: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ToolResult {
|
|
25
|
+
id: string; // matches the ToolCall.id
|
|
26
|
+
name: string;
|
|
27
|
+
success: boolean;
|
|
28
|
+
output?: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
durationMs: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Diff / Change Review Types ──────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export type DiffLineType = 'add' | 'remove' | 'context';
|
|
36
|
+
|
|
37
|
+
export interface DiffLine {
|
|
38
|
+
type: DiffLineType;
|
|
39
|
+
lineNumber: number; // 1-based line number in the proposed file (0 for ellipsis)
|
|
40
|
+
content: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DiffResult {
|
|
44
|
+
file: string;
|
|
45
|
+
lines: DiffLine[];
|
|
46
|
+
additions: number;
|
|
47
|
+
deletions: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ProposedFileChange {
|
|
51
|
+
path: string;
|
|
52
|
+
originalContent: string;
|
|
53
|
+
proposedContent: string;
|
|
54
|
+
diff: DiffResult;
|
|
55
|
+
isNewFile: boolean; // true if file didn't exist before
|
|
56
|
+
isDeletion: boolean; // true if proposedContent is empty (delete)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ReviewAction = 'accept' | 'reject' | 'edit' | 'batch';
|
|
60
|
+
|
|
61
|
+
export interface ReviewPrompt {
|
|
62
|
+
changes: ProposedFileChange[];
|
|
63
|
+
action: ReviewAction;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// LLM Types
|
|
67
|
+
export interface LLMConfig {
|
|
68
|
+
apiKey: string;
|
|
69
|
+
baseUrl: string;
|
|
70
|
+
model: string;
|
|
71
|
+
timeout: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface LLMResponse {
|
|
75
|
+
content: string;
|
|
76
|
+
finishReason: 'stop' | 'length' | 'tool_calls' | 'content_filter';
|
|
77
|
+
toolCalls: ToolCall[];
|
|
78
|
+
usage?: {
|
|
79
|
+
promptTokens: number;
|
|
80
|
+
completionTokens: number;
|
|
81
|
+
totalTokens: number;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Tool Schema Types (OpenAI function calling format)
|
|
86
|
+
export interface ToolParameterProperty {
|
|
87
|
+
type: string;
|
|
88
|
+
description?: string;
|
|
89
|
+
default?: unknown;
|
|
90
|
+
enum?: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ToolParameter {
|
|
94
|
+
type: 'object';
|
|
95
|
+
properties: Record<string, ToolParameterProperty>;
|
|
96
|
+
required?: string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ToolDef {
|
|
100
|
+
name: string;
|
|
101
|
+
description: string;
|
|
102
|
+
parameters: ToolParameter;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface Tool {
|
|
106
|
+
name: string;
|
|
107
|
+
description: string;
|
|
108
|
+
execute(args: Record<string, unknown>): Promise<ToolResult>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Session
|
|
112
|
+
export interface Session {
|
|
113
|
+
id: string;
|
|
114
|
+
projectRoot: string;
|
|
115
|
+
startedAt: Date;
|
|
116
|
+
messages: ConversationMessage[];
|
|
117
|
+
currentAgent: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Agent
|
|
121
|
+
export interface Agent {
|
|
122
|
+
name: string;
|
|
123
|
+
description: string;
|
|
124
|
+
systemPrompt: string;
|
|
125
|
+
tools: string[]; // tool names this agent can use
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// UI Types
|
|
129
|
+
export interface RenderOptions {
|
|
130
|
+
timestamp?: boolean;
|
|
131
|
+
agentBadge?: string;
|
|
132
|
+
color?: 'user' | 'assistant' | 'error' | 'success' | 'info' | 'warning';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface InputHistory {
|
|
136
|
+
history: string[];
|
|
137
|
+
index: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Config
|
|
141
|
+
export interface AppConfig {
|
|
142
|
+
nimApiKey: string;
|
|
143
|
+
nimBaseUrl: string;
|
|
144
|
+
nimModel: string;
|
|
145
|
+
agentTermoDir: string;
|
|
146
|
+
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
|
147
|
+
confirmDestructive: boolean;
|
|
148
|
+
allowedCommands: string[];
|
|
149
|
+
llmTimeout: number;
|
|
150
|
+
bashTimeout: number;
|
|
151
|
+
}
|