agent-termo 0.0.0-stage → 1.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/.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 +163 -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 +210 -0
- package/dist/cli.js.map +7 -0
- package/dist/main.js +173 -0
- package/dist/main.js.map +7 -0
- package/package.json +90 -6
- package/src/agents/index.ts +94 -0
- package/src/cli.ts +155 -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 +118 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Agent-Termo — Main REPL entry point
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import { NimClient } from './llm/client.js';
|
|
6
|
+
import { TOOL_DEFINITIONS } from './llm/tools.js';
|
|
7
|
+
import { executeTool, applyFileChange } from './tools/index.js';
|
|
8
|
+
import { loadMemory, logToTimeline, logAgentTermoTimeline } from './memory/index.js';
|
|
9
|
+
import { loadAllAgents } from './agents/index.js';
|
|
10
|
+
import { buildSystemPrompt } from './agents/index.js';
|
|
11
|
+
import { resolveProjectRoot } from './utils/config.js';
|
|
12
|
+
import {
|
|
13
|
+
printWelcome,
|
|
14
|
+
printUserMessage,
|
|
15
|
+
printAssistantMessage,
|
|
16
|
+
printToolCall,
|
|
17
|
+
printToolResult,
|
|
18
|
+
printError,
|
|
19
|
+
printSuccess,
|
|
20
|
+
printInfo,
|
|
21
|
+
printHelp,
|
|
22
|
+
printAgentsList,
|
|
23
|
+
printMemoryFile,
|
|
24
|
+
printTimelineFile,
|
|
25
|
+
startSpinner,
|
|
26
|
+
stopSpinner,
|
|
27
|
+
clearScreen,
|
|
28
|
+
createInput,
|
|
29
|
+
moveToFreshLine,
|
|
30
|
+
promptAcceptReject,
|
|
31
|
+
} from './ui/index.js';
|
|
32
|
+
import type { ConversationMessage, ToolCall, ProposedFileChange } from './types/index.js';
|
|
33
|
+
|
|
34
|
+
const TOOL_KEYWORDS = [
|
|
35
|
+
'build', 'create', 'write', 'edit', 'modify', 'file', 'code', 'function', 'class',
|
|
36
|
+
'component', 'debug', 'fix', 'git', 'commit', 'push', 'install', 'npm', 'run',
|
|
37
|
+
'bash', 'shell', 'command', 'read', 'grep', 'check', 'show', 'implement',
|
|
38
|
+
'configure', 'setup', 'src', 'package', 'app', 'project', 'api', 'deploy',
|
|
39
|
+
'test', 'compile', 'typescript', 'javascript', 'python', 'node', 'add', 'delete',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function mightNeedTools(message: string): boolean {
|
|
43
|
+
const lower = message.toLowerCase();
|
|
44
|
+
return TOOL_KEYWORDS.some((w) => lower.includes(w));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function dedup(arr: ToolCall[]): ToolCall[] {
|
|
48
|
+
const seen = new Set<string>();
|
|
49
|
+
return arr.filter((tc) => {
|
|
50
|
+
if (seen.has(tc.id)) return false;
|
|
51
|
+
seen.add(tc.id);
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractProposedChange(toolName: string, resultOutput: string): ProposedFileChange | undefined {
|
|
57
|
+
const isFileTool = toolName === 'file:write' || toolName === 'file:edit';
|
|
58
|
+
if (!isFileTool) return undefined;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(resultOutput);
|
|
61
|
+
if (parsed?.needsApproval && parsed?.proposedChange) {
|
|
62
|
+
return parsed.proposedChange as ProposedFileChange;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Not JSON — tool returned a simple string
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface InputController {
|
|
71
|
+
close(): void;
|
|
72
|
+
showPrompt(): void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Open a VS Code diff window for the proposed change.
|
|
77
|
+
* Writes original + proposed content to temp files and launches `code --diff`.
|
|
78
|
+
*/
|
|
79
|
+
async function openVsCodeDiff(change: ProposedFileChange): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
const { exec } = await import('node:child_process');
|
|
82
|
+
const { promisify } = await import('node:util');
|
|
83
|
+
const { mkdtemp, writeFile } = await import('node:fs/promises');
|
|
84
|
+
const { basename } = await import('node:path');
|
|
85
|
+
const { tmpdir } = await import('node:os');
|
|
86
|
+
|
|
87
|
+
const execAsync = promisify(exec);
|
|
88
|
+
|
|
89
|
+
const originalPath = change.path;
|
|
90
|
+
const tmpDir = await mkdtemp(tmpdir() + '/agent-termo-diff-');
|
|
91
|
+
const tmpFile = tmpDir + '/' + basename(originalPath);
|
|
92
|
+
|
|
93
|
+
await writeFile(tmpFile, change.proposedContent, 'utf-8');
|
|
94
|
+
|
|
95
|
+
// Write original content to a temp file for diff display
|
|
96
|
+
if (!change.isNewFile && change.originalContent.length > 0) {
|
|
97
|
+
const originalTmp = tmpDir + '/original-' + basename(originalPath);
|
|
98
|
+
await writeFile(originalTmp, change.originalContent, 'utf-8');
|
|
99
|
+
execAsync(`code --diff "${originalTmp}" "${tmpFile}" --wait`).catch(() => {
|
|
100
|
+
// VS Code not available or user closed — continue silently
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
// New file: just open the proposed file
|
|
104
|
+
execAsync(`code "${tmpFile}" --wait`).catch(() => {
|
|
105
|
+
// Silently fall back
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Silently fall back to terminal diff view
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function executeToolWithReview(
|
|
114
|
+
toolName: string,
|
|
115
|
+
args: Record<string, unknown>,
|
|
116
|
+
projectRoot: string,
|
|
117
|
+
input: InputController,
|
|
118
|
+
): Promise<{ toolResult: { success: boolean; output?: string; error?: string }; change?: ProposedFileChange; applied: boolean }> {
|
|
119
|
+
console.log('');
|
|
120
|
+
printToolCall(toolName, args);
|
|
121
|
+
startSpinner('Running ' + toolName);
|
|
122
|
+
const toolResult = await executeTool(toolName, args, projectRoot);
|
|
123
|
+
stopSpinner(toolResult.success);
|
|
124
|
+
|
|
125
|
+
const proposedChange = extractProposedChange(toolName, toolResult.output ?? '');
|
|
126
|
+
|
|
127
|
+
if (proposedChange) {
|
|
128
|
+
// Pause REPL input to prevent stale listeners from interfering with promptAcceptReject
|
|
129
|
+
input.close();
|
|
130
|
+
|
|
131
|
+
// Show terminal diff viewer
|
|
132
|
+
printToolResult(toolName, { success: toolResult.success, output: toolResult.output, error: toolResult.error }, { needsApproval: true, proposedChange });
|
|
133
|
+
|
|
134
|
+
// Also open VS Code diff window (non-blocking, fire-and-forget)
|
|
135
|
+
openVsCodeDiff(proposedChange);
|
|
136
|
+
|
|
137
|
+
const decision = await promptAcceptReject('Accept this change?');
|
|
138
|
+
|
|
139
|
+
// Restore input for next REPL iteration
|
|
140
|
+
input.showPrompt();
|
|
141
|
+
|
|
142
|
+
if (decision === 'accept') {
|
|
143
|
+
startSpinner('Writing to disk');
|
|
144
|
+
const applyResult = await applyFileChange(proposedChange, projectRoot);
|
|
145
|
+
stopSpinner(applyResult.success);
|
|
146
|
+
if (applyResult.success) {
|
|
147
|
+
printSuccess(applyResult.output ?? 'Change applied');
|
|
148
|
+
return { toolResult: applyResult, change: proposedChange, applied: true };
|
|
149
|
+
} else {
|
|
150
|
+
printError(applyResult.error ?? 'Failed to apply change');
|
|
151
|
+
return { toolResult: applyResult, change: proposedChange, applied: false };
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
printInfo('Change rejected — file was not modified');
|
|
155
|
+
return { toolResult, change: proposedChange, applied: false };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
printToolResult(toolName, toolResult);
|
|
160
|
+
return { toolResult, applied: false };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Run the tool-call loop: execute tools, feed results back to the LLM,
|
|
165
|
+
* and repeat until the LLM stops issuing tool calls.
|
|
166
|
+
*/
|
|
167
|
+
async function runToolLoop(
|
|
168
|
+
conversation: ConversationMessage[],
|
|
169
|
+
projectRoot: string,
|
|
170
|
+
initialToolCalls: ToolCall[],
|
|
171
|
+
input: InputController,
|
|
172
|
+
nimClient: NimClient,
|
|
173
|
+
): Promise<string> {
|
|
174
|
+
let toolCalls = initialToolCalls;
|
|
175
|
+
let finalText = '';
|
|
176
|
+
|
|
177
|
+
// Continue looping while the LLM keeps issuing tool calls
|
|
178
|
+
while (toolCalls.length > 0) {
|
|
179
|
+
// Execute all tool calls from this turn
|
|
180
|
+
for (const tc of toolCalls) {
|
|
181
|
+
const execution = await executeToolWithReview(tc.name, tc.arguments, projectRoot, input);
|
|
182
|
+
|
|
183
|
+
const toolResult = execution.toolResult;
|
|
184
|
+
const statusText = toolResult.success ? 'OK' : ('ERR: ' + (toolResult.error ? toolResult.error.slice(0, 80) : 'unknown error'));
|
|
185
|
+
const changeNote = execution.change ? (execution.applied ? ' (applied)' : ' (rejected)') : '';
|
|
186
|
+
|
|
187
|
+
conversation.push({
|
|
188
|
+
id: crypto.randomUUID(),
|
|
189
|
+
role: 'tool',
|
|
190
|
+
content: (toolResult.success ? 'Result: ' : 'Error: ') + (toolResult.success ? (toolResult.output ?? '') : toolResult.error) + changeNote,
|
|
191
|
+
timestamp: new Date(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
logToTimeline(projectRoot, 'Tool: ' + tc.name + ' -- ' + statusText + changeNote);
|
|
195
|
+
logAgentTermoTimeline('Tool: ' + tc.name + ' -- ' + statusText + changeNote);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Send conversation (including tool results) back to the LLM
|
|
199
|
+
// The LLM will either produce text or more tool calls
|
|
200
|
+
moveToFreshLine();
|
|
201
|
+
startSpinner('Working...');
|
|
202
|
+
try {
|
|
203
|
+
const tools = nimClient.buildToolDefs(TOOL_DEFINITIONS);
|
|
204
|
+
const result = await nimClient.complete(conversation, tools);
|
|
205
|
+
finalText = result.content;
|
|
206
|
+
toolCalls = dedup(result.toolCalls);
|
|
207
|
+
process.stdout.write('\n');
|
|
208
|
+
stopSpinner(true);
|
|
209
|
+
|
|
210
|
+
// If the LLM produced text alongside tool calls, save it
|
|
211
|
+
if (finalText && finalText.trim() && finalText.trim() !== 'null') {
|
|
212
|
+
conversation.push({
|
|
213
|
+
id: crypto.randomUUID(),
|
|
214
|
+
role: 'assistant',
|
|
215
|
+
content: finalText.trim(),
|
|
216
|
+
timestamp: new Date(),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
process.stdout.write('\n');
|
|
221
|
+
stopSpinner(false);
|
|
222
|
+
printError('LLM error: ' + (err instanceof Error ? err.message : String(err)));
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return finalText;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function runRepl(projectRoot?: string): Promise<void> {
|
|
231
|
+
projectRoot = resolveProjectRoot(projectRoot);
|
|
232
|
+
|
|
233
|
+
const memory = await loadMemory(projectRoot);
|
|
234
|
+
const nimClient = new NimClient(projectRoot);
|
|
235
|
+
const systemPrompt = buildSystemPrompt(memory, '', projectRoot);
|
|
236
|
+
|
|
237
|
+
const session = { id: crypto.randomUUID(), projectRoot, startedAt: new Date() };
|
|
238
|
+
|
|
239
|
+
const conversation: ConversationMessage[] = [
|
|
240
|
+
{
|
|
241
|
+
id: crypto.randomUUID(),
|
|
242
|
+
role: 'system',
|
|
243
|
+
content: systemPrompt,
|
|
244
|
+
timestamp: new Date(),
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
printWelcome(projectRoot);
|
|
249
|
+
console.log('Model: ' + nimClient.getModel());
|
|
250
|
+
logAgentTermoTimeline('Agent-Termo session started. Model: ' + nimClient.getModel());
|
|
251
|
+
logToTimeline(projectRoot, 'Agent-Termo session started. Model: ' + nimClient.getModel());
|
|
252
|
+
logAgentTermoTimeline('Commands: /help /clear /memory /timeline /model /agents /history');
|
|
253
|
+
logToTimeline(projectRoot, 'Commands: /help /clear /memory /timeline /model /agents /history');
|
|
254
|
+
|
|
255
|
+
const input = createInput();
|
|
256
|
+
|
|
257
|
+
const cleanup = () => {
|
|
258
|
+
input.close();
|
|
259
|
+
process.stdout.write('\n');
|
|
260
|
+
console.log(
|
|
261
|
+
'Session complete. ' +
|
|
262
|
+
Math.round((Date.now() - session.startedAt.getTime()) / 60000) +
|
|
263
|
+
' minutes.',
|
|
264
|
+
);
|
|
265
|
+
process.exit(0);
|
|
266
|
+
};
|
|
267
|
+
process.on('SIGINT', cleanup);
|
|
268
|
+
|
|
269
|
+
mainLoop: while (true) {
|
|
270
|
+
try {
|
|
271
|
+
const userInput = await input.question('');
|
|
272
|
+
if (!userInput) continue;
|
|
273
|
+
if (userInput === 'exit' || userInput === 'quit') {
|
|
274
|
+
cleanup();
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (userInput.startsWith('/')) {
|
|
279
|
+
const cmd = userInput.toLowerCase().trim();
|
|
280
|
+
|
|
281
|
+
if (cmd === '/help' || cmd === '/') {
|
|
282
|
+
printHelp();
|
|
283
|
+
continue mainLoop;
|
|
284
|
+
}
|
|
285
|
+
if (cmd === '/clear' || cmd === '/cls') {
|
|
286
|
+
conversation.length = 1;
|
|
287
|
+
clearScreen();
|
|
288
|
+
printWelcome(projectRoot);
|
|
289
|
+
printSuccess('Conversation cleared.');
|
|
290
|
+
continue mainLoop;
|
|
291
|
+
}
|
|
292
|
+
if (cmd === '/memory') {
|
|
293
|
+
await printMemoryFile();
|
|
294
|
+
continue mainLoop;
|
|
295
|
+
}
|
|
296
|
+
if (cmd === '/timeline' || cmd === '/tl') {
|
|
297
|
+
await printTimelineFile();
|
|
298
|
+
continue mainLoop;
|
|
299
|
+
}
|
|
300
|
+
if (cmd === '/model') {
|
|
301
|
+
printInfo('Current model: ' + nimClient.getModel());
|
|
302
|
+
continue mainLoop;
|
|
303
|
+
}
|
|
304
|
+
if (cmd === '/agents') {
|
|
305
|
+
const agents = loadAllAgents(process.env.AGENT_TERMO_DIR || '.claude');
|
|
306
|
+
printAgentsList(agents);
|
|
307
|
+
continue mainLoop;
|
|
308
|
+
}
|
|
309
|
+
if (cmd === '/history' || cmd === '/hist') {
|
|
310
|
+
printInfo('Conversation (excluding system): ' + (conversation.length - 1) + ' messages');
|
|
311
|
+
continue mainLoop;
|
|
312
|
+
}
|
|
313
|
+
if (cmd === '/exit' || cmd === '/quit') {
|
|
314
|
+
cleanup();
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log('');
|
|
319
|
+
printHelp();
|
|
320
|
+
console.log('Unknown command: ' + cmd);
|
|
321
|
+
console.log('Type /help for available commands.\n');
|
|
322
|
+
continue mainLoop;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const userMsg: ConversationMessage = {
|
|
326
|
+
id: crypto.randomUUID(),
|
|
327
|
+
role: 'user',
|
|
328
|
+
content: userInput,
|
|
329
|
+
timestamp: new Date(),
|
|
330
|
+
};
|
|
331
|
+
conversation.push(userMsg);
|
|
332
|
+
printUserMessage(userMsg);
|
|
333
|
+
|
|
334
|
+
moveToFreshLine();
|
|
335
|
+
|
|
336
|
+
const useTools = mightNeedTools(userInput);
|
|
337
|
+
const tools = useTools ? nimClient.buildToolDefs(TOOL_DEFINITIONS) : undefined;
|
|
338
|
+
|
|
339
|
+
if (useTools) {
|
|
340
|
+
startSpinner('Working...');
|
|
341
|
+
try {
|
|
342
|
+
const result = await nimClient.complete(conversation, tools);
|
|
343
|
+
const fullText = result.content;
|
|
344
|
+
let toolCalls = dedup(result.toolCalls);
|
|
345
|
+
stopSpinner(true);
|
|
346
|
+
|
|
347
|
+
if (toolCalls.length === 0) {
|
|
348
|
+
// No tool calls — just display the text response
|
|
349
|
+
const cleanText = (fullText ?? '').trim().replace(/^null$/, '').trim();
|
|
350
|
+
if (cleanText) {
|
|
351
|
+
conversation.push({
|
|
352
|
+
id: crypto.randomUUID(),
|
|
353
|
+
role: 'assistant',
|
|
354
|
+
content: cleanText,
|
|
355
|
+
timestamp: new Date(),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
// Run the tool-call loop: execute tools, feed results back, repeat
|
|
360
|
+
const finalText = await runToolLoop(conversation, projectRoot, toolCalls, input, nimClient);
|
|
361
|
+
if (finalText && finalText.trim() && finalText.trim() !== 'null') {
|
|
362
|
+
printAssistantMessage(finalText.trim());
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} catch (err) {
|
|
366
|
+
process.stdout.write('\n');
|
|
367
|
+
stopSpinner(false);
|
|
368
|
+
printError('LLM error: ' + (err instanceof Error ? err.message : String(err)));
|
|
369
|
+
conversation.pop();
|
|
370
|
+
continue mainLoop;
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
// No tools — stream the response
|
|
374
|
+
startSpinner('Thinking...');
|
|
375
|
+
try {
|
|
376
|
+
const gen = nimClient.streamComplete(conversation, undefined, (text: string) => {
|
|
377
|
+
process.stdout.write(text);
|
|
378
|
+
});
|
|
379
|
+
let receivedContent = false;
|
|
380
|
+
for await (const result of gen) {
|
|
381
|
+
if (result.content && result.content.trim()) {
|
|
382
|
+
receivedContent = true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (receivedContent) {
|
|
386
|
+
process.stdout.write('\n');
|
|
387
|
+
stopSpinner(true);
|
|
388
|
+
} else {
|
|
389
|
+
process.stdout.write('\n');
|
|
390
|
+
stopSpinner(undefined);
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
process.stdout.write('\n');
|
|
394
|
+
stopSpinner(false);
|
|
395
|
+
startSpinner('Retrying...');
|
|
396
|
+
try {
|
|
397
|
+
const result = await nimClient.complete(conversation, undefined);
|
|
398
|
+
process.stdout.write('\n');
|
|
399
|
+
stopSpinner(true);
|
|
400
|
+
const cleanText = (result.content ?? '').trim();
|
|
401
|
+
if (cleanText) {
|
|
402
|
+
conversation.push({
|
|
403
|
+
id: crypto.randomUUID(),
|
|
404
|
+
role: 'assistant',
|
|
405
|
+
content: cleanText,
|
|
406
|
+
timestamp: new Date(),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
} catch (err2) {
|
|
410
|
+
process.stdout.write('\n');
|
|
411
|
+
stopSpinner(false);
|
|
412
|
+
printError('LLM error: ' + (err2 instanceof Error ? err2.message : String(err2)));
|
|
413
|
+
conversation.pop();
|
|
414
|
+
continue mainLoop;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (conversation.length > 42) {
|
|
420
|
+
conversation.splice(1, conversation.length - 42);
|
|
421
|
+
}
|
|
422
|
+
} catch (err) {
|
|
423
|
+
stopSpinner(false);
|
|
424
|
+
printError('Session error: ' + (err instanceof Error ? err.message : String(err)));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Memory & Timeline Runtime
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import { readFileSync as readFile, appendFileSync as appendFile, writeFileSync as writeFile, existsSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
// ── Agent-Termo Home Memory ──────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function ensureAgentTermoDir(): string {
|
|
11
|
+
const dir = resolve(process.env.HOME || process.env.USERPROFILE || '.', '.agent-termo');
|
|
12
|
+
try {
|
|
13
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
14
|
+
} catch {
|
|
15
|
+
// non-fatal; callers will handle missing file
|
|
16
|
+
}
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const AGENT_TERMO_MEMORY_PATH = resolve(ensureAgentTermoDir(), 'memory.md');
|
|
21
|
+
|
|
22
|
+
export function loadAgentTermoMemory(): string {
|
|
23
|
+
if (!existsSync(AGENT_TERMO_MEMORY_PATH)) return '';
|
|
24
|
+
try {
|
|
25
|
+
return readFile(AGENT_TERMO_MEMORY_PATH, 'utf-8');
|
|
26
|
+
} catch {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function appendToAgentTermoMemory(entry: string): void {
|
|
32
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
33
|
+
const content = '\n\n## ' + timestamp + '\n\n' + entry;
|
|
34
|
+
try {
|
|
35
|
+
appendFile(AGENT_TERMO_MEMORY_PATH, content, 'utf-8');
|
|
36
|
+
} catch {
|
|
37
|
+
// ignore persistence failure
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const AGENT_TERMO_TIMELINE_PATH = resolve(ensureAgentTermoDir(), 'timeline.md');
|
|
42
|
+
|
|
43
|
+
export function loadAgentTermoTimeline(): string {
|
|
44
|
+
if (!existsSync(AGENT_TERMO_TIMELINE_PATH)) return '';
|
|
45
|
+
try {
|
|
46
|
+
return readFile(AGENT_TERMO_TIMELINE_PATH, 'utf-8');
|
|
47
|
+
} catch {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function logAgentTermoTimeline(message: string): void {
|
|
53
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
54
|
+
const entry = '\n- [' + timestamp + '] ' + message;
|
|
55
|
+
try {
|
|
56
|
+
appendFile(AGENT_TERMO_TIMELINE_PATH, entry, 'utf-8');
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore persistence failure
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Project Memory & Timeline Runtime ──────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export async function loadMemory(projectRoot: string): Promise<string> {
|
|
65
|
+
const path = resolve(projectRoot, 'docs', 'MEMORY.md');
|
|
66
|
+
if (!existsSync(path)) return '';
|
|
67
|
+
try {
|
|
68
|
+
return readFile(path, 'utf-8');
|
|
69
|
+
} catch {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function appendToMemory(projectRoot: string, entry: string): Promise<void> {
|
|
75
|
+
const path = resolve(projectRoot, 'docs', 'MEMORY.md');
|
|
76
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
77
|
+
appendFile(path, '\n\n## ' + timestamp + '\n\n' + entry, 'utf-8');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Timeline System ──────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
const TIMELINE_TEMPLATE = '# Project Timeline\n\nThis document tracks the complete chronological history of the project.\n\n---\n\n';
|
|
83
|
+
|
|
84
|
+
export async function loadTimeline(projectRoot: string): Promise<string> {
|
|
85
|
+
const path = resolve(projectRoot, 'docs', 'TIMELINE.md');
|
|
86
|
+
if (!existsSync(path)) {
|
|
87
|
+
try {
|
|
88
|
+
writeFile(path, TIMELINE_TEMPLATE, 'utf-8');
|
|
89
|
+
} catch {
|
|
90
|
+
// ignore
|
|
91
|
+
}
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return readFile(path, 'utf-8');
|
|
96
|
+
} catch {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function appendToTimeline(
|
|
102
|
+
projectRoot: string,
|
|
103
|
+
agent: string,
|
|
104
|
+
phase: string,
|
|
105
|
+
actions: string[],
|
|
106
|
+
notes: string[] = [],
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
const path = resolve(projectRoot, 'docs', 'TIMELINE.md');
|
|
109
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
110
|
+
|
|
111
|
+
const entry =
|
|
112
|
+
'\n\n---\n\n## [' +
|
|
113
|
+
timestamp +
|
|
114
|
+
'] - ' +
|
|
115
|
+
agent +
|
|
116
|
+
'\n\n### Phase/Activity\n' +
|
|
117
|
+
phase +
|
|
118
|
+
'\n\n### Actions Taken\n' +
|
|
119
|
+
actions.map((a) => '- ' + a).join('\n') +
|
|
120
|
+
'\n\n### Notes\n' +
|
|
121
|
+
(notes.length > 0 ? notes.map((n) => '- ' + n).join('\n') : '- (none)');
|
|
122
|
+
|
|
123
|
+
appendFile(path, entry, 'utf-8');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function logToTimeline(projectRoot: string, message: string): Promise<void> {
|
|
127
|
+
const path = resolve(projectRoot, 'docs', 'TIMELINE.md');
|
|
128
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
129
|
+
appendFile(path, '\n- [' + timestamp + '] ' + message, 'utf-8');
|
|
130
|
+
}
|