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.
- package/README.md +154 -305
- package/bin/nha.mjs +34 -3
- package/package.json +2 -2
- package/src/cli.mjs +105 -153
- package/src/commands/ask.mjs +18 -206
- package/src/commands/chat.mjs +64 -482
- package/src/commands/ui.mjs +41 -837
- package/src/config.mjs +0 -2
- package/src/constants.mjs +1 -1
- package/src/services/google-oauth.mjs +21 -12
- package/src/services/llm.mjs +0 -138
- package/src/services/ops-daemon.mjs +236 -0
- package/src/services/screen-capture.mjs +160 -0
- package/src/services/tool-executor.mjs +88 -335
- package/src/services/web-ui.mjs +126 -423
- package/src/services/browser-engine.mjs +0 -1240
- package/src/services/conversations.mjs +0 -277
- package/src/services/web-tools.mjs +0 -430
package/src/commands/chat.mjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* nha chat — Interactive conversational REPL for PAO (Personal Agent Ops).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
22
|
-
import {
|
|
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,
|
|
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
|
-
|
|
260
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
${
|
|
392
|
-
${C}/
|
|
393
|
-
${C}/
|
|
394
|
-
${C}/
|
|
395
|
-
${C}/
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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: /
|
|
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
|
|
647
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
684
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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(
|
|
299
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
729
300
|
console.log(` ${R}Error executing ${action}: ${err.message}${NC}\n`);
|
|
730
|
-
|
|
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
|
-
|
|
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');
|