nothumanallowed 9.0.5 → 9.1.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/README.md +32 -4
- package/package.json +2 -2
- package/src/commands/chat.mjs +326 -80
- package/src/constants.mjs +1 -1
- package/src/services/conversations.mjs +277 -0
- package/src/services/llm.mjs +138 -0
- package/src/services/tool-executor.mjs +74 -0
- package/src/services/web-tools.mjs +430 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Your agents. Your machine. Your rules.**
|
|
4
4
|
|
|
5
|
-
38 specialized AI agents + 50 productivity tools. Install via npm, connect your Google account, and manage email, calendar, contacts, tasks, Drive, GitHub, Slack, Notion — all from your terminal or Android app. 100% local. Zero data on our servers.
|
|
5
|
+
38 specialized AI agents + 50 productivity tools + web search. Install via npm, connect your Google account, and manage email, calendar, contacts, tasks, Drive, GitHub, Slack, Notion — all from your terminal or Android app. Streaming responses, multi-conversation history, export. 100% local. Zero data on our servers.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -28,13 +28,15 @@ nha chat
|
|
|
28
28
|
|
|
29
29
|
## What You Can Do
|
|
30
30
|
|
|
31
|
-
### Chat with 50 Tools
|
|
31
|
+
### Chat with 50 Tools + Web Search
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
nha chat
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
**Streaming responses** — tokens appear as they're generated. **Multi-conversation** — `/new`, `/list`, `/switch`, `/rename`, `/delete`. **Export** — `/export md`, `/export json`. **Web search** built in.
|
|
38
|
+
|
|
39
|
+
Ask naturally. The AI reads your email, manages your calendar, searches the web, handles tasks — all through conversation:
|
|
38
40
|
|
|
39
41
|
```
|
|
40
42
|
You: Read my latest emails
|
|
@@ -68,6 +70,7 @@ NHA: Available 60-min slots:
|
|
|
68
70
|
| **GitHub** | Notifications, issues, PRs, create issues |
|
|
69
71
|
| **Slack** | List channels, read messages, send messages |
|
|
70
72
|
| **Notion** | Search pages/databases, read page content |
|
|
73
|
+
| **Web** | Web search (DuckDuckGo), fetch URL content, deep search with page extraction |
|
|
71
74
|
| **Other** | Maps directions, reminders, file reading |
|
|
72
75
|
|
|
73
76
|
Every tool is called directly from your machine to the provider's API. NHA servers are never involved.
|
|
@@ -226,10 +229,35 @@ Your Machine Provider APIs
|
|
|
226
229
|
|
|
227
230
|
Anthropic (Claude), OpenAI (GPT), Google (Gemini), DeepSeek, xAI (Grok), Mistral, Cohere.
|
|
228
231
|
|
|
232
|
+
## Chat Commands
|
|
233
|
+
|
|
234
|
+
Inside `nha chat`, use slash commands:
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
/new Start a new conversation
|
|
238
|
+
/list List all conversations
|
|
239
|
+
/switch <id> Switch to a conversation
|
|
240
|
+
/rename <title> Rename current conversation
|
|
241
|
+
/delete <id> Delete a conversation
|
|
242
|
+
/export Export as Markdown (saved to ~/)
|
|
243
|
+
/export json Export as JSON (saved to ~/)
|
|
244
|
+
/agents List available agents
|
|
245
|
+
/agent <name> Switch to chatting with a specific agent
|
|
246
|
+
/agent off Return to NHA Chat
|
|
247
|
+
/create-agent Create a custom agent
|
|
248
|
+
/tasks Show today's tasks
|
|
249
|
+
/plan Run daily planner
|
|
250
|
+
/clear Clear current conversation
|
|
251
|
+
/help Show all commands
|
|
252
|
+
/quit Exit
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Inline agent routing: `@saber audit this function for SQL injection`
|
|
256
|
+
|
|
229
257
|
## Commands
|
|
230
258
|
|
|
231
259
|
```bash
|
|
232
|
-
# Chat (50 tools)
|
|
260
|
+
# Chat (50 tools + web search, streaming)
|
|
233
261
|
nha chat # Interactive chat with tools
|
|
234
262
|
nha voice # Voice chat with TTS
|
|
235
263
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "9.
|
|
4
|
-
"description": "NotHumanAllowed — 38 AI agents +
|
|
3
|
+
"version": "9.1.1",
|
|
4
|
+
"description": "NotHumanAllowed — 38 AI agents + 50 tools + web search. Streaming chat, multi-conversation, export. Gmail, Calendar, Drive, GitHub, Notion, Slack. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nha": "./bin/nha.mjs",
|
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
|
*
|
|
@@ -13,11 +16,12 @@
|
|
|
13
16
|
import readline from 'readline';
|
|
14
17
|
import fs from 'fs';
|
|
15
18
|
import path from 'path';
|
|
19
|
+
import os from 'os';
|
|
16
20
|
import { loadConfig } from '../config.mjs';
|
|
17
21
|
import { AGENTS_DIR, AGENTS } from '../constants.mjs';
|
|
18
|
-
import {
|
|
22
|
+
import { callLLMStream, parseAgentFile } from '../services/llm.mjs';
|
|
19
23
|
|
|
20
|
-
import {
|
|
24
|
+
import { extractMemory } from '../services/memory.mjs';
|
|
21
25
|
import { fail, info, ok, warn, C, G, Y, D, W, BOLD, NC, R } from '../ui.mjs';
|
|
22
26
|
import {
|
|
23
27
|
DESTRUCTIVE_ACTIONS,
|
|
@@ -33,6 +37,20 @@ import {
|
|
|
33
37
|
getUnreadImportant,
|
|
34
38
|
} from '../services/mail-router.mjs';
|
|
35
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';
|
|
36
54
|
|
|
37
55
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
38
56
|
|
|
@@ -119,9 +137,23 @@ async function fetchInitialContext(config) {
|
|
|
119
137
|
return parts.join('\n\n');
|
|
120
138
|
}
|
|
121
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
|
+
|
|
122
154
|
// ── Slash Command Handlers ───────────────────────────────────────────────────
|
|
123
155
|
|
|
124
|
-
async function handleSlashCommand(input, config,
|
|
156
|
+
async function handleSlashCommand(input, config, conv, rl) {
|
|
125
157
|
const trimmed = input.trim();
|
|
126
158
|
|
|
127
159
|
if (trimmed === '/quit' || trimmed === '/exit' || trimmed === '/q') {
|
|
@@ -129,12 +161,106 @@ async function handleSlashCommand(input, config, history) {
|
|
|
129
161
|
process.exit(0);
|
|
130
162
|
}
|
|
131
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
|
+
|
|
132
258
|
if (trimmed === '/clear') {
|
|
133
|
-
|
|
134
|
-
|
|
259
|
+
conv.messages.length = 0;
|
|
260
|
+
saveConversation(conv);
|
|
135
261
|
console.clear();
|
|
136
|
-
console.log(` ${G}Conversation cleared (memory preserved
|
|
137
|
-
return true;
|
|
262
|
+
console.log(` ${G}Conversation cleared (memory preserved).${NC}`);
|
|
263
|
+
return { handled: true };
|
|
138
264
|
}
|
|
139
265
|
|
|
140
266
|
if (trimmed === '/tasks') {
|
|
@@ -151,7 +277,7 @@ async function handleSlashCommand(input, config, history) {
|
|
|
151
277
|
} catch (err) {
|
|
152
278
|
console.log(` ${R}Could not load tasks: ${err.message}${NC}`);
|
|
153
279
|
}
|
|
154
|
-
return true;
|
|
280
|
+
return { handled: true };
|
|
155
281
|
}
|
|
156
282
|
|
|
157
283
|
if (trimmed === '/plan') {
|
|
@@ -161,34 +287,34 @@ async function handleSlashCommand(input, config, history) {
|
|
|
161
287
|
} catch (err) {
|
|
162
288
|
console.log(` ${R}Plan error: ${err.message}${NC}`);
|
|
163
289
|
}
|
|
164
|
-
return true;
|
|
290
|
+
return { handled: true };
|
|
165
291
|
}
|
|
166
292
|
|
|
167
293
|
// /agent <name> — switch to talking with a specific agent
|
|
168
294
|
if (trimmed.startsWith('/agent ')) {
|
|
169
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
|
+
|
|
170
303
|
const agentFile = path.join(AGENTS_DIR, `${agentName}.mjs`);
|
|
171
304
|
if (!fs.existsSync(agentFile)) {
|
|
172
305
|
console.log(` ${R}Agent "${agentName}" not found. Available: ${AGENTS.join(', ')}${NC}`);
|
|
173
|
-
return true;
|
|
306
|
+
return { handled: true };
|
|
174
307
|
}
|
|
175
308
|
const agentSource = fs.readFileSync(agentFile, 'utf-8');
|
|
176
309
|
const { card, systemPrompt: agentSysPrompt } = parseAgentFile(agentSource, agentName);
|
|
177
310
|
if (agentSysPrompt) {
|
|
178
|
-
// Store agent context for subsequent messages
|
|
179
311
|
config._chatAgent = { name: agentName, systemPrompt: agentSysPrompt, card };
|
|
180
312
|
console.log(` ${G}Now chatting with ${BOLD}${card?.displayName || agentName.toUpperCase()}${NC}${G} (${card?.tagline || 'agent'})${NC}`);
|
|
181
313
|
console.log(` ${D}Type /agent off to return to NHA Chat${NC}`);
|
|
182
314
|
} else {
|
|
183
315
|
console.log(` ${R}Agent "${agentName}" has no system prompt.${NC}`);
|
|
184
316
|
}
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (trimmed === '/agent off' || trimmed === '/agent reset') {
|
|
189
|
-
delete config._chatAgent;
|
|
190
|
-
console.log(` ${G}Switched back to NHA Chat.${NC}`);
|
|
191
|
-
return true;
|
|
317
|
+
return { handled: true };
|
|
192
318
|
}
|
|
193
319
|
|
|
194
320
|
// /create-agent <name> "<tagline>" "<system prompt>"
|
|
@@ -198,24 +324,21 @@ async function handleSlashCommand(input, config, history) {
|
|
|
198
324
|
console.log(`\n ${BOLD}${Y}Create Custom Agent${NC}`);
|
|
199
325
|
console.log(` Usage: ${C}/create-agent mybot "Short description" "You are an expert in..."${NC}`);
|
|
200
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`);
|
|
201
|
-
return true;
|
|
327
|
+
return { handled: true };
|
|
202
328
|
}
|
|
203
|
-
// Parse: name "tagline" "system prompt"
|
|
204
329
|
const nameMatch = parts.match(/^(\S+)\s+(.+)/);
|
|
205
330
|
if (!nameMatch) {
|
|
206
331
|
console.log(` ${R}Usage: /create-agent <name> "<tagline>" "<system prompt>"${NC}`);
|
|
207
|
-
return true;
|
|
332
|
+
return { handled: true };
|
|
208
333
|
}
|
|
209
334
|
const name = nameMatch[1].toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
210
335
|
const rest = nameMatch[2];
|
|
211
|
-
// Split remaining by quotes
|
|
212
336
|
const quoteParts = rest.match(/"([^"]*)"/g);
|
|
213
337
|
let tagline = '', sysPrompt = '';
|
|
214
338
|
if (quoteParts && quoteParts.length >= 2) {
|
|
215
339
|
tagline = quoteParts[0].replace(/"/g, '');
|
|
216
340
|
sysPrompt = quoteParts[1].replace(/"/g, '');
|
|
217
341
|
} else {
|
|
218
|
-
// Fallback: first sentence is tagline, rest is prompt
|
|
219
342
|
const firstDot = rest.indexOf('.');
|
|
220
343
|
if (firstDot > 0) {
|
|
221
344
|
tagline = rest.slice(0, firstDot).replace(/"/g, '').trim();
|
|
@@ -228,13 +351,13 @@ async function handleSlashCommand(input, config, history) {
|
|
|
228
351
|
|
|
229
352
|
if (!name || !tagline || !sysPrompt) {
|
|
230
353
|
console.log(` ${R}All fields required. Usage: /create-agent name "tagline" "system prompt"${NC}`);
|
|
231
|
-
return true;
|
|
354
|
+
return { handled: true };
|
|
232
355
|
}
|
|
233
356
|
|
|
234
357
|
const agentFile = path.join(AGENTS_DIR, `${name}.mjs`);
|
|
235
358
|
if (fs.existsSync(agentFile)) {
|
|
236
359
|
console.log(` ${R}Agent "${name}" already exists.${NC}`);
|
|
237
|
-
return true;
|
|
360
|
+
return { handled: true };
|
|
238
361
|
}
|
|
239
362
|
|
|
240
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`;
|
|
@@ -242,7 +365,7 @@ async function handleSlashCommand(input, config, history) {
|
|
|
242
365
|
fs.writeFileSync(agentFile, content, 'utf-8');
|
|
243
366
|
console.log(` ${G}Agent "${name}" created!${NC}`);
|
|
244
367
|
console.log(` ${D}Switch to it: /agent ${name}${NC}`);
|
|
245
|
-
return true;
|
|
368
|
+
return { handled: true };
|
|
246
369
|
}
|
|
247
370
|
|
|
248
371
|
// /agents — list available agents
|
|
@@ -258,34 +381,146 @@ async function handleSlashCommand(input, config, history) {
|
|
|
258
381
|
console.log(` ${C}${a}${NC}`);
|
|
259
382
|
}
|
|
260
383
|
console.log(`\n ${D}Switch: /agent <name> | Create: /create-agent${NC}`);
|
|
261
|
-
return true;
|
|
384
|
+
return { handled: true };
|
|
262
385
|
}
|
|
263
386
|
|
|
264
387
|
if (trimmed === '/help') {
|
|
265
388
|
console.log(`
|
|
266
389
|
${BOLD}Chat Commands${NC}
|
|
267
390
|
|
|
268
|
-
${
|
|
269
|
-
${C}/
|
|
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}
|
|
270
401
|
${C}/agents${NC} List available agents
|
|
271
402
|
${C}/agent <name>${NC} Switch to chatting with a specific agent
|
|
272
403
|
${C}/agent off${NC} Return to NHA Chat
|
|
273
|
-
${C}/create-agent${NC} Create a new custom agent
|
|
274
|
-
|
|
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
|
|
275
410
|
${C}/help${NC} Show this help
|
|
276
411
|
${C}/quit${NC} Exit chat
|
|
277
412
|
|
|
278
|
-
${D}
|
|
413
|
+
${D}Tip: Type @agent in any message to route it inline.
|
|
279
414
|
Example: "@saber audit this function for SQL injection"
|
|
280
415
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"what's on my calendar tomorrow?", "list GitHub issues", etc.${NC}
|
|
416
|
+
Type naturally — "show my unread emails", "add a task",
|
|
417
|
+
"what's on my calendar tomorrow?", "list GitHub issues"${NC}
|
|
284
418
|
`);
|
|
285
|
-
return true;
|
|
419
|
+
return { handled: true };
|
|
286
420
|
}
|
|
287
421
|
|
|
288
|
-
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 'gmail_list':
|
|
437
|
+
return `Searching emails...`;
|
|
438
|
+
case 'gmail_read':
|
|
439
|
+
return `Reading email...`;
|
|
440
|
+
case 'gmail_send':
|
|
441
|
+
case 'gmail_send_attach':
|
|
442
|
+
return `Sending email to ${params.to || '...'}...`;
|
|
443
|
+
case 'gmail_reply':
|
|
444
|
+
return `Sending reply...`;
|
|
445
|
+
case 'calendar_create':
|
|
446
|
+
return `Creating event "${params.summary || '...'}"...`;
|
|
447
|
+
case 'calendar_today':
|
|
448
|
+
case 'calendar_tomorrow':
|
|
449
|
+
case 'calendar_upcoming':
|
|
450
|
+
case 'calendar_week':
|
|
451
|
+
return `Loading calendar...`;
|
|
452
|
+
case 'github_issues':
|
|
453
|
+
case 'github_prs':
|
|
454
|
+
return `Fetching from GitHub...`;
|
|
455
|
+
case 'notion_search':
|
|
456
|
+
return `Searching Notion...`;
|
|
457
|
+
case 'slack_messages':
|
|
458
|
+
case 'slack_channels':
|
|
459
|
+
return `Loading Slack...`;
|
|
460
|
+
default:
|
|
461
|
+
return `Executing ${action}...`;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Format a result header with visual indicator based on tool type.
|
|
467
|
+
*/
|
|
468
|
+
function formatToolResult(action, params, result) {
|
|
469
|
+
switch (action) {
|
|
470
|
+
case 'web_search': {
|
|
471
|
+
const count = (result.match(/\d+\. /g) || []).length;
|
|
472
|
+
const deep = params.deep ? ', deep mode' : '';
|
|
473
|
+
return `${C}[Web Search: ${count} results${deep}]${NC}`;
|
|
474
|
+
}
|
|
475
|
+
case 'fetch_url': {
|
|
476
|
+
const domain = (params.url || '').replace(/^https?:\/\//, '').split('/')[0];
|
|
477
|
+
return `${C}[Fetched: ${domain}]${NC}`;
|
|
478
|
+
}
|
|
479
|
+
case 'gmail_list':
|
|
480
|
+
case 'gmail_read':
|
|
481
|
+
case 'gmail_send':
|
|
482
|
+
case 'gmail_send_attach':
|
|
483
|
+
case 'gmail_reply':
|
|
484
|
+
case 'gmail_draft':
|
|
485
|
+
case 'gmail_mark_read':
|
|
486
|
+
case 'gmail_mark_unread':
|
|
487
|
+
case 'gmail_archive':
|
|
488
|
+
case 'gmail_delete':
|
|
489
|
+
return `${G}[Email]${NC}`;
|
|
490
|
+
case 'calendar_today':
|
|
491
|
+
case 'calendar_tomorrow':
|
|
492
|
+
case 'calendar_upcoming':
|
|
493
|
+
case 'calendar_week':
|
|
494
|
+
case 'calendar_create':
|
|
495
|
+
case 'calendar_move':
|
|
496
|
+
case 'calendar_find':
|
|
497
|
+
case 'calendar_update':
|
|
498
|
+
case 'schedule_meeting':
|
|
499
|
+
case 'schedule_draft_email':
|
|
500
|
+
return `${G}[Calendar]${NC}`;
|
|
501
|
+
case 'task_list':
|
|
502
|
+
case 'task_add':
|
|
503
|
+
case 'task_done':
|
|
504
|
+
case 'task_move':
|
|
505
|
+
case 'task_delete':
|
|
506
|
+
case 'task_clear':
|
|
507
|
+
case 'task_edit':
|
|
508
|
+
return `${G}[Tasks]${NC}`;
|
|
509
|
+
case 'github_issues':
|
|
510
|
+
case 'github_prs':
|
|
511
|
+
case 'github_notifications':
|
|
512
|
+
case 'github_create_issue':
|
|
513
|
+
return `${G}[GitHub]${NC}`;
|
|
514
|
+
case 'notion_search':
|
|
515
|
+
case 'notion_page':
|
|
516
|
+
return `${G}[Notion]${NC}`;
|
|
517
|
+
case 'slack_channels':
|
|
518
|
+
case 'slack_messages':
|
|
519
|
+
case 'slack_send':
|
|
520
|
+
return `${G}[Slack]${NC}`;
|
|
521
|
+
default:
|
|
522
|
+
return `${G}[${action}]${NC}`;
|
|
523
|
+
}
|
|
289
524
|
}
|
|
290
525
|
|
|
291
526
|
// ── Main REPL ────────────────────────────────────────────────────────────────
|
|
@@ -298,12 +533,26 @@ export async function cmdChat(args) {
|
|
|
298
533
|
process.exit(1);
|
|
299
534
|
}
|
|
300
535
|
|
|
536
|
+
// Migrate old single-file chat history on first run
|
|
537
|
+
migrateOldHistory();
|
|
538
|
+
|
|
539
|
+
// Load or create active conversation
|
|
540
|
+
let conv = getOrCreateActive();
|
|
541
|
+
|
|
301
542
|
console.log(`
|
|
302
543
|
${BOLD}${C}NHA Chat${NC} ${D}— Personal Operations Assistant${NC}
|
|
303
544
|
${D}Type naturally to manage emails, calendar, tasks, GitHub, Notion, Slack.${NC}
|
|
304
|
-
${D}Commands: /
|
|
545
|
+
${D}Commands: /new /list /export /help /quit${NC}
|
|
305
546
|
`);
|
|
306
547
|
|
|
548
|
+
// Show active conversation info
|
|
549
|
+
const turns = Math.floor(conv.messages.length / 2);
|
|
550
|
+
if (turns > 0) {
|
|
551
|
+
ok(`Conversation: "${conv.title}" (${turns} turns)`);
|
|
552
|
+
} else {
|
|
553
|
+
info(`New conversation started. (${conv.id})`);
|
|
554
|
+
}
|
|
555
|
+
|
|
307
556
|
info('Loading today\'s context...');
|
|
308
557
|
let initialContext = '';
|
|
309
558
|
try {
|
|
@@ -322,10 +571,6 @@ export async function cmdChat(args) {
|
|
|
322
571
|
terminal: true,
|
|
323
572
|
});
|
|
324
573
|
|
|
325
|
-
const history = loadChatHistory();
|
|
326
|
-
if (history.length > 0) {
|
|
327
|
-
ok(`Loaded ${Math.floor(history.length / 2)} previous conversation turns from memory.`);
|
|
328
|
-
}
|
|
329
574
|
const systemPrompt = buildSystemPrompt('NHA Chat', CHAT_PERSONA, config, initialContext);
|
|
330
575
|
|
|
331
576
|
rl.on('close', () => {
|
|
@@ -356,8 +601,12 @@ export async function cmdChat(args) {
|
|
|
356
601
|
}
|
|
357
602
|
|
|
358
603
|
if (input.startsWith('/')) {
|
|
359
|
-
const
|
|
360
|
-
if (handled) {
|
|
604
|
+
const result = await handleSlashCommand(input, config, conv, rl);
|
|
605
|
+
if (result.handled) {
|
|
606
|
+
// Handle conversation switch
|
|
607
|
+
if (result.switchTo) {
|
|
608
|
+
conv = result.switchTo;
|
|
609
|
+
}
|
|
361
610
|
rl.prompt();
|
|
362
611
|
continue;
|
|
363
612
|
}
|
|
@@ -382,22 +631,29 @@ export async function cmdChat(args) {
|
|
|
382
631
|
}
|
|
383
632
|
}
|
|
384
633
|
} else if (config._chatAgent) {
|
|
385
|
-
// Persistent agent mode via /agent <name>
|
|
386
634
|
effectiveSystemPrompt = config._chatAgent.systemPrompt;
|
|
387
635
|
}
|
|
388
636
|
|
|
637
|
+
const history = getHistory(conv, MAX_HISTORY);
|
|
389
638
|
const userMessage = serializeHistory(history, effectiveInput);
|
|
390
639
|
|
|
391
640
|
process.stdout.write(`\n ${D}Thinking...${NC}`);
|
|
392
|
-
|
|
393
|
-
|
|
641
|
+
let firstToken = true;
|
|
642
|
+
const response = await callLLMStream(config, effectiveSystemPrompt, userMessage, (chunk) => {
|
|
643
|
+
if (firstToken) {
|
|
644
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r\n ');
|
|
645
|
+
firstToken = false;
|
|
646
|
+
}
|
|
647
|
+
process.stdout.write(chunk);
|
|
648
|
+
});
|
|
649
|
+
if (firstToken) {
|
|
650
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
651
|
+
} else {
|
|
652
|
+
process.stdout.write('\n');
|
|
653
|
+
}
|
|
394
654
|
|
|
395
655
|
const { textParts, actions } = parseActions(response);
|
|
396
|
-
|
|
397
|
-
if (textParts.length > 0) {
|
|
398
|
-
const text = textParts.join('\n\n');
|
|
399
|
-
console.log(`\n ${W}${text}${NC}\n`);
|
|
400
|
-
}
|
|
656
|
+
console.log('');
|
|
401
657
|
|
|
402
658
|
for (const { action, params } of actions) {
|
|
403
659
|
const isDestructive = DESTRUCTIVE_ACTIONS.has(action);
|
|
@@ -408,45 +664,35 @@ export async function cmdChat(args) {
|
|
|
408
664
|
|
|
409
665
|
if (!confirmed) {
|
|
410
666
|
console.log(` ${D}Cancelled.${NC}\n`);
|
|
411
|
-
|
|
412
|
-
history.push({ role: 'assistant', content: response + '\n[User cancelled this action]' });
|
|
667
|
+
addMessages(conv, input, response + '\n[User cancelled this action]');
|
|
413
668
|
continue;
|
|
414
669
|
}
|
|
415
670
|
}
|
|
416
671
|
|
|
417
672
|
try {
|
|
418
|
-
|
|
673
|
+
// Show action-specific indicator
|
|
674
|
+
const toolLabel = formatToolLabel(action, params);
|
|
675
|
+
process.stdout.write(` ${D}${toolLabel}${NC}`);
|
|
419
676
|
const result = await executeTool(action, params, config);
|
|
420
|
-
process.stdout.write('\r' + ' '.repeat(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
});
|
|
677
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
678
|
+
|
|
679
|
+
// Show action-specific result header
|
|
680
|
+
const resultHeader = formatToolResult(action, params, result);
|
|
681
|
+
console.log(` ${resultHeader}`);
|
|
682
|
+
console.log(` ${result.split('\n').join('\n ')}\n`);
|
|
683
|
+
|
|
684
|
+
addMessages(conv, input, response + `\n\n[Tool ${action} executed. Result: ${result}]`);
|
|
428
685
|
} catch (err) {
|
|
429
|
-
process.stdout.write('\r' + ' '.repeat(
|
|
686
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
430
687
|
console.log(` ${R}Error executing ${action}: ${err.message}${NC}\n`);
|
|
431
|
-
|
|
432
|
-
history.push({
|
|
433
|
-
role: 'assistant',
|
|
434
|
-
content: response + `\n\n[Tool ${action} failed: ${err.message}]`,
|
|
435
|
-
});
|
|
688
|
+
addMessages(conv, input, response + `\n\n[Tool ${action} failed: ${err.message}]`);
|
|
436
689
|
}
|
|
437
690
|
}
|
|
438
691
|
|
|
439
692
|
if (actions.length === 0) {
|
|
440
|
-
|
|
441
|
-
history.push({ role: 'assistant', content: response });
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
while (history.length > MAX_HISTORY * 2) {
|
|
445
|
-
history.shift();
|
|
446
|
-
history.shift();
|
|
693
|
+
addMessages(conv, input, response);
|
|
447
694
|
}
|
|
448
695
|
|
|
449
|
-
try { saveChatHistory(history); } catch { /* non-critical */ }
|
|
450
696
|
try { extractMemory('chat', input, response); } catch { /* non-critical */ }
|
|
451
697
|
} catch (err) {
|
|
452
698
|
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|