@rxpm/forge-cli 0.0.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/.dockerignore +6 -0
- package/.prettierrc +9 -0
- package/AGENT_RULES.md +42 -0
- package/Dockerfile +32 -0
- package/README.md +117 -0
- package/REQUIREMENTS.md +233 -0
- package/assets/preview_explain.webp +0 -0
- package/dist/agent/loop.d.ts +6 -0
- package/dist/agent/loop.js +62 -0
- package/dist/agent/loop.js.map +1 -0
- package/dist/cli/commands.d.ts +3 -0
- package/dist/cli/commands.js +40 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/config/env.d.ts +9 -0
- package/dist/config/env.js +16 -0
- package/dist/config/env.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/provider.d.ts +4 -0
- package/dist/llm/provider.js +12 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/tools/code.d.ts +15 -0
- package/dist/tools/code.js +53 -0
- package/dist/tools/code.js.map +1 -0
- package/dist/tools/fs.d.ts +18 -0
- package/dist/tools/fs.js +70 -0
- package/dist/tools/fs.js.map +1 -0
- package/dist/tools/git.d.ts +15 -0
- package/dist/tools/git.js +61 -0
- package/dist/tools/git.js.map +1 -0
- package/dist/tools/index.d.ts +35 -0
- package/dist/tools/index.js +22 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/shell.d.ts +9 -0
- package/dist/tools/shell.js +31 -0
- package/dist/tools/shell.js.map +1 -0
- package/dist/ui/activity.d.ts +49 -0
- package/dist/ui/activity.js +421 -0
- package/dist/ui/activity.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +10 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger2.d.ts +16 -0
- package/dist/utils/logger2.js +55 -0
- package/dist/utils/logger2.js.map +1 -0
- package/dist/utils/test-tools.d.ts +1 -0
- package/dist/utils/test-tools.js +31 -0
- package/dist/utils/test-tools.js.map +1 -0
- package/package.json +43 -0
- package/src/agent/loop.ts +68 -0
- package/src/cli/commands.ts +44 -0
- package/src/config/env.ts +17 -0
- package/src/index.ts +22 -0
- package/src/llm/provider.ts +13 -0
- package/src/tools/code.ts +53 -0
- package/src/tools/fs.ts +71 -0
- package/src/tools/git.ts +60 -0
- package/src/tools/index.ts +23 -0
- package/src/tools/shell.ts +32 -0
- package/src/ui/activity.ts +504 -0
- package/src/utils/logger.ts +10 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: activity.ts
|
|
3
|
+
* Author: Rajat Sharma
|
|
4
|
+
* Description: Gemini-inspired terminal activity display for the AI agent.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { relative } from 'node:path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import ora, { type Ora } from 'ora';
|
|
12
|
+
import { defaultModel } from '../llm/provider.js';
|
|
13
|
+
|
|
14
|
+
const ANSI_PATTERN = /\x1B\[[0-9;]*m/g;
|
|
15
|
+
|
|
16
|
+
const box = {
|
|
17
|
+
topLeft: '╭',
|
|
18
|
+
topRight: '╮',
|
|
19
|
+
bottomLeft: '╰',
|
|
20
|
+
bottomRight: '╯',
|
|
21
|
+
horizontal: '─',
|
|
22
|
+
vertical: '│',
|
|
23
|
+
teeLeft: '┤',
|
|
24
|
+
teeRight: '├',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const theme = {
|
|
28
|
+
accent: chalk.yellowBright,
|
|
29
|
+
accentStrong: chalk.yellowBright.bold,
|
|
30
|
+
success: chalk.greenBright,
|
|
31
|
+
error: chalk.red,
|
|
32
|
+
warning: chalk.yellow,
|
|
33
|
+
info: chalk.blue,
|
|
34
|
+
text: chalk.white,
|
|
35
|
+
soft: chalk.white,
|
|
36
|
+
muted: chalk.dim,
|
|
37
|
+
border: chalk.gray,
|
|
38
|
+
panel: chalk.gray,
|
|
39
|
+
strong: chalk.bold,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface ToolCallEntry {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
displayName: string;
|
|
46
|
+
args: Record<string, any>;
|
|
47
|
+
startTime: number;
|
|
48
|
+
endTime?: number;
|
|
49
|
+
result?: string;
|
|
50
|
+
status: 'running' | 'done' | 'error';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface StepToolCall {
|
|
54
|
+
toolName: string;
|
|
55
|
+
toolCallId: string;
|
|
56
|
+
args?: Record<string, any>;
|
|
57
|
+
title?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface StepToolResult {
|
|
61
|
+
toolName: string;
|
|
62
|
+
toolCallId: string;
|
|
63
|
+
output?: any;
|
|
64
|
+
result?: any;
|
|
65
|
+
title?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function terminalWidth(): number {
|
|
69
|
+
const width = process.stdout.columns || 100;
|
|
70
|
+
return Math.max(76, Math.min(width, 118));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function contentWidth(totalWidth = terminalWidth()): number {
|
|
74
|
+
return totalWidth - 4;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function visibleLength(value: string): number {
|
|
78
|
+
return value.replace(ANSI_PATTERN, '').length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function padLine(value: string, width: number): string {
|
|
82
|
+
const gap = Math.max(0, width - visibleLength(value));
|
|
83
|
+
return value + ' '.repeat(gap);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function truncate(value: string, maxLen: number): string {
|
|
87
|
+
if (value.length <= maxLen) return value;
|
|
88
|
+
if (maxLen <= 1) return value.slice(0, maxLen);
|
|
89
|
+
return value.slice(0, maxLen - 1) + '…';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function wrapText(value: string, width: number): string[] {
|
|
93
|
+
if (!value) return [''];
|
|
94
|
+
|
|
95
|
+
const chunks = value.replace(/\t/g, ' ').split('\n');
|
|
96
|
+
const lines: string[] = [];
|
|
97
|
+
|
|
98
|
+
for (const chunk of chunks) {
|
|
99
|
+
if (!chunk.trim()) {
|
|
100
|
+
lines.push('');
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let current = '';
|
|
105
|
+
const words = chunk.split(/\s+/);
|
|
106
|
+
|
|
107
|
+
for (const word of words) {
|
|
108
|
+
if (!current) {
|
|
109
|
+
current = word;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const next = `${current} ${word}`;
|
|
114
|
+
if (next.length <= width) {
|
|
115
|
+
current = next;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
lines.push(current);
|
|
120
|
+
|
|
121
|
+
if (word.length <= width) {
|
|
122
|
+
current = word;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let remaining = word;
|
|
127
|
+
while (remaining.length > width) {
|
|
128
|
+
lines.push(remaining.slice(0, width - 1) + '…');
|
|
129
|
+
remaining = remaining.slice(width - 1);
|
|
130
|
+
}
|
|
131
|
+
current = remaining;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (current) lines.push(current);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return lines.length > 0 ? lines : [''];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatDuration(ms: number): string {
|
|
141
|
+
if (ms < 1000) return `${ms}ms`;
|
|
142
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function safeStringify(value: unknown): string {
|
|
146
|
+
if (typeof value === 'string') return value;
|
|
147
|
+
try {
|
|
148
|
+
return JSON.stringify(value);
|
|
149
|
+
} catch {
|
|
150
|
+
return String(value);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatResultPreview(result: string): string {
|
|
155
|
+
if (!result) return 'No output';
|
|
156
|
+
|
|
157
|
+
const trimmed = result.trim();
|
|
158
|
+
if (!trimmed) return 'No output';
|
|
159
|
+
|
|
160
|
+
const lines = trimmed.split('\n');
|
|
161
|
+
const firstLine = truncate(lines[0], 72);
|
|
162
|
+
return lines.length > 1 ? `${firstLine} (+${lines.length - 1} lines)` : firstLine;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getToolResultValue(result: StepToolResult | undefined): unknown {
|
|
166
|
+
if (!result) return undefined;
|
|
167
|
+
if ('output' in result && result.output !== undefined) return result.output;
|
|
168
|
+
return result.result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatToolBody(call: StepToolCall, result: StepToolResult | undefined): string[] {
|
|
172
|
+
const rawValue = getToolResultValue(result);
|
|
173
|
+
if (rawValue === undefined) return [theme.warning('No output captured')];
|
|
174
|
+
|
|
175
|
+
const text = safeStringify(rawValue).trim();
|
|
176
|
+
if (!text) return [theme.muted('No output')];
|
|
177
|
+
|
|
178
|
+
const lines = text.split('\n');
|
|
179
|
+
const isError = text.toLowerCase().startsWith('error');
|
|
180
|
+
const colorize = isError ? theme.error : theme.muted;
|
|
181
|
+
|
|
182
|
+
if (call.toolName === 'run_command' || call.toolName === 'RunCommand') {
|
|
183
|
+
const preview = lines.slice(0, 6).map((line) => colorize(line));
|
|
184
|
+
if (lines.length > 6) {
|
|
185
|
+
preview.push(theme.muted(`... (+${lines.length - 6} more lines)`));
|
|
186
|
+
}
|
|
187
|
+
return preview;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return [colorize(formatResultPreview(text))];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getWorkspaceLabel(): string {
|
|
194
|
+
const cwd = process.cwd();
|
|
195
|
+
const home = homedir();
|
|
196
|
+
if (cwd.startsWith(home)) {
|
|
197
|
+
const rel = relative(home, cwd);
|
|
198
|
+
return rel ? `~/${rel}` : '~';
|
|
199
|
+
}
|
|
200
|
+
return cwd;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getBranchName(): string {
|
|
204
|
+
try {
|
|
205
|
+
return (
|
|
206
|
+
execSync('git branch --show-current', {
|
|
207
|
+
cwd: process.cwd(),
|
|
208
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
209
|
+
encoding: 'utf8',
|
|
210
|
+
}).trim() || 'detached'
|
|
211
|
+
);
|
|
212
|
+
} catch {
|
|
213
|
+
return 'unknown';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function summarizeTool(toolName: string, args: Record<string, any>): string {
|
|
218
|
+
switch (toolName) {
|
|
219
|
+
case 'ReadFile':
|
|
220
|
+
case 'read_file':
|
|
221
|
+
return args.path ? truncate(args.path, 54) : 'Inspect file';
|
|
222
|
+
case 'WriteFile':
|
|
223
|
+
case 'write_file':
|
|
224
|
+
return args.path ? truncate(args.path, 54) : 'Write file';
|
|
225
|
+
case 'ListFiles':
|
|
226
|
+
case 'list_files':
|
|
227
|
+
return args.path ? truncate(args.path, 54) : '.';
|
|
228
|
+
case 'SearchCode':
|
|
229
|
+
case 'search_code':
|
|
230
|
+
return args.query ? `"${truncate(args.query, 42)}"` : 'Search codebase';
|
|
231
|
+
case 'OpenFileLines':
|
|
232
|
+
case 'open_file_lines':
|
|
233
|
+
return args.path ? `${truncate(args.path, 36)}:${args.start}-${args.end}` : 'Open file range';
|
|
234
|
+
case 'RunCommand':
|
|
235
|
+
case 'run_command':
|
|
236
|
+
return args.command ? truncate(args.command, 54) : 'Run command';
|
|
237
|
+
case 'GitDiff':
|
|
238
|
+
case 'git_diff':
|
|
239
|
+
return args.staged ? 'staged changes' : 'working tree';
|
|
240
|
+
case 'GitCommit':
|
|
241
|
+
case 'git_commit':
|
|
242
|
+
return args.message ? `"${truncate(args.message, 42)}"` : 'Create commit';
|
|
243
|
+
default:
|
|
244
|
+
return Object.keys(args).length > 0 ? truncate(safeStringify(args), 54) : 'Working';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatToolLabel(toolName: string): string {
|
|
249
|
+
switch (toolName) {
|
|
250
|
+
case 'ReadFile':
|
|
251
|
+
case 'read_file':
|
|
252
|
+
return 'Read';
|
|
253
|
+
case 'WriteFile':
|
|
254
|
+
case 'write_file':
|
|
255
|
+
return 'Edit';
|
|
256
|
+
case 'ListFiles':
|
|
257
|
+
case 'list_files':
|
|
258
|
+
return 'List';
|
|
259
|
+
case 'SearchCode':
|
|
260
|
+
case 'search_code':
|
|
261
|
+
return 'Search';
|
|
262
|
+
case 'OpenFileLines':
|
|
263
|
+
case 'open_file_lines':
|
|
264
|
+
return 'Inspect';
|
|
265
|
+
case 'RunCommand':
|
|
266
|
+
case 'run_command':
|
|
267
|
+
return 'Shell';
|
|
268
|
+
case 'GitStatus':
|
|
269
|
+
case 'git_status':
|
|
270
|
+
return 'Git status';
|
|
271
|
+
case 'GitDiff':
|
|
272
|
+
case 'git_diff':
|
|
273
|
+
return 'Git diff';
|
|
274
|
+
case 'GitCommit':
|
|
275
|
+
case 'git_commit':
|
|
276
|
+
return 'Commit';
|
|
277
|
+
default:
|
|
278
|
+
return toolName.replace(/_/g, ' ');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buildTopBorder(title: string, width = terminalWidth()): string {
|
|
283
|
+
const plain = ` ${title} `;
|
|
284
|
+
const fill = Math.max(0, width - visibleLength(plain) - 1);
|
|
285
|
+
return theme.border(`${box.topLeft}${box.horizontal}${title}${box.horizontal.repeat(fill)}${box.topRight}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function buildBottomBorder(width = terminalWidth()): string {
|
|
289
|
+
return theme.border(`${box.bottomLeft}${box.horizontal.repeat(width - 2)}${box.bottomRight}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function renderPanel(title: string, rows: string[], width = terminalWidth()): void {
|
|
293
|
+
const innerWidth = contentWidth(width);
|
|
294
|
+
console.log(buildTopBorder(title, width));
|
|
295
|
+
|
|
296
|
+
for (const row of rows) {
|
|
297
|
+
const wrapped = wrapText(row, innerWidth);
|
|
298
|
+
for (const line of wrapped) {
|
|
299
|
+
console.log(`${theme.border(box.vertical)} ${padLine(line, innerWidth)} ${theme.border(box.vertical)}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log(buildBottomBorder(width));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function createMetaRow(label: string, value: string, width: number): string[] {
|
|
307
|
+
const left = theme.muted(`${label.padEnd(10)} `);
|
|
308
|
+
const wrapped = wrapText(value, width - 11);
|
|
309
|
+
return wrapped.map((line, index) => {
|
|
310
|
+
if (index === 0) return `${left}${theme.text(line)}`;
|
|
311
|
+
return `${' '.repeat(11)}${theme.text(line)}`;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function renderStatusLine(label: string, status: 'success' | 'error' | 'info', meta?: string): void {
|
|
316
|
+
const color = status === 'success' ? theme.success : status === 'error' ? theme.error : theme.info;
|
|
317
|
+
const summary = meta ? `${label} ${theme.muted(meta)}` : label;
|
|
318
|
+
console.log(`${color('●')} ${theme.text(summary)}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export class ActivityDisplay {
|
|
322
|
+
private spinner: Ora | null = null;
|
|
323
|
+
private stepCount = 0;
|
|
324
|
+
private toolCalls: ToolCallEntry[] = [];
|
|
325
|
+
private startTime = Date.now();
|
|
326
|
+
private readonly branch = getBranchName();
|
|
327
|
+
|
|
328
|
+
startThinking(): void {
|
|
329
|
+
if (!process.stdout.isTTY || !process.stderr.isTTY) return;
|
|
330
|
+
if (this.spinner) return;
|
|
331
|
+
|
|
332
|
+
this.spinner = ora({
|
|
333
|
+
text: theme.soft('Forge is working...'),
|
|
334
|
+
spinner: 'dots',
|
|
335
|
+
color: 'yellow',
|
|
336
|
+
stream: process.stdout,
|
|
337
|
+
}).start();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
stopThinking(): void {
|
|
341
|
+
if (!this.spinner) return;
|
|
342
|
+
this.spinner.clear();
|
|
343
|
+
this.spinner.stop();
|
|
344
|
+
this.spinner = null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
onStepStart(): void {
|
|
348
|
+
this.stopThinking();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
onToolCall(call: { toolName: string; toolCallId: string; args: Record<string, any> }): void {
|
|
352
|
+
this.stopThinking();
|
|
353
|
+
|
|
354
|
+
const entry: ToolCallEntry = {
|
|
355
|
+
id: call.toolCallId,
|
|
356
|
+
name: call.toolName,
|
|
357
|
+
displayName: formatToolLabel(call.toolName),
|
|
358
|
+
args: call.args,
|
|
359
|
+
startTime: Date.now(),
|
|
360
|
+
status: 'running',
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
this.toolCalls.push(entry);
|
|
364
|
+
|
|
365
|
+
const label = `${entry.displayName} ${summarizeTool(call.toolName, call.args)}`;
|
|
366
|
+
renderStatusLine(label, 'info');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
onToolResult(result: { toolName: string; toolCallId: string; output?: any; result?: any }): void {
|
|
370
|
+
const entry = this.toolCalls.find((item) => item.id === result.toolCallId);
|
|
371
|
+
if (entry) {
|
|
372
|
+
entry.endTime = Date.now();
|
|
373
|
+
entry.result = safeStringify(getToolResultValue(result));
|
|
374
|
+
entry.status = entry.result.toLowerCase().startsWith('error') ? 'error' : 'done';
|
|
375
|
+
const duration = formatDuration(entry.endTime - entry.startTime);
|
|
376
|
+
const preview = formatResultPreview(entry.result);
|
|
377
|
+
renderStatusLine(preview, entry.status === 'error' ? 'error' : 'success', `· ${duration}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.startThinking();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
onText(text: string): void {
|
|
384
|
+
this.stopThinking();
|
|
385
|
+
if (!text.trim()) return;
|
|
386
|
+
|
|
387
|
+
const width = terminalWidth();
|
|
388
|
+
const rows: string[] = [];
|
|
389
|
+
|
|
390
|
+
for (const block of text.trim().split('\n')) {
|
|
391
|
+
const prefix = block.trim() ? `${theme.accent('✦')} ${theme.text(block.trim())}` : '';
|
|
392
|
+
rows.push(prefix);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
renderPanel(theme.panel(' Response '), rows, width);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
onStepFinish(step: { toolCalls: StepToolCall[]; toolResults: StepToolResult[]; text?: string }): void {
|
|
399
|
+
this.stopThinking();
|
|
400
|
+
this.stepCount++;
|
|
401
|
+
|
|
402
|
+
const width = terminalWidth();
|
|
403
|
+
const rows: string[] = [];
|
|
404
|
+
const resultsById = new Map(step.toolResults.map((result) => [result.toolCallId, result]));
|
|
405
|
+
|
|
406
|
+
for (const call of step.toolCalls) {
|
|
407
|
+
const args = call.args || {};
|
|
408
|
+
const label = call.title || formatToolLabel(call.toolName);
|
|
409
|
+
const summary = summarizeTool(call.toolName, args);
|
|
410
|
+
const result = resultsById.get(call.toolCallId);
|
|
411
|
+
const resultValue = getToolResultValue(result);
|
|
412
|
+
const resultText = formatResultPreview(safeStringify(resultValue ?? ''));
|
|
413
|
+
const isError = safeStringify(resultValue ?? '')
|
|
414
|
+
.toLowerCase()
|
|
415
|
+
.startsWith('error');
|
|
416
|
+
|
|
417
|
+
const existing = this.toolCalls.find((entry) => entry.id === call.toolCallId);
|
|
418
|
+
if (!existing) {
|
|
419
|
+
this.toolCalls.push({
|
|
420
|
+
id: call.toolCallId,
|
|
421
|
+
name: call.toolName,
|
|
422
|
+
displayName: label,
|
|
423
|
+
args,
|
|
424
|
+
startTime: Date.now(),
|
|
425
|
+
endTime: Date.now(),
|
|
426
|
+
result: safeStringify(resultValue ?? ''),
|
|
427
|
+
status: isError ? 'error' : 'done',
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const icon = isError ? theme.error('✕') : theme.success('✓');
|
|
432
|
+
rows.push(`${icon} ${theme.strong(label)} ${theme.soft(summary)}`);
|
|
433
|
+
for (const bodyLine of formatToolBody(call, result)) {
|
|
434
|
+
rows.push(` ${bodyLine}`);
|
|
435
|
+
}
|
|
436
|
+
rows.push('');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (rows.length > 0 && rows[rows.length - 1] === '') {
|
|
440
|
+
rows.pop();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (rows.length > 0) {
|
|
444
|
+
renderPanel(`${theme.text(` Step ${this.stepCount} `)}${theme.muted(`· ${step.toolCalls.length} tool${step.toolCalls.length === 1 ? '' : 's'}`)} `, rows, width);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (step.text?.trim()) {
|
|
448
|
+
const responseRows: string[] = [];
|
|
449
|
+
for (const line of step.text.trim().split('\n')) {
|
|
450
|
+
if (!line.trim()) {
|
|
451
|
+
responseRows.push('');
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
responseRows.push(`${theme.accent('✦')} ${theme.text(line.trim())}`);
|
|
455
|
+
}
|
|
456
|
+
renderPanel(theme.panel(' Response '), responseRows, width);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
this.startThinking();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
printSummary(status: 'success' | 'error', message?: string): void {
|
|
463
|
+
this.stopThinking();
|
|
464
|
+
|
|
465
|
+
const elapsed = formatDuration(Date.now() - this.startTime);
|
|
466
|
+
const title =
|
|
467
|
+
status === 'success'
|
|
468
|
+
? `${theme.success(' Task complete ')}${theme.muted(`· ${this.stepCount} steps · ${elapsed}`)}`
|
|
469
|
+
: `${theme.error(' Task failed ')}${theme.muted(`· ${this.stepCount} steps · ${elapsed}`)}`;
|
|
470
|
+
|
|
471
|
+
const rows = status === 'error' && message ? [theme.error(message)] : [theme.soft('Session finished. You can run another task with `forge "<task>"`.')];
|
|
472
|
+
|
|
473
|
+
console.log('');
|
|
474
|
+
renderPanel(title, rows);
|
|
475
|
+
this.renderFooter();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private renderFooter(): void {
|
|
479
|
+
const width = terminalWidth();
|
|
480
|
+
const inner = contentWidth(width);
|
|
481
|
+
const rows = [
|
|
482
|
+
...createMetaRow('workspace', getWorkspaceLabel(), inner),
|
|
483
|
+
...createMetaRow('branch', this.branch, inner),
|
|
484
|
+
...createMetaRow('provider', 'Ollama', inner),
|
|
485
|
+
...createMetaRow('model', defaultModel, inner),
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
renderPanel(theme.panel(' Session '), rows, width);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
static printBanner(task: string): void {
|
|
492
|
+
const width = terminalWidth();
|
|
493
|
+
const inner = contentWidth(width);
|
|
494
|
+
const rows = [
|
|
495
|
+
...createMetaRow('task', task, inner),
|
|
496
|
+
...createMetaRow('workspace', getWorkspaceLabel(), inner),
|
|
497
|
+
...createMetaRow('branch', getBranchName(), inner),
|
|
498
|
+
...createMetaRow('model', `${defaultModel} via Ollama`, inner),
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
console.log('');
|
|
502
|
+
renderPanel(`${theme.accentStrong(' Forge ')}${theme.muted('· terminal coding agent')}`, rows, width);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
info: (msg: string) => console.log(chalk.blue('ℹ'), msg),
|
|
5
|
+
success: (msg: string) => console.log(chalk.green('✔'), msg),
|
|
6
|
+
error: (msg: string) => console.log(chalk.red('✖'), msg),
|
|
7
|
+
warn: (msg: string) => console.log(chalk.yellow('⚠'), msg),
|
|
8
|
+
tool: (name: string, params: any) => console.log(chalk.cyan('⚒'), chalk.bold(name), chalk.gray(JSON.stringify(params))),
|
|
9
|
+
step: (msg: string) => console.log(chalk.magenta('➜'), msg),
|
|
10
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
"strict": false,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"declaration": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|