nothumanallowed 9.7.2 → 9.8.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/package.json +1 -1
- package/src/commands/ask.mjs +206 -18
- package/src/commands/chat.mjs +482 -64
- package/src/commands/ui.mjs +843 -89
- package/src/constants.mjs +1 -1
- package/src/services/browser-engine.mjs +1240 -0
- package/src/services/conversations.mjs +277 -0
- package/src/services/llm.mjs +120 -89
- package/src/services/tool-executor.mjs +384 -59
- package/src/services/web-tools.mjs +430 -0
- package/src/services/web-ui.mjs +422 -175
package/src/commands/chat.mjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* nha chat — Interactive conversational REPL for PAO (Personal Agent Ops).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
7
10
|
*
|
|
8
11
|
* All tool definitions, parsing, and execution are in tool-executor.mjs (DRY).
|
|
9
12
|
*
|
|
@@ -11,10 +14,15 @@
|
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
import readline from 'readline';
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import os from 'os';
|
|
14
20
|
import { loadConfig } from '../config.mjs';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
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';
|
|
18
26
|
import {
|
|
19
27
|
DESTRUCTIVE_ACTIONS,
|
|
20
28
|
parseActions,
|
|
@@ -29,6 +37,20 @@ import {
|
|
|
29
37
|
getUnreadImportant,
|
|
30
38
|
} from '../services/mail-router.mjs';
|
|
31
39
|
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';
|
|
32
54
|
|
|
33
55
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
34
56
|
|
|
@@ -115,9 +137,23 @@ async function fetchInitialContext(config) {
|
|
|
115
137
|
return parts.join('\n\n');
|
|
116
138
|
}
|
|
117
139
|
|
|
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
|
+
|
|
118
154
|
// ── Slash Command Handlers ───────────────────────────────────────────────────
|
|
119
155
|
|
|
120
|
-
async function handleSlashCommand(input, config,
|
|
156
|
+
async function handleSlashCommand(input, config, conv, rl) {
|
|
121
157
|
const trimmed = input.trim();
|
|
122
158
|
|
|
123
159
|
if (trimmed === '/quit' || trimmed === '/exit' || trimmed === '/q') {
|
|
@@ -125,12 +161,106 @@ async function handleSlashCommand(input, config, history) {
|
|
|
125
161
|
process.exit(0);
|
|
126
162
|
}
|
|
127
163
|
|
|
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
|
+
|
|
128
258
|
if (trimmed === '/clear') {
|
|
129
|
-
|
|
130
|
-
|
|
259
|
+
conv.messages.length = 0;
|
|
260
|
+
saveConversation(conv);
|
|
131
261
|
console.clear();
|
|
132
|
-
console.log(` ${G}Conversation cleared (memory preserved
|
|
133
|
-
return true;
|
|
262
|
+
console.log(` ${G}Conversation cleared (memory preserved).${NC}`);
|
|
263
|
+
return { handled: true };
|
|
134
264
|
}
|
|
135
265
|
|
|
136
266
|
if (trimmed === '/tasks') {
|
|
@@ -147,7 +277,7 @@ async function handleSlashCommand(input, config, history) {
|
|
|
147
277
|
} catch (err) {
|
|
148
278
|
console.log(` ${R}Could not load tasks: ${err.message}${NC}`);
|
|
149
279
|
}
|
|
150
|
-
return true;
|
|
280
|
+
return { handled: true };
|
|
151
281
|
}
|
|
152
282
|
|
|
153
283
|
if (trimmed === '/plan') {
|
|
@@ -157,27 +287,282 @@ async function handleSlashCommand(input, config, history) {
|
|
|
157
287
|
} catch (err) {
|
|
158
288
|
console.log(` ${R}Plan error: ${err.message}${NC}`);
|
|
159
289
|
}
|
|
160
|
-
return true;
|
|
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 };
|
|
161
385
|
}
|
|
162
386
|
|
|
163
387
|
if (trimmed === '/help') {
|
|
164
388
|
console.log(`
|
|
165
389
|
${BOLD}Chat Commands${NC}
|
|
166
390
|
|
|
167
|
-
${
|
|
168
|
-
${C}/
|
|
169
|
-
${C}/
|
|
170
|
-
${C}/
|
|
171
|
-
${C}/
|
|
172
|
-
|
|
173
|
-
${
|
|
174
|
-
|
|
175
|
-
|
|
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}
|
|
176
418
|
`);
|
|
177
|
-
return true;
|
|
419
|
+
return { handled: true };
|
|
178
420
|
}
|
|
179
421
|
|
|
180
|
-
return false;
|
|
422
|
+
return { handled: false };
|
|
423
|
+
}
|
|
424
|
+
|
|
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}...`;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
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
|
+
}
|
|
181
566
|
}
|
|
182
567
|
|
|
183
568
|
// ── Main REPL ────────────────────────────────────────────────────────────────
|
|
@@ -190,12 +575,26 @@ export async function cmdChat(args) {
|
|
|
190
575
|
process.exit(1);
|
|
191
576
|
}
|
|
192
577
|
|
|
578
|
+
// Migrate old single-file chat history on first run
|
|
579
|
+
migrateOldHistory();
|
|
580
|
+
|
|
581
|
+
// Load or create active conversation
|
|
582
|
+
let conv = getOrCreateActive();
|
|
583
|
+
|
|
193
584
|
console.log(`
|
|
194
585
|
${BOLD}${C}NHA Chat${NC} ${D}— Personal Operations Assistant${NC}
|
|
195
586
|
${D}Type naturally to manage emails, calendar, tasks, GitHub, Notion, Slack.${NC}
|
|
196
|
-
${D}Commands: /
|
|
587
|
+
${D}Commands: /new /list /export /help /quit${NC}
|
|
197
588
|
`);
|
|
198
589
|
|
|
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
|
+
|
|
199
598
|
info('Loading today\'s context...');
|
|
200
599
|
let initialContext = '';
|
|
201
600
|
try {
|
|
@@ -214,10 +613,6 @@ export async function cmdChat(args) {
|
|
|
214
613
|
terminal: true,
|
|
215
614
|
});
|
|
216
615
|
|
|
217
|
-
const history = loadChatHistory();
|
|
218
|
-
if (history.length > 0) {
|
|
219
|
-
ok(`Loaded ${Math.floor(history.length / 2)} previous conversation turns from memory.`);
|
|
220
|
-
}
|
|
221
616
|
const systemPrompt = buildSystemPrompt('NHA Chat', CHAT_PERSONA, config, initialContext);
|
|
222
617
|
|
|
223
618
|
rl.on('close', () => {
|
|
@@ -248,26 +643,59 @@ export async function cmdChat(args) {
|
|
|
248
643
|
}
|
|
249
644
|
|
|
250
645
|
if (input.startsWith('/')) {
|
|
251
|
-
const
|
|
252
|
-
if (handled) {
|
|
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
|
+
}
|
|
253
652
|
rl.prompt();
|
|
254
653
|
continue;
|
|
255
654
|
}
|
|
256
655
|
}
|
|
257
656
|
|
|
258
657
|
try {
|
|
259
|
-
|
|
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);
|
|
260
681
|
|
|
261
682
|
process.stdout.write(`\n ${D}Thinking...${NC}`);
|
|
262
|
-
|
|
263
|
-
|
|
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
|
+
}
|
|
264
696
|
|
|
265
697
|
const { textParts, actions } = parseActions(response);
|
|
266
|
-
|
|
267
|
-
if (textParts.length > 0) {
|
|
268
|
-
const text = textParts.join('\n\n');
|
|
269
|
-
console.log(`\n ${W}${text}${NC}\n`);
|
|
270
|
-
}
|
|
698
|
+
console.log('');
|
|
271
699
|
|
|
272
700
|
for (const { action, params } of actions) {
|
|
273
701
|
const isDestructive = DESTRUCTIVE_ACTIONS.has(action);
|
|
@@ -278,45 +706,35 @@ export async function cmdChat(args) {
|
|
|
278
706
|
|
|
279
707
|
if (!confirmed) {
|
|
280
708
|
console.log(` ${D}Cancelled.${NC}\n`);
|
|
281
|
-
|
|
282
|
-
history.push({ role: 'assistant', content: response + '\n[User cancelled this action]' });
|
|
709
|
+
addMessages(conv, input, response + '\n[User cancelled this action]');
|
|
283
710
|
continue;
|
|
284
711
|
}
|
|
285
712
|
}
|
|
286
713
|
|
|
287
714
|
try {
|
|
288
|
-
|
|
715
|
+
// Show action-specific indicator
|
|
716
|
+
const toolLabel = formatToolLabel(action, params);
|
|
717
|
+
process.stdout.write(` ${D}${toolLabel}${NC}`);
|
|
289
718
|
const result = await executeTool(action, params, config);
|
|
290
|
-
process.stdout.write('\r' + ' '.repeat(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
});
|
|
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}]`);
|
|
298
727
|
} catch (err) {
|
|
299
|
-
process.stdout.write('\r' + ' '.repeat(
|
|
728
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
300
729
|
console.log(` ${R}Error executing ${action}: ${err.message}${NC}\n`);
|
|
301
|
-
|
|
302
|
-
history.push({
|
|
303
|
-
role: 'assistant',
|
|
304
|
-
content: response + `\n\n[Tool ${action} failed: ${err.message}]`,
|
|
305
|
-
});
|
|
730
|
+
addMessages(conv, input, response + `\n\n[Tool ${action} failed: ${err.message}]`);
|
|
306
731
|
}
|
|
307
732
|
}
|
|
308
733
|
|
|
309
734
|
if (actions.length === 0) {
|
|
310
|
-
|
|
311
|
-
history.push({ role: 'assistant', content: response });
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
while (history.length > MAX_HISTORY * 2) {
|
|
315
|
-
history.shift();
|
|
316
|
-
history.shift();
|
|
735
|
+
addMessages(conv, input, response);
|
|
317
736
|
}
|
|
318
737
|
|
|
319
|
-
try { saveChatHistory(history); } catch { /* non-critical */ }
|
|
320
738
|
try { extractMemory('chat', input, response); } catch { /* non-critical */ }
|
|
321
739
|
} catch (err) {
|
|
322
740
|
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|