nothumanallowed 9.5.2 → 9.6.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.
@@ -1,12 +1,9 @@
1
1
  /**
2
2
  * nha chat — Interactive conversational REPL for PAO (Personal Agent Ops).
3
3
  *
4
- * Features:
5
- * - Streaming responses (token-by-token display)
6
- * - Multi-conversation management (/new, /list, /switch, /delete, /rename)
7
- * - Export conversations (/export md, /export json)
8
- * - @agent inline routing and /agent persistent mode
9
- * - Tool execution with confirmation for destructive actions
4
+ * The user types natural language; an LLM interprets intent, optionally
5
+ * invokes Gmail / Calendar / Tasks / GitHub / Notion / Slack APIs via a
6
+ * structured JSON action protocol, and responds conversationally.
10
7
  *
11
8
  * All tool definitions, parsing, and execution are in tool-executor.mjs (DRY).
12
9
  *
@@ -14,15 +11,10 @@
14
11
  */
15
12
 
16
13
  import readline from 'readline';
17
- import fs from 'fs';
18
- import path from 'path';
19
- import os from 'os';
20
14
  import { loadConfig } from '../config.mjs';
21
- import { AGENTS_DIR, AGENTS } from '../constants.mjs';
22
- import { callLLMStream, parseAgentFile } from '../services/llm.mjs';
23
-
24
- import { extractMemory } from '../services/memory.mjs';
25
- import { fail, info, ok, warn, C, G, Y, D, W, BOLD, NC, R, M } from '../ui.mjs';
15
+ import { callLLM } from '../services/llm.mjs';
16
+ import { loadChatHistory, saveChatHistory, extractMemory } from '../services/memory.mjs';
17
+ import { fail, info, ok, warn, C, G, Y, D, W, BOLD, NC, R } from '../ui.mjs';
26
18
  import {
27
19
  DESTRUCTIVE_ACTIONS,
28
20
  parseActions,
@@ -37,20 +29,6 @@ import {
37
29
  getUnreadImportant,
38
30
  } from '../services/mail-router.mjs';
39
31
  import { getTasks } from '../services/task-store.mjs';
40
- import {
41
- createConversation,
42
- loadConversation,
43
- saveConversation,
44
- deleteConversation,
45
- listConversations,
46
- getOrCreateActive,
47
- setActiveId,
48
- getHistory,
49
- addMessages,
50
- exportAsMarkdown,
51
- exportAsJson,
52
- migrateOldHistory,
53
- } from '../services/conversations.mjs';
54
32
 
55
33
  // ── Constants ────────────────────────────────────────────────────────────────
56
34
 
@@ -137,23 +115,9 @@ async function fetchInitialContext(config) {
137
115
  return parts.join('\n\n');
138
116
  }
139
117
 
140
- // ── Relative Time ────────────────────────────────────────────────────────────
141
-
142
- function relativeTime(isoString) {
143
- const ms = Date.now() - new Date(isoString).getTime();
144
- const minutes = Math.floor(ms / 60000);
145
- if (minutes < 1) return 'just now';
146
- if (minutes < 60) return `${minutes}m ago`;
147
- const hours = Math.floor(minutes / 60);
148
- if (hours < 24) return `${hours}h ago`;
149
- const days = Math.floor(hours / 24);
150
- if (days < 7) return `${days}d ago`;
151
- return new Date(isoString).toLocaleDateString();
152
- }
153
-
154
118
  // ── Slash Command Handlers ───────────────────────────────────────────────────
155
119
 
156
- async function handleSlashCommand(input, config, conv, rl) {
120
+ async function handleSlashCommand(input, config, history) {
157
121
  const trimmed = input.trim();
158
122
 
159
123
  if (trimmed === '/quit' || trimmed === '/exit' || trimmed === '/q') {
@@ -161,106 +125,12 @@ async function handleSlashCommand(input, config, conv, rl) {
161
125
  process.exit(0);
162
126
  }
163
127
 
164
- // ── Multi-conversation commands ──────────────────────────────────────────
165
-
166
- if (trimmed === '/new') {
167
- const newConv = createConversation();
168
- // Update the outer reference via return
169
- console.log(` ${G}New conversation started.${NC} ${D}(${newConv.id})${NC}`);
170
- return { handled: true, switchTo: newConv };
171
- }
172
-
173
- if (trimmed === '/list' || trimmed === '/conversations') {
174
- const convs = listConversations();
175
- if (convs.length === 0) {
176
- console.log(` ${D}No conversations yet.${NC}`);
177
- } else {
178
- console.log(`\n ${BOLD}Conversations${NC} (${convs.length})\n`);
179
- for (const c of convs) {
180
- const active = c.id === conv.id ? ` ${G}<- active${NC}` : '';
181
- const turns = Math.floor(c.messageCount / 2);
182
- console.log(` ${C}${c.id}${NC} ${c.title} ${D}(${turns} turns, ${relativeTime(c.updatedAt)})${NC}${active}`);
183
- }
184
- console.log(`\n ${D}Switch: /switch <id> | New: /new | Delete: /delete <id>${NC}`);
185
- }
186
- return { handled: true };
187
- }
188
-
189
- if (trimmed.startsWith('/switch ')) {
190
- const targetId = trimmed.slice(8).trim();
191
- const target = loadConversation(targetId);
192
- if (!target) {
193
- console.log(` ${R}Conversation "${targetId}" not found. Use /list to see all.${NC}`);
194
- return { handled: true };
195
- }
196
- setActiveId(targetId);
197
- const turns = Math.floor(target.messages.length / 2);
198
- console.log(` ${G}Switched to:${NC} ${target.title} ${D}(${turns} turns)${NC}`);
199
- return { handled: true, switchTo: target };
200
- }
201
-
202
- if (trimmed.startsWith('/delete ')) {
203
- const targetId = trimmed.slice(8).trim();
204
- if (targetId === conv.id) {
205
- console.log(` ${R}Cannot delete active conversation. Switch to another first.${NC}`);
206
- return { handled: true };
207
- }
208
- if (deleteConversation(targetId)) {
209
- console.log(` ${G}Deleted conversation ${targetId}.${NC}`);
210
- } else {
211
- console.log(` ${R}Conversation "${targetId}" not found.${NC}`);
212
- }
213
- return { handled: true };
214
- }
215
-
216
- if (trimmed.startsWith('/rename ')) {
217
- const newTitle = trimmed.slice(8).trim();
218
- if (!newTitle) {
219
- console.log(` ${R}Usage: /rename <new title>${NC}`);
220
- return { handled: true };
221
- }
222
- conv.title = newTitle;
223
- saveConversation(conv);
224
- console.log(` ${G}Renamed to:${NC} ${newTitle}`);
225
- return { handled: true };
226
- }
227
-
228
- // ── Export commands ──────────────────────────────────────────────────────
229
-
230
- if (trimmed === '/export' || trimmed === '/export md' || trimmed === '/export markdown') {
231
- if (conv.messages.length === 0) {
232
- console.log(` ${D}Nothing to export — conversation is empty.${NC}`);
233
- return { handled: true };
234
- }
235
- const md = exportAsMarkdown(conv);
236
- const filename = `nha-chat-${conv.id}.md`;
237
- const filePath = path.join(os.homedir(), filename);
238
- fs.writeFileSync(filePath, md, 'utf-8');
239
- console.log(` ${G}Exported as Markdown:${NC} ~/${filename}`);
240
- return { handled: true };
241
- }
242
-
243
- if (trimmed === '/export json') {
244
- if (conv.messages.length === 0) {
245
- console.log(` ${D}Nothing to export — conversation is empty.${NC}`);
246
- return { handled: true };
247
- }
248
- const json = exportAsJson(conv);
249
- const filename = `nha-chat-${conv.id}.json`;
250
- const filePath = path.join(os.homedir(), filename);
251
- fs.writeFileSync(filePath, json, 'utf-8');
252
- console.log(` ${G}Exported as JSON:${NC} ~/${filename}`);
253
- return { handled: true };
254
- }
255
-
256
- // ── Original commands ────────────────────────────────────────────────────
257
-
258
128
  if (trimmed === '/clear') {
259
- conv.messages.length = 0;
260
- saveConversation(conv);
129
+ history.length = 0;
130
+ try { saveChatHistory([]); } catch { /* non-critical */ }
261
131
  console.clear();
262
- console.log(` ${G}Conversation cleared (memory preserved).${NC}`);
263
- return { handled: true };
132
+ console.log(` ${G}Conversation cleared (memory preserved, chat history reset).${NC}`);
133
+ return true;
264
134
  }
265
135
 
266
136
  if (trimmed === '/tasks') {
@@ -277,7 +147,7 @@ async function handleSlashCommand(input, config, conv, rl) {
277
147
  } catch (err) {
278
148
  console.log(` ${R}Could not load tasks: ${err.message}${NC}`);
279
149
  }
280
- return { handled: true };
150
+ return true;
281
151
  }
282
152
 
283
153
  if (trimmed === '/plan') {
@@ -287,282 +157,27 @@ async function handleSlashCommand(input, config, conv, rl) {
287
157
  } catch (err) {
288
158
  console.log(` ${R}Plan error: ${err.message}${NC}`);
289
159
  }
290
- return { handled: true };
291
- }
292
-
293
- // /agent <name> — switch to talking with a specific agent
294
- if (trimmed.startsWith('/agent ')) {
295
- const agentName = trimmed.slice(7).trim().toLowerCase();
296
-
297
- if (agentName === 'off' || agentName === 'reset') {
298
- delete config._chatAgent;
299
- console.log(` ${G}Switched back to NHA Chat.${NC}`);
300
- return { handled: true };
301
- }
302
-
303
- const agentFile = path.join(AGENTS_DIR, `${agentName}.mjs`);
304
- if (!fs.existsSync(agentFile)) {
305
- console.log(` ${R}Agent "${agentName}" not found. Available: ${AGENTS.join(', ')}${NC}`);
306
- return { handled: true };
307
- }
308
- const agentSource = fs.readFileSync(agentFile, 'utf-8');
309
- const { card, systemPrompt: agentSysPrompt } = parseAgentFile(agentSource, agentName);
310
- if (agentSysPrompt) {
311
- config._chatAgent = { name: agentName, systemPrompt: agentSysPrompt, card };
312
- console.log(` ${G}Now chatting with ${BOLD}${card?.displayName || agentName.toUpperCase()}${NC}${G} (${card?.tagline || 'agent'})${NC}`);
313
- console.log(` ${D}Type /agent off to return to NHA Chat${NC}`);
314
- } else {
315
- console.log(` ${R}Agent "${agentName}" has no system prompt.${NC}`);
316
- }
317
- return { handled: true };
318
- }
319
-
320
- // /create-agent <name> "<tagline>" "<system prompt>"
321
- if (trimmed === '/create-agent' || trimmed.startsWith('/create-agent ')) {
322
- const parts = trimmed.slice(14).trim();
323
- if (!parts) {
324
- console.log(`\n ${BOLD}${Y}Create Custom Agent${NC}`);
325
- console.log(` Usage: ${C}/create-agent mybot "Short description" "You are an expert in..."${NC}`);
326
- console.log(` Example: ${D}/create-agent chef "Italian cooking expert" "You are a master Italian chef. Always suggest authentic recipes with step-by-step instructions."${NC}\n`);
327
- return { handled: true };
328
- }
329
- const nameMatch = parts.match(/^(\S+)\s+(.+)/);
330
- if (!nameMatch) {
331
- console.log(` ${R}Usage: /create-agent <name> "<tagline>" "<system prompt>"${NC}`);
332
- return { handled: true };
333
- }
334
- const name = nameMatch[1].toLowerCase().replace(/[^a-z0-9_-]/g, '');
335
- const rest = nameMatch[2];
336
- const quoteParts = rest.match(/"([^"]*)"/g);
337
- let tagline = '', sysPrompt = '';
338
- if (quoteParts && quoteParts.length >= 2) {
339
- tagline = quoteParts[0].replace(/"/g, '');
340
- sysPrompt = quoteParts[1].replace(/"/g, '');
341
- } else {
342
- const firstDot = rest.indexOf('.');
343
- if (firstDot > 0) {
344
- tagline = rest.slice(0, firstDot).replace(/"/g, '').trim();
345
- sysPrompt = rest.slice(firstDot + 1).replace(/"/g, '').trim();
346
- } else {
347
- tagline = rest.replace(/"/g, '').trim();
348
- sysPrompt = tagline;
349
- }
350
- }
351
-
352
- if (!name || !tagline || !sysPrompt) {
353
- console.log(` ${R}All fields required. Usage: /create-agent name "tagline" "system prompt"${NC}`);
354
- return { handled: true };
355
- }
356
-
357
- const agentFile = path.join(AGENTS_DIR, `${name}.mjs`);
358
- if (fs.existsSync(agentFile)) {
359
- console.log(` ${R}Agent "${name}" already exists.${NC}`);
360
- return { handled: true };
361
- }
362
-
363
- const content = `// NHA Custom Agent: ${name}\n// Created: ${new Date().toISOString()}\n\nexport const CARD = {\n name: '${name}',\n displayName: '${name.toUpperCase()}',\n category: 'custom',\n tagline: '${tagline.replace(/'/g, "\\'")}',\n};\n\nexport const SYSTEM_PROMPT = \`${sysPrompt.replace(/`/g, '\\`')}\`;\n`;
364
- if (!fs.existsSync(AGENTS_DIR)) fs.mkdirSync(AGENTS_DIR, { recursive: true });
365
- fs.writeFileSync(agentFile, content, 'utf-8');
366
- console.log(` ${G}Agent "${name}" created!${NC}`);
367
- console.log(` ${D}Switch to it: /agent ${name}${NC}`);
368
- return { handled: true };
369
- }
370
-
371
- // /agents — list available agents
372
- if (trimmed === '/agents') {
373
- const available = [];
374
- if (fs.existsSync(AGENTS_DIR)) {
375
- for (const f of fs.readdirSync(AGENTS_DIR)) {
376
- if (f.endsWith('.mjs')) available.push(f.replace('.mjs', ''));
377
- }
378
- }
379
- console.log(` ${BOLD}Available Agents${NC} (${available.length})`);
380
- for (const a of available) {
381
- console.log(` ${C}${a}${NC}`);
382
- }
383
- console.log(`\n ${D}Switch: /agent <name> | Create: /create-agent${NC}`);
384
- return { handled: true };
160
+ return true;
385
161
  }
386
162
 
387
163
  if (trimmed === '/help') {
388
164
  console.log(`
389
165
  ${BOLD}Chat Commands${NC}
390
166
 
391
- ${BOLD}Conversations${NC}
392
- ${C}/new${NC} Start a new conversation
393
- ${C}/list${NC} List all conversations
394
- ${C}/switch <id>${NC} Switch to a conversation
395
- ${C}/rename <title>${NC} Rename current conversation
396
- ${C}/delete <id>${NC} Delete a conversation
397
- ${C}/export${NC} Export as Markdown (~/)
398
- ${C}/export json${NC} Export as JSON (~/)
399
-
400
- ${BOLD}Agents${NC}
401
- ${C}/agents${NC} List available agents
402
- ${C}/agent <name>${NC} Switch to chatting with a specific agent
403
- ${C}/agent off${NC} Return to NHA Chat
404
- ${C}/create-agent${NC} Create a new custom agent
405
-
406
- ${BOLD}Tools${NC}
407
- ${C}/tasks${NC} Show today's tasks
408
- ${C}/plan${NC} Run daily planner
409
- ${C}/clear${NC} Clear current conversation
410
- ${C}/help${NC} Show this help
411
- ${C}/quit${NC} Exit chat
412
-
413
- ${D}Tip: Type @agent in any message to route it inline.
414
- Example: "@saber audit this function for SQL injection"
415
-
416
- Type naturally — "show my unread emails", "add a task",
417
- "what's on my calendar tomorrow?", "list GitHub issues"${NC}
418
- `);
419
- return { handled: true };
420
- }
421
-
422
- return { handled: false };
423
- }
167
+ ${C}/tasks${NC} Show today's tasks
168
+ ${C}/plan${NC} Run daily planner
169
+ ${C}/clear${NC} Clear conversation history
170
+ ${C}/help${NC} Show this help
171
+ ${C}/quit${NC} Exit chat
424
172
 
425
- // ── Tool Indicators ──────────────────────────────────────────────────────────
426
-
427
- /**
428
- * Format a user-visible label while a tool is executing.
429
- */
430
- function formatToolLabel(action, params) {
431
- switch (action) {
432
- case 'web_search':
433
- return `Searching the web for "${params.query || '...'}"...`;
434
- case 'fetch_url':
435
- return `Fetching ${params.url || 'URL'}...`;
436
- case 'browser_open':
437
- return `Opening ${params.url || 'page'} in browser...`;
438
- case 'browser_screenshot':
439
- return `Taking screenshot...`;
440
- case 'browser_click':
441
- return `Clicking ${params.selector || `(${params.x}, ${params.y})`}...`;
442
- case 'browser_type':
443
- return `Typing into ${params.selector || 'field'}...`;
444
- case 'browser_extract':
445
- return `Extracting content from ${params.selector || 'page'}...`;
446
- case 'browser_js':
447
- return `Executing JavaScript...`;
448
- case 'browser_wait':
449
- return `Waiting for ${params.selector || 'element'}...`;
450
- case 'browser_scroll':
451
- return `Scrolling ${params.direction || 'down'}...`;
452
- case 'browser_key':
453
- return `Pressing ${params.key || 'key'}...`;
454
- case 'browser_close':
455
- return `Closing browser...`;
456
- case 'gmail_list':
457
- return `Searching emails...`;
458
- case 'gmail_read':
459
- return `Reading email...`;
460
- case 'gmail_send':
461
- case 'gmail_send_attach':
462
- return `Sending email to ${params.to || '...'}...`;
463
- case 'gmail_reply':
464
- return `Sending reply...`;
465
- case 'calendar_create':
466
- return `Creating event "${params.summary || '...'}"...`;
467
- case 'calendar_today':
468
- case 'calendar_tomorrow':
469
- case 'calendar_upcoming':
470
- case 'calendar_week':
471
- return `Loading calendar...`;
472
- case 'github_issues':
473
- case 'github_prs':
474
- return `Fetching from GitHub...`;
475
- case 'notion_search':
476
- return `Searching Notion...`;
477
- case 'slack_messages':
478
- case 'slack_channels':
479
- return `Loading Slack...`;
480
- default:
481
- return `Executing ${action}...`;
173
+ ${D}Otherwise, just type naturally — the AI understands
174
+ requests like "show my unread emails", "add a task to review PR #42",
175
+ "what's on my calendar tomorrow?", "list GitHub issues", etc.${NC}
176
+ `);
177
+ return true;
482
178
  }
483
- }
484
179
 
485
- /**
486
- * Format a result header with visual indicator based on tool type.
487
- */
488
- function formatToolResult(action, params, result) {
489
- switch (action) {
490
- case 'web_search': {
491
- const count = (result.match(/\d+\. /g) || []).length;
492
- const deep = params.deep ? ', deep mode' : '';
493
- return `${C}[Web Search: ${count} results${deep}]${NC}`;
494
- }
495
- case 'fetch_url': {
496
- const domain = (params.url || '').replace(/^https?:\/\//, '').split('/')[0];
497
- return `${C}[Fetched: ${domain}]${NC}`;
498
- }
499
- case 'browser_open': {
500
- const domain = (params.url || '').replace(/^https?:\/\//, '').split('/')[0];
501
- return `${M}[Browser: ${domain}]${NC}`;
502
- }
503
- case 'browser_screenshot':
504
- return `${M}[Screenshot]${NC}`;
505
- case 'browser_click':
506
- return `${M}[Click: ${params.selector || `(${params.x}, ${params.y})`}]${NC}`;
507
- case 'browser_type':
508
- return `${M}[Typed: ${(params.text || '').slice(0, 30)}${(params.text || '').length > 30 ? '...' : ''}]${NC}`;
509
- case 'browser_extract':
510
- return `${M}[Extracted: ${params.selector || 'page'}]${NC}`;
511
- case 'browser_js':
512
- return `${M}[JS executed]${NC}`;
513
- case 'browser_wait':
514
- return `${M}[Found: ${params.selector}]${NC}`;
515
- case 'browser_scroll':
516
- return `${M}[Scrolled ${params.direction || 'down'}]${NC}`;
517
- case 'browser_key':
518
- return `${M}[Key: ${params.key}]${NC}`;
519
- case 'browser_close':
520
- return `${M}[Browser closed]${NC}`;
521
- case 'gmail_list':
522
- case 'gmail_read':
523
- case 'gmail_send':
524
- case 'gmail_send_attach':
525
- case 'gmail_reply':
526
- case 'gmail_draft':
527
- case 'gmail_mark_read':
528
- case 'gmail_mark_unread':
529
- case 'gmail_archive':
530
- case 'gmail_delete':
531
- return `${G}[Email]${NC}`;
532
- case 'calendar_today':
533
- case 'calendar_tomorrow':
534
- case 'calendar_upcoming':
535
- case 'calendar_week':
536
- case 'calendar_create':
537
- case 'calendar_move':
538
- case 'calendar_find':
539
- case 'calendar_update':
540
- case 'schedule_meeting':
541
- case 'schedule_draft_email':
542
- return `${G}[Calendar]${NC}`;
543
- case 'task_list':
544
- case 'task_add':
545
- case 'task_done':
546
- case 'task_move':
547
- case 'task_delete':
548
- case 'task_clear':
549
- case 'task_edit':
550
- return `${G}[Tasks]${NC}`;
551
- case 'github_issues':
552
- case 'github_prs':
553
- case 'github_notifications':
554
- case 'github_create_issue':
555
- return `${G}[GitHub]${NC}`;
556
- case 'notion_search':
557
- case 'notion_page':
558
- return `${G}[Notion]${NC}`;
559
- case 'slack_channels':
560
- case 'slack_messages':
561
- case 'slack_send':
562
- return `${G}[Slack]${NC}`;
563
- default:
564
- return `${G}[${action}]${NC}`;
565
- }
180
+ return false;
566
181
  }
567
182
 
568
183
  // ── Main REPL ────────────────────────────────────────────────────────────────
@@ -575,26 +190,12 @@ export async function cmdChat(args) {
575
190
  process.exit(1);
576
191
  }
577
192
 
578
- // Migrate old single-file chat history on first run
579
- migrateOldHistory();
580
-
581
- // Load or create active conversation
582
- let conv = getOrCreateActive();
583
-
584
193
  console.log(`
585
194
  ${BOLD}${C}NHA Chat${NC} ${D}— Personal Operations Assistant${NC}
586
195
  ${D}Type naturally to manage emails, calendar, tasks, GitHub, Notion, Slack.${NC}
587
- ${D}Commands: /new /list /export /help /quit${NC}
196
+ ${D}Commands: /tasks /plan /clear /help /quit${NC}
588
197
  `);
589
198
 
590
- // Show active conversation info
591
- const turns = Math.floor(conv.messages.length / 2);
592
- if (turns > 0) {
593
- ok(`Conversation: "${conv.title}" (${turns} turns)`);
594
- } else {
595
- info(`New conversation started. (${conv.id})`);
596
- }
597
-
598
199
  info('Loading today\'s context...');
599
200
  let initialContext = '';
600
201
  try {
@@ -613,6 +214,10 @@ export async function cmdChat(args) {
613
214
  terminal: true,
614
215
  });
615
216
 
217
+ const history = loadChatHistory();
218
+ if (history.length > 0) {
219
+ ok(`Loaded ${Math.floor(history.length / 2)} previous conversation turns from memory.`);
220
+ }
616
221
  const systemPrompt = buildSystemPrompt('NHA Chat', CHAT_PERSONA, config, initialContext);
617
222
 
618
223
  rl.on('close', () => {
@@ -643,59 +248,26 @@ export async function cmdChat(args) {
643
248
  }
644
249
 
645
250
  if (input.startsWith('/')) {
646
- const result = await handleSlashCommand(input, config, conv, rl);
647
- if (result.handled) {
648
- // Handle conversation switch
649
- if (result.switchTo) {
650
- conv = result.switchTo;
651
- }
251
+ const handled = await handleSlashCommand(input, config, history);
252
+ if (handled) {
652
253
  rl.prompt();
653
254
  continue;
654
255
  }
655
256
  }
656
257
 
657
258
  try {
658
- // Handle @agent inline routing
659
- let effectiveSystemPrompt = systemPrompt;
660
- let effectiveInput = input;
661
- const atMatch = input.match(/^@(\w+)\s+(.*)/s);
662
- if (atMatch) {
663
- const inlineAgent = atMatch[1].toLowerCase();
664
- const inlinePrompt = atMatch[2];
665
- const agentFile = path.join(AGENTS_DIR, `${inlineAgent}.mjs`);
666
- if (fs.existsSync(agentFile)) {
667
- const agentSource = fs.readFileSync(agentFile, 'utf-8');
668
- const { card, systemPrompt: agentSysPrompt } = parseAgentFile(agentSource, inlineAgent);
669
- if (agentSysPrompt) {
670
- effectiveSystemPrompt = agentSysPrompt;
671
- effectiveInput = inlinePrompt;
672
- process.stdout.write(` ${D}Routing to ${card?.displayName || inlineAgent.toUpperCase()}...${NC}\n`);
673
- }
674
- }
675
- } else if (config._chatAgent) {
676
- effectiveSystemPrompt = config._chatAgent.systemPrompt;
677
- }
678
-
679
- const history = getHistory(conv, MAX_HISTORY);
680
- const userMessage = serializeHistory(history, effectiveInput);
259
+ const userMessage = serializeHistory(history, input);
681
260
 
682
261
  process.stdout.write(`\n ${D}Thinking...${NC}`);
683
- let firstToken = true;
684
- const response = await callLLMStream(config, effectiveSystemPrompt, userMessage, (chunk) => {
685
- if (firstToken) {
686
- process.stdout.write('\r' + ' '.repeat(40) + '\r\n ');
687
- firstToken = false;
688
- }
689
- process.stdout.write(chunk);
690
- });
691
- if (firstToken) {
692
- process.stdout.write('\r' + ' '.repeat(40) + '\r');
693
- } else {
694
- process.stdout.write('\n');
695
- }
262
+ const response = await callLLM(config, systemPrompt, userMessage);
263
+ process.stdout.write('\r' + ' '.repeat(40) + '\r');
696
264
 
697
265
  const { textParts, actions } = parseActions(response);
698
- console.log('');
266
+
267
+ if (textParts.length > 0) {
268
+ const text = textParts.join('\n\n');
269
+ console.log(`\n ${W}${text}${NC}\n`);
270
+ }
699
271
 
700
272
  for (const { action, params } of actions) {
701
273
  const isDestructive = DESTRUCTIVE_ACTIONS.has(action);
@@ -706,35 +278,45 @@ export async function cmdChat(args) {
706
278
 
707
279
  if (!confirmed) {
708
280
  console.log(` ${D}Cancelled.${NC}\n`);
709
- addMessages(conv, input, response + '\n[User cancelled this action]');
281
+ history.push({ role: 'user', content: input });
282
+ history.push({ role: 'assistant', content: response + '\n[User cancelled this action]' });
710
283
  continue;
711
284
  }
712
285
  }
713
286
 
714
287
  try {
715
- // Show action-specific indicator
716
- const toolLabel = formatToolLabel(action, params);
717
- process.stdout.write(` ${D}${toolLabel}${NC}`);
288
+ process.stdout.write(` ${D}Executing ${action}...${NC}`);
718
289
  const result = await executeTool(action, params, config);
719
- process.stdout.write('\r' + ' '.repeat(80) + '\r');
720
-
721
- // Show action-specific result header
722
- const resultHeader = formatToolResult(action, params, result);
723
- console.log(` ${resultHeader}`);
724
- console.log(` ${result.split('\n').join('\n ')}\n`);
725
-
726
- addMessages(conv, input, response + `\n\n[Tool ${action} executed. Result: ${result}]`);
290
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
291
+ console.log(` ${G}Result:${NC}\n ${result.split('\n').join('\n ')}\n`);
292
+
293
+ history.push({ role: 'user', content: input });
294
+ history.push({
295
+ role: 'assistant',
296
+ content: response + `\n\n[Tool ${action} executed. Result: ${result}]`,
297
+ });
727
298
  } catch (err) {
728
- process.stdout.write('\r' + ' '.repeat(80) + '\r');
299
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
729
300
  console.log(` ${R}Error executing ${action}: ${err.message}${NC}\n`);
730
- addMessages(conv, input, response + `\n\n[Tool ${action} failed: ${err.message}]`);
301
+ history.push({ role: 'user', content: input });
302
+ history.push({
303
+ role: 'assistant',
304
+ content: response + `\n\n[Tool ${action} failed: ${err.message}]`,
305
+ });
731
306
  }
732
307
  }
733
308
 
734
309
  if (actions.length === 0) {
735
- addMessages(conv, input, response);
310
+ history.push({ role: 'user', content: input });
311
+ history.push({ role: 'assistant', content: response });
312
+ }
313
+
314
+ while (history.length > MAX_HISTORY * 2) {
315
+ history.shift();
316
+ history.shift();
736
317
  }
737
318
 
319
+ try { saveChatHistory(history); } catch { /* non-critical */ }
738
320
  try { extractMemory('chat', input, response); } catch { /* non-critical */ }
739
321
  } catch (err) {
740
322
  process.stdout.write('\r' + ' '.repeat(40) + '\r');