@alia-codea/cli 1.0.0 → 2.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/dist/index.js +1362 -491
- package/package.json +5 -5
- package/src/app.tsx +281 -0
- package/src/commands/auth.ts +149 -20
- package/src/commands/repl.ts +11 -299
- package/src/commands/run.ts +103 -126
- package/src/commands/sessions.ts +5 -6
- package/src/components/ApprovalPrompt.tsx +60 -0
- package/src/components/Header.tsx +39 -0
- package/src/components/InputBar.tsx +36 -0
- package/src/components/MessageList.tsx +81 -0
- package/src/components/ThinkingIndicator.tsx +28 -0
- package/src/components/ToolCallCard.tsx +68 -0
- package/src/index.ts +31 -11
- package/src/tools/executor.ts +140 -14
- package/src/tools/patch.ts +167 -0
- package/src/utils/api.ts +22 -3
- package/src/utils/approval.ts +31 -0
- package/src/utils/context.ts +65 -4
- package/src/utils/conversation.ts +141 -0
- package/dist/api-X2G5QROW.js +0 -10
- package/dist/chunk-SVPL4GNV.js +0 -230
- package/dist/index.d.ts +0 -1
- package/src/utils/ui.ts +0 -153
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
interface Hunk {
|
|
5
|
+
oldStart: number;
|
|
6
|
+
oldLines: string[];
|
|
7
|
+
newLines: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FilePatch {
|
|
11
|
+
filePath: string;
|
|
12
|
+
hunks: Hunk[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface PatchResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
results: Array<{ file: string; success: boolean; message: string }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parsePatch(patchText: string): FilePatch[] {
|
|
21
|
+
const files: FilePatch[] = [];
|
|
22
|
+
const lines = patchText.split('\n');
|
|
23
|
+
let currentFile: FilePatch | null = null;
|
|
24
|
+
let currentHunk: Hunk | null = null;
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const line = lines[i];
|
|
28
|
+
|
|
29
|
+
// File header: --- a/path or --- path
|
|
30
|
+
if (line.startsWith('--- ')) {
|
|
31
|
+
// Next line should be +++ b/path
|
|
32
|
+
const nextLine = lines[i + 1];
|
|
33
|
+
if (nextLine && nextLine.startsWith('+++ ')) {
|
|
34
|
+
let filePath = nextLine.slice(4).trim();
|
|
35
|
+
// Remove b/ prefix
|
|
36
|
+
if (filePath.startsWith('b/')) {
|
|
37
|
+
filePath = filePath.slice(2);
|
|
38
|
+
}
|
|
39
|
+
currentFile = { filePath, hunks: [] };
|
|
40
|
+
files.push(currentFile);
|
|
41
|
+
i++; // skip the +++ line
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Hunk header: @@ -start,count +start,count @@
|
|
47
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/);
|
|
48
|
+
if (hunkMatch && currentFile) {
|
|
49
|
+
currentHunk = {
|
|
50
|
+
oldStart: parseInt(hunkMatch[1], 10),
|
|
51
|
+
oldLines: [],
|
|
52
|
+
newLines: [],
|
|
53
|
+
};
|
|
54
|
+
currentFile.hunks.push(currentHunk);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Hunk content
|
|
59
|
+
if (currentHunk) {
|
|
60
|
+
if (line.startsWith('-')) {
|
|
61
|
+
currentHunk.oldLines.push(line.slice(1));
|
|
62
|
+
} else if (line.startsWith('+')) {
|
|
63
|
+
currentHunk.newLines.push(line.slice(1));
|
|
64
|
+
} else if (line.startsWith(' ')) {
|
|
65
|
+
// Context line - appears in both old and new
|
|
66
|
+
currentHunk.oldLines.push(line.slice(1));
|
|
67
|
+
currentHunk.newLines.push(line.slice(1));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findHunkPosition(fileLines: string[], hunkOldLines: string[], expectedStart: number, drift: number = 20): number {
|
|
76
|
+
// Try exact position first (0-indexed)
|
|
77
|
+
const start = expectedStart - 1;
|
|
78
|
+
if (matchesAt(fileLines, hunkOldLines, start)) {
|
|
79
|
+
return start;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fuzzy search within ±drift lines
|
|
83
|
+
for (let offset = 1; offset <= drift; offset++) {
|
|
84
|
+
if (matchesAt(fileLines, hunkOldLines, start + offset)) {
|
|
85
|
+
return start + offset;
|
|
86
|
+
}
|
|
87
|
+
if (matchesAt(fileLines, hunkOldLines, start - offset)) {
|
|
88
|
+
return start - offset;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return -1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function matchesAt(fileLines: string[], hunkOldLines: string[], position: number): boolean {
|
|
96
|
+
if (position < 0 || position + hunkOldLines.length > fileLines.length) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < hunkOldLines.length; i++) {
|
|
101
|
+
// Normalize whitespace for comparison
|
|
102
|
+
const fileLine = fileLines[position + i].trimEnd();
|
|
103
|
+
const hunkLine = hunkOldLines[i].trimEnd();
|
|
104
|
+
if (fileLine !== hunkLine) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function applyPatch(patchText: string, basePath: string): Promise<PatchResult> {
|
|
113
|
+
const filePatches = parsePatch(patchText);
|
|
114
|
+
const results: PatchResult['results'] = [];
|
|
115
|
+
let allSuccess = true;
|
|
116
|
+
|
|
117
|
+
for (const filePatch of filePatches) {
|
|
118
|
+
const absolutePath = path.resolve(basePath, filePatch.filePath);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
let content: string;
|
|
122
|
+
try {
|
|
123
|
+
content = await fs.readFile(absolutePath, 'utf-8');
|
|
124
|
+
} catch {
|
|
125
|
+
// File doesn't exist - if all hunks are additions, create it
|
|
126
|
+
const allAdditions = filePatch.hunks.every((h) => h.oldLines.length === 0);
|
|
127
|
+
if (allAdditions) {
|
|
128
|
+
const newContent = filePatch.hunks.map((h) => h.newLines.join('\n')).join('\n');
|
|
129
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
130
|
+
await fs.writeFile(absolutePath, newContent, 'utf-8');
|
|
131
|
+
results.push({ file: filePatch.filePath, success: true, message: 'Created new file' });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`File not found: ${filePatch.filePath}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let fileLines = content.split('\n');
|
|
138
|
+
|
|
139
|
+
// Apply hunks in reverse order to preserve line numbers
|
|
140
|
+
const sortedHunks = [...filePatch.hunks].sort((a, b) => b.oldStart - a.oldStart);
|
|
141
|
+
|
|
142
|
+
for (const hunk of sortedHunks) {
|
|
143
|
+
const position = findHunkPosition(fileLines, hunk.oldLines, hunk.oldStart);
|
|
144
|
+
if (position === -1) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Could not find match for hunk at line ${hunk.oldStart} in ${filePatch.filePath}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Replace old lines with new lines
|
|
151
|
+
fileLines.splice(position, hunk.oldLines.length, ...hunk.newLines);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await fs.writeFile(absolutePath, fileLines.join('\n'), 'utf-8');
|
|
155
|
+
results.push({
|
|
156
|
+
file: filePatch.filePath,
|
|
157
|
+
success: true,
|
|
158
|
+
message: `Applied ${filePatch.hunks.length} hunk(s)`,
|
|
159
|
+
});
|
|
160
|
+
} catch (error: any) {
|
|
161
|
+
allSuccess = false;
|
|
162
|
+
results.push({ file: filePatch.filePath, success: false, message: error.message });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { success: allSuccess, results };
|
|
167
|
+
}
|
package/src/utils/api.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const fileTools = [
|
|
|
59
59
|
type: 'function',
|
|
60
60
|
function: {
|
|
61
61
|
name: 'edit_file',
|
|
62
|
-
description: 'Make targeted edits to a file by replacing specific text',
|
|
62
|
+
description: 'Make targeted edits to a file by replacing specific text. For small single-location changes.',
|
|
63
63
|
parameters: {
|
|
64
64
|
type: 'object',
|
|
65
65
|
properties: {
|
|
@@ -71,6 +71,23 @@ export const fileTools = [
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
type: 'function',
|
|
76
|
+
function: {
|
|
77
|
+
name: 'apply_patch',
|
|
78
|
+
description: 'Apply a unified diff patch to one or more files. Preferred for multi-line or multi-file changes. Uses standard unified diff format with fuzzy line matching (±20 line drift).',
|
|
79
|
+
parameters: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
patch: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'The unified diff patch text. Must include --- a/file and +++ b/file headers and @@ hunk headers.'
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
required: ['patch']
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
74
91
|
{
|
|
75
92
|
type: 'function',
|
|
76
93
|
function: {
|
|
@@ -89,13 +106,15 @@ export const fileTools = [
|
|
|
89
106
|
type: 'function',
|
|
90
107
|
function: {
|
|
91
108
|
name: 'search_files',
|
|
92
|
-
description: 'Search for text patterns across files',
|
|
109
|
+
description: 'Search for text patterns across files. Uses ripgrep when available for fast results with context lines.',
|
|
93
110
|
parameters: {
|
|
94
111
|
type: 'object',
|
|
95
112
|
properties: {
|
|
96
113
|
pattern: { type: 'string', description: 'The search pattern (regex supported)' },
|
|
97
114
|
path: { type: 'string', description: 'Directory to search in (default: current)' },
|
|
98
|
-
file_pattern: { type: 'string', description: 'File glob pattern (e.g., "*.ts")' }
|
|
115
|
+
file_pattern: { type: 'string', description: 'File glob pattern (e.g., "*.ts")' },
|
|
116
|
+
context_lines: { type: 'number', description: 'Number of context lines around matches (default: 2)' },
|
|
117
|
+
max_results: { type: 'number', description: 'Maximum number of matches to return (default: 50)' }
|
|
99
118
|
},
|
|
100
119
|
required: ['pattern']
|
|
101
120
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ApprovalMode = 'suggest' | 'auto-edit' | 'full-auto';
|
|
2
|
+
export type ToolCategory = 'read_only' | 'file_write' | 'shell';
|
|
3
|
+
|
|
4
|
+
const TOOL_CATEGORIES: Record<string, ToolCategory> = {
|
|
5
|
+
read_file: 'read_only',
|
|
6
|
+
list_files: 'read_only',
|
|
7
|
+
search_files: 'read_only',
|
|
8
|
+
write_file: 'file_write',
|
|
9
|
+
edit_file: 'file_write',
|
|
10
|
+
apply_patch: 'file_write',
|
|
11
|
+
run_command: 'shell',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function categorize(toolName: string): ToolCategory {
|
|
15
|
+
return TOOL_CATEGORIES[toolName] || 'shell';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function needsApproval(toolName: string, mode: ApprovalMode): boolean {
|
|
19
|
+
const category = categorize(toolName);
|
|
20
|
+
|
|
21
|
+
switch (mode) {
|
|
22
|
+
case 'full-auto':
|
|
23
|
+
return false;
|
|
24
|
+
case 'auto-edit':
|
|
25
|
+
return category === 'shell';
|
|
26
|
+
case 'suggest':
|
|
27
|
+
return category === 'file_write' || category === 'shell';
|
|
28
|
+
default:
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/utils/context.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { promisify } from 'util';
|
|
|
5
5
|
|
|
6
6
|
const execAsync = promisify(exec);
|
|
7
7
|
|
|
8
|
-
export function buildSystemMessage(model: string, codebaseContext: string): string {
|
|
8
|
+
export function buildSystemMessage(model: string, codebaseContext: string, projectInstructions?: string): string {
|
|
9
9
|
let systemMessage = `You are Codea, an expert AI coding assistant created by Alia. You help developers write, debug, refactor, and understand code directly in their terminal.
|
|
10
10
|
|
|
11
11
|
## Core Principles
|
|
@@ -19,16 +19,18 @@ You have powerful tools to interact with the user's workspace:
|
|
|
19
19
|
|
|
20
20
|
- **read_file**: Read file contents. Use to understand existing code before making changes.
|
|
21
21
|
- **write_file**: Create new files or completely rewrite existing ones.
|
|
22
|
-
- **edit_file**: Make precise, targeted changes
|
|
22
|
+
- **edit_file**: Make precise, targeted changes using exact text match and replace.
|
|
23
|
+
- **apply_patch**: Apply unified diff patches to files. Preferred for multi-line or multi-file changes. Supports fuzzy line matching.
|
|
23
24
|
- **list_files**: Explore directory structure. Use to understand project layout.
|
|
24
|
-
- **search_files**: Find text/patterns across the codebase
|
|
25
|
+
- **search_files**: Find text/patterns across the codebase with context lines. Uses ripgrep when available.
|
|
25
26
|
- **run_command**: Execute shell commands (build, test, git, npm, etc.)
|
|
26
27
|
|
|
27
28
|
## Best Practices
|
|
28
29
|
1. **Read before writing**: Always read relevant files before modifying them.
|
|
29
30
|
2. **Minimal changes**: Make the smallest change necessary to accomplish the task.
|
|
30
31
|
3. **Preserve style**: Match existing formatting, naming conventions, and patterns.
|
|
31
|
-
4. **
|
|
32
|
+
4. **Prefer apply_patch**: For multi-line edits, use apply_patch with unified diff format.
|
|
33
|
+
5. **Explain when helpful**: For complex changes, briefly explain the approach.
|
|
32
34
|
|
|
33
35
|
## Response Style
|
|
34
36
|
- Use markdown for formatting code blocks, lists, and emphasis.
|
|
@@ -36,6 +38,10 @@ You have powerful tools to interact with the user's workspace:
|
|
|
36
38
|
- For code changes, be precise and action-oriented.
|
|
37
39
|
- If unsure about requirements, ask clarifying questions.`;
|
|
38
40
|
|
|
41
|
+
if (projectInstructions) {
|
|
42
|
+
systemMessage += `\n\n## Project Instructions (from CODEA.md)\n${projectInstructions}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
if (codebaseContext) {
|
|
40
46
|
systemMessage += `\n\n## Current Codebase Context\n${codebaseContext}`;
|
|
41
47
|
}
|
|
@@ -43,6 +49,61 @@ You have powerful tools to interact with the user's workspace:
|
|
|
43
49
|
return systemMessage;
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
export async function loadProjectInstructions(): Promise<string> {
|
|
53
|
+
const parts: string[] = [];
|
|
54
|
+
|
|
55
|
+
// 1. Global: ~/.codea/CODEA.md
|
|
56
|
+
const home = process.env.HOME || '';
|
|
57
|
+
if (home) {
|
|
58
|
+
const globalPath = path.join(home, '.codea', 'CODEA.md');
|
|
59
|
+
try {
|
|
60
|
+
const content = await fs.readFile(globalPath, 'utf-8');
|
|
61
|
+
if (content.trim()) {
|
|
62
|
+
parts.push(`# Global Instructions (~/.codea/CODEA.md)\n${content.trim()}`);
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// No global instructions
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. Project root: {git_root}/CODEA.md
|
|
70
|
+
let gitRoot = '';
|
|
71
|
+
try {
|
|
72
|
+
const { stdout } = await execAsync('git rev-parse --show-toplevel', { cwd: process.cwd() });
|
|
73
|
+
gitRoot = stdout.trim();
|
|
74
|
+
} catch {
|
|
75
|
+
// Not a git repo
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (gitRoot) {
|
|
79
|
+
const projectPath = path.join(gitRoot, 'CODEA.md');
|
|
80
|
+
try {
|
|
81
|
+
const content = await fs.readFile(projectPath, 'utf-8');
|
|
82
|
+
if (content.trim()) {
|
|
83
|
+
parts.push(`# Project Instructions (CODEA.md)\n${content.trim()}`);
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// No project instructions
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. Directory-level: {cwd}/CODEA.md (if different from git root)
|
|
91
|
+
const cwd = process.cwd();
|
|
92
|
+
if (cwd !== gitRoot) {
|
|
93
|
+
const dirPath = path.join(cwd, 'CODEA.md');
|
|
94
|
+
try {
|
|
95
|
+
const content = await fs.readFile(dirPath, 'utf-8');
|
|
96
|
+
if (content.trim()) {
|
|
97
|
+
parts.push(`# Directory Instructions (./CODEA.md)\n${content.trim()}`);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// No directory instructions
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return parts.join('\n\n---\n\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
46
107
|
export async function getCodebaseContext(): Promise<string> {
|
|
47
108
|
const cwd = process.cwd();
|
|
48
109
|
const contextParts: string[] = [];
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { streamChat } from './api.js';
|
|
2
|
+
import { executeTool } from '../tools/executor.js';
|
|
3
|
+
import { ApprovalMode, needsApproval } from './approval.js';
|
|
4
|
+
|
|
5
|
+
export interface Message {
|
|
6
|
+
role: 'user' | 'assistant' | 'system' | 'tool';
|
|
7
|
+
content: string;
|
|
8
|
+
tool_calls?: ToolCall[];
|
|
9
|
+
tool_call_id?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ToolCall {
|
|
13
|
+
id: string;
|
|
14
|
+
type: 'function';
|
|
15
|
+
function: {
|
|
16
|
+
name: string;
|
|
17
|
+
arguments: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ToolExecution {
|
|
22
|
+
id: string;
|
|
23
|
+
tool: string;
|
|
24
|
+
args: Record<string, any>;
|
|
25
|
+
result?: string;
|
|
26
|
+
success?: boolean;
|
|
27
|
+
approved?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ConversationEvent =
|
|
31
|
+
| { type: 'thinking' }
|
|
32
|
+
| { type: 'content'; text: string }
|
|
33
|
+
| { type: 'tool_start'; execution: ToolExecution }
|
|
34
|
+
| { type: 'approval_needed'; execution: ToolExecution; resolve: (approved: boolean) => void }
|
|
35
|
+
| { type: 'tool_done'; execution: ToolExecution }
|
|
36
|
+
| { type: 'done'; content: string }
|
|
37
|
+
| { type: 'error'; message: string };
|
|
38
|
+
|
|
39
|
+
export interface ConversationOptions {
|
|
40
|
+
messages: Message[];
|
|
41
|
+
systemMessage: string;
|
|
42
|
+
model: string;
|
|
43
|
+
approvalMode: ApprovalMode;
|
|
44
|
+
onEvent: (event: ConversationEvent) => void;
|
|
45
|
+
requestApproval: (execution: ToolExecution) => Promise<boolean>;
|
|
46
|
+
isActive: () => boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function processConversation(opts: ConversationOptions): Promise<void> {
|
|
50
|
+
const { messages, systemMessage, model, approvalMode, onEvent, requestApproval, isActive } = opts;
|
|
51
|
+
|
|
52
|
+
while (isActive()) {
|
|
53
|
+
let fullContent = '';
|
|
54
|
+
let toolCalls: ToolCall[] | undefined;
|
|
55
|
+
|
|
56
|
+
onEvent({ type: 'thinking' });
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await streamChat(messages, systemMessage, model, {
|
|
60
|
+
onContent: (content) => {
|
|
61
|
+
if (!isActive()) return;
|
|
62
|
+
fullContent += content;
|
|
63
|
+
onEvent({ type: 'content', text: content });
|
|
64
|
+
},
|
|
65
|
+
onToolCall: () => {},
|
|
66
|
+
onDone: (_content, tcs) => {
|
|
67
|
+
toolCalls = tcs;
|
|
68
|
+
},
|
|
69
|
+
onError: (error) => {
|
|
70
|
+
onEvent({ type: 'error', message: error.message });
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
} catch (error: any) {
|
|
74
|
+
onEvent({ type: 'error', message: error.message });
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!isActive()) break;
|
|
79
|
+
|
|
80
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
81
|
+
messages.push({
|
|
82
|
+
role: 'assistant',
|
|
83
|
+
content: fullContent,
|
|
84
|
+
tool_calls: toolCalls,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
for (const tc of toolCalls) {
|
|
88
|
+
if (!isActive()) break;
|
|
89
|
+
|
|
90
|
+
const args = JSON.parse(tc.function.arguments);
|
|
91
|
+
const execution: ToolExecution = {
|
|
92
|
+
id: tc.id,
|
|
93
|
+
tool: tc.function.name,
|
|
94
|
+
args,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
onEvent({ type: 'tool_start', execution });
|
|
98
|
+
|
|
99
|
+
// Check approval
|
|
100
|
+
if (needsApproval(tc.function.name, approvalMode)) {
|
|
101
|
+
const approved = await requestApproval(execution);
|
|
102
|
+
if (!approved) {
|
|
103
|
+
execution.approved = false;
|
|
104
|
+
execution.success = false;
|
|
105
|
+
execution.result = 'User declined this action.';
|
|
106
|
+
messages.push({
|
|
107
|
+
role: 'tool',
|
|
108
|
+
tool_call_id: tc.id,
|
|
109
|
+
content: 'User declined this action.',
|
|
110
|
+
});
|
|
111
|
+
onEvent({ type: 'tool_done', execution });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
execution.approved = true;
|
|
115
|
+
} else {
|
|
116
|
+
execution.approved = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = await executeTool(tc.function.name, args);
|
|
120
|
+
execution.result = result.result;
|
|
121
|
+
execution.success = result.success;
|
|
122
|
+
|
|
123
|
+
messages.push({
|
|
124
|
+
role: 'tool',
|
|
125
|
+
tool_call_id: tc.id,
|
|
126
|
+
content: result.result,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
onEvent({ type: 'tool_done', execution });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
continue;
|
|
133
|
+
} else {
|
|
134
|
+
if (fullContent) {
|
|
135
|
+
messages.push({ role: 'assistant', content: fullContent });
|
|
136
|
+
}
|
|
137
|
+
onEvent({ type: 'done', content: fullContent });
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|