nothumanallowed 8.5.1 → 8.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "8.5.1",
3
+ "version": "8.6.1",
4
4
  "description": "NotHumanAllowed — 38 AI agents + unified productivity suite. Gmail, Calendar, Drive, Contacts, Tasks, GitHub, Notion, Slack, voice chat, smart scheduler. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -8,7 +8,7 @@ import { spawnCore } from './spawn.mjs';
8
8
  import { loadConfig, setConfigValue } from './config.mjs';
9
9
  import { checkForUpdates, runUpdate, checkNpmVersion } from './updater.mjs';
10
10
  import { download } from './downloader.mjs';
11
- import { cmdAsk } from './commands/ask.mjs';
11
+ import { cmdAsk, cmdAgentCreate, cmdAgentList, cmdAgentDelete } from './commands/ask.mjs';
12
12
  import { cmdPlan } from './commands/plan.mjs';
13
13
  import { cmdTasks } from './commands/tasks.mjs';
14
14
  import { cmdOps } from './commands/ops.mjs';
@@ -57,6 +57,15 @@ export async function main(argv) {
57
57
  case 'ask':
58
58
  return cmdAsk(args);
59
59
 
60
+ case 'agent:create':
61
+ return cmdAgentCreate(args);
62
+
63
+ case 'agent:list':
64
+ return cmdAgentList();
65
+
66
+ case 'agent:delete':
67
+ return cmdAgentDelete(args);
68
+
60
69
  case 'run':
61
70
  return cmdRun(args);
62
71
 
@@ -18,9 +18,12 @@ export async function cmdAsk(args) {
18
18
  if (!agentName || agentName.startsWith('-')) {
19
19
  fail('Usage: nha ask <agent> "your question"');
20
20
  fail(' nha ask saber "Audit this Express app for OWASP Top 10"');
21
- fail(' nha ask oracle "Analyze this CSV for trends" --file data.csv');
21
+ fail(' nha ask oracle "Analyze this CSV" --file data.csv');
22
+ fail(' nha ask forge "What\'s in this?" --image screenshot.png');
22
23
  console.log('');
23
24
  info('Available agents: ' + AGENTS.join(', '));
25
+ info('Custom agents in ~/.nha/agents/ are also available.');
26
+ info('Create one: nha agent:create myagent "Expert in X" "You are..."');
24
27
  process.exit(1);
25
28
  }
26
29
 
@@ -28,6 +31,7 @@ export async function cmdAsk(args) {
28
31
  if (!fs.existsSync(agentFile)) {
29
32
  fail(`Agent "${agentName}" not found in ~/.nha/agents/`);
30
33
  info('Available: ' + AGENTS.join(', '));
34
+ info('Create custom: nha agent:create <name> <tagline> <system-prompt>');
31
35
  process.exit(1);
32
36
  }
33
37
 
@@ -36,32 +40,33 @@ export async function cmdAsk(args) {
36
40
  let model = null;
37
41
  let stream = true;
38
42
  let attachFile = null;
43
+ let attachImage = null;
39
44
 
40
45
  for (let i = 1; i < args.length; i++) {
41
46
  if (args[i] === '--provider' && args[i + 1]) { provider = args[++i]; continue; }
42
47
  if (args[i] === '--model' && args[i + 1]) { model = args[++i]; continue; }
43
48
  if (args[i] === '--no-stream') { stream = false; continue; }
44
49
  if (args[i] === '--file' && args[i + 1]) { attachFile = args[++i]; continue; }
50
+ if (args[i] === '--image' && args[i + 1]) { attachImage = args[++i]; continue; }
45
51
  promptParts.push(args[i]);
46
52
  }
47
53
 
48
54
  let userMessage = promptParts.join(' ');
49
- if (!userMessage) {
50
- fail('No prompt provided.');
51
- fail('Usage: nha ask saber "your question here"');
52
- process.exit(1);
53
- }
54
55
 
55
56
  if (attachFile) {
56
57
  const filePath = path.resolve(attachFile);
57
- if (!fs.existsSync(filePath)) {
58
- fail(`File not found: ${attachFile}`);
59
- process.exit(1);
60
- }
58
+ if (!fs.existsSync(filePath)) { fail(`File not found: ${attachFile}`); process.exit(1); }
61
59
  const content = fs.readFileSync(filePath, 'utf-8');
62
60
  const maxChars = 100_000;
63
61
  const truncated = content.length > maxChars ? content.slice(0, maxChars) + '\n\n[... truncated ...]' : content;
64
- userMessage += `\n\n--- Attached file: ${path.basename(filePath)} ---\n${truncated}`;
62
+ info(`Attached: ${path.basename(filePath)} (${Math.round(content.length / 1024)} KB)`);
63
+ userMessage = (userMessage || 'Analyze this file') + `\n\n--- Attached file: ${path.basename(filePath)} ---\n${truncated}`;
64
+ }
65
+
66
+ if (!userMessage && !attachImage) {
67
+ fail('No prompt provided.');
68
+ fail('Usage: nha ask saber "your question here"');
69
+ process.exit(1);
65
70
  }
66
71
 
67
72
  const config = loadConfig();
@@ -85,16 +90,92 @@ export async function cmdAsk(args) {
85
90
  console.log(`\n ${BOLD}${card?.displayName || agentName.toUpperCase()}${NC} ${D}(${card?.tagline || card?.category || 'agent'})${NC}`);
86
91
  console.log(` ${D}Provider: ${provider}${model ? ' / ' + model : ''} | Direct call — no server${NC}\n`);
87
92
 
88
- const callFn = getProviderCall(provider);
89
- if (!callFn) {
90
- fail(`Unknown provider: ${provider}`);
91
- info('Supported: anthropic, openai, gemini, deepseek, grok, mistral, cohere');
92
- process.exit(1);
93
- }
94
-
95
93
  const startTime = Date.now();
96
94
 
97
95
  try {
96
+ // Image attachment — use vision API
97
+ if (attachImage) {
98
+ const imagePath = path.resolve(attachImage);
99
+ if (!fs.existsSync(imagePath)) { fail(`Image not found: ${attachImage}`); process.exit(1); }
100
+ const imageBuffer = fs.readFileSync(imagePath);
101
+ const base64 = imageBuffer.toString('base64');
102
+ const ext = path.extname(imagePath).toLowerCase();
103
+ const mimeMap = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif' };
104
+ const mimeType = mimeMap[ext] || 'image/jpeg';
105
+ info(`Attached image: ${path.basename(imagePath)} (${Math.round(base64.length * 3 / 4 / 1024)} KB)`);
106
+
107
+ const imagePrompt = userMessage || 'Describe this image in detail. Extract any text, data, or important information.';
108
+ let response = '';
109
+
110
+ if (provider === 'anthropic') {
111
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
112
+ method: 'POST',
113
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
114
+ body: JSON.stringify({
115
+ model: model || 'claude-sonnet-4-20250514', max_tokens: 8192, system: systemPrompt,
116
+ messages: [{ role: 'user', content: [
117
+ { type: 'image', source: { type: 'base64', media_type: mimeType, data: base64 } },
118
+ { type: 'text', text: imagePrompt },
119
+ ]}],
120
+ }),
121
+ });
122
+ if (!res.ok) throw new Error(`Anthropic ${res.status}: ${(await res.text()).slice(0, 300)}`);
123
+ const data = await res.json();
124
+ response = data.content?.[0]?.text || '';
125
+ } else if (provider === 'openai') {
126
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
127
+ method: 'POST',
128
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
129
+ body: JSON.stringify({
130
+ model: model || 'gpt-4o-mini', max_tokens: 8192,
131
+ messages: [
132
+ { role: 'system', content: systemPrompt },
133
+ { role: 'user', content: [
134
+ { type: 'image_url', image_url: { url: `data:${mimeType};base64,${base64}` } },
135
+ { type: 'text', text: imagePrompt },
136
+ ]},
137
+ ],
138
+ }),
139
+ });
140
+ if (!res.ok) throw new Error(`OpenAI ${res.status}: ${(await res.text()).slice(0, 300)}`);
141
+ const data = await res.json();
142
+ response = data.choices?.[0]?.message?.content || '';
143
+ } else if (provider === 'gemini') {
144
+ const m = model || 'gemini-2.0-flash';
145
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${m}:generateContent?key=${apiKey}`, {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify({
149
+ system_instruction: { parts: [{ text: systemPrompt }] },
150
+ contents: [{ parts: [
151
+ { inline_data: { mime_type: mimeType, data: base64 } },
152
+ { text: imagePrompt },
153
+ ]}],
154
+ generationConfig: { maxOutputTokens: 8192 },
155
+ }),
156
+ });
157
+ if (!res.ok) throw new Error(`Gemini ${res.status}: ${(await res.text()).slice(0, 300)}`);
158
+ const data = await res.json();
159
+ response = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
160
+ } else {
161
+ fail(`Vision not supported for "${provider}". Use anthropic, openai, or gemini.`);
162
+ process.exit(1);
163
+ }
164
+
165
+ console.log(response);
166
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
167
+ console.log(`\n ${D}${elapsed}s | ${provider}${model ? ' / ' + model : ''} | ${card?.displayName || agentName}${NC}\n`);
168
+ return;
169
+ }
170
+
171
+ // Text / file — standard LLM call
172
+ const callFn = getProviderCall(provider);
173
+ if (!callFn) {
174
+ fail(`Unknown provider: ${provider}`);
175
+ info('Supported: anthropic, openai, gemini, deepseek, grok, mistral, cohere');
176
+ process.exit(1);
177
+ }
178
+
98
179
  const useStream = stream && (provider === 'anthropic' || provider === 'openai' || provider === 'deepseek' || provider === 'grok' || provider === 'mistral');
99
180
  const result = await callFn(apiKey, model, systemPrompt, userMessage, useStream);
100
181
 
@@ -109,3 +190,110 @@ export async function cmdAsk(args) {
109
190
  process.exit(1);
110
191
  }
111
192
  }
193
+
194
+ /**
195
+ * nha agent:create <name> <tagline> <system-prompt>
196
+ * Creates a custom agent file in ~/.nha/agents/
197
+ */
198
+ /**
199
+ * nha agent:list — Show all available agents (built-in + custom)
200
+ */
201
+ export async function cmdAgentList() {
202
+ info('Built-in agents:');
203
+ for (const name of AGENTS) {
204
+ const agentFile = path.join(AGENTS_DIR, `${name}.mjs`);
205
+ if (fs.existsSync(agentFile)) {
206
+ const source = fs.readFileSync(agentFile, 'utf-8');
207
+ const { card } = parseAgentFile(source, name);
208
+ console.log(` ${C}${name.padEnd(16)}${NC} ${D}${card?.tagline || card?.category || ''}${NC}`);
209
+ } else {
210
+ console.log(` ${D}${name.padEnd(16)} (not downloaded)${NC}`);
211
+ }
212
+ }
213
+
214
+ // Custom agents
215
+ if (fs.existsSync(AGENTS_DIR)) {
216
+ const custom = fs.readdirSync(AGENTS_DIR)
217
+ .filter(f => f.endsWith('.mjs'))
218
+ .map(f => f.replace('.mjs', ''))
219
+ .filter(n => !AGENTS.includes(n));
220
+ if (custom.length > 0) {
221
+ console.log(`\n${Y}Custom agents:${NC}`);
222
+ for (const name of custom) {
223
+ const source = fs.readFileSync(path.join(AGENTS_DIR, `${name}.mjs`), 'utf-8');
224
+ const { card } = parseAgentFile(source, name);
225
+ console.log(` ${Y}${name.padEnd(16)}${NC} ${D}${card?.tagline || ''}${NC}`);
226
+ }
227
+ }
228
+ }
229
+ console.log(`\n ${D}Invoke: nha ask <agent> "prompt"${NC}`);
230
+ console.log(` ${D}Create: nha agent:create <name> "<tagline>" "<system prompt>"${NC}`);
231
+ console.log(` ${D}Delete: nha agent:delete <name>${NC}\n`);
232
+ }
233
+
234
+ /**
235
+ * nha agent:delete <name> — Delete a custom agent
236
+ */
237
+ export async function cmdAgentDelete(args) {
238
+ const name = (args[0] || '').toLowerCase();
239
+ if (!name) {
240
+ fail('Usage: nha agent:delete <agent-name>');
241
+ process.exit(1);
242
+ }
243
+ if (AGENTS.includes(name)) {
244
+ fail(`"${name}" is a built-in agent and cannot be deleted.`);
245
+ process.exit(1);
246
+ }
247
+ const agentFile = path.join(AGENTS_DIR, `${name}.mjs`);
248
+ if (!fs.existsSync(agentFile)) {
249
+ fail(`Agent "${name}" not found.`);
250
+ process.exit(1);
251
+ }
252
+ fs.unlinkSync(agentFile);
253
+ ok(`Agent "${name}" deleted.`);
254
+ }
255
+
256
+ export async function cmdAgentCreate(args) {
257
+ const name = (args[0] || '').toLowerCase().replace(/[^a-z0-9_-]/g, '');
258
+ const tagline = args[1] || '';
259
+ const sysPrompt = args.slice(2).join(' ') || '';
260
+
261
+ if (!name || !tagline || !sysPrompt) {
262
+ fail('Usage: nha agent:create <name> "<tagline>" "<system prompt>"');
263
+ console.log('');
264
+ info('Example:');
265
+ info(' nha agent:create reviewer "Code review expert" "You are a senior code reviewer..."');
266
+ console.log('');
267
+ info('The agent will be available as: nha ask reviewer "review this code"');
268
+ process.exit(1);
269
+ }
270
+
271
+ const agentFile = path.join(AGENTS_DIR, `${name}.mjs`);
272
+ if (fs.existsSync(agentFile)) {
273
+ fail(`Agent "${name}" already exists at ${agentFile}`);
274
+ process.exit(1);
275
+ }
276
+
277
+ const content = `// NHA Custom Agent: ${name}
278
+ // Created: ${new Date().toISOString()}
279
+
280
+ export const CARD = {
281
+ name: '${name}',
282
+ displayName: '${name.toUpperCase()}',
283
+ category: 'custom',
284
+ tagline: '${tagline.replace(/'/g, "\\'")}',
285
+ };
286
+
287
+ export const SYSTEM_PROMPT = \`${sysPrompt.replace(/`/g, '\\`')}\`;
288
+ `;
289
+
290
+ if (!fs.existsSync(AGENTS_DIR)) {
291
+ fs.mkdirSync(AGENTS_DIR, { recursive: true });
292
+ }
293
+
294
+ fs.writeFileSync(agentFile, content, 'utf-8');
295
+ ok(`Agent "${name}" created at ${agentFile}`);
296
+ info(`Invoke it: nha ask ${name} "your question"`);
297
+ info(`With file: nha ask ${name} "analyze" --file report.csv`);
298
+ info(`With image: nha ask ${name} "what is this?" --image photo.jpg`);
299
+ }
@@ -11,8 +11,12 @@
11
11
  */
12
12
 
13
13
  import readline from 'readline';
14
+ import fs from 'fs';
15
+ import path from 'path';
14
16
  import { loadConfig } from '../config.mjs';
15
- import { callLLM } from '../services/llm.mjs';
17
+ import { AGENTS_DIR, AGENTS } from '../constants.mjs';
18
+ import { callLLM, parseAgentFile } from '../services/llm.mjs';
19
+
16
20
  import { loadChatHistory, saveChatHistory, extractMemory } from '../services/memory.mjs';
17
21
  import { fail, info, ok, warn, C, G, Y, D, W, BOLD, NC, R } from '../ui.mjs';
18
22
  import {
@@ -160,17 +164,97 @@ async function handleSlashCommand(input, config, history) {
160
164
  return true;
161
165
  }
162
166
 
167
+ // /agent <name> — switch to talking with a specific agent
168
+ if (trimmed.startsWith('/agent ')) {
169
+ const agentName = trimmed.slice(7).trim().toLowerCase();
170
+ const agentFile = path.join(AGENTS_DIR, `${agentName}.mjs`);
171
+ if (!fs.existsSync(agentFile)) {
172
+ console.log(` ${R}Agent "${agentName}" not found. Available: ${AGENTS.join(', ')}${NC}`);
173
+ return true;
174
+ }
175
+ const agentSource = fs.readFileSync(agentFile, 'utf-8');
176
+ const { card, systemPrompt: agentSysPrompt } = parseAgentFile(agentSource, agentName);
177
+ if (agentSysPrompt) {
178
+ // Store agent context for subsequent messages
179
+ config._chatAgent = { name: agentName, systemPrompt: agentSysPrompt, card };
180
+ console.log(` ${G}Now chatting with ${BOLD}${card?.displayName || agentName.toUpperCase()}${NC}${G} (${card?.tagline || 'agent'})${NC}`);
181
+ console.log(` ${D}Type /agent off to return to NHA Chat${NC}`);
182
+ } else {
183
+ console.log(` ${R}Agent "${agentName}" has no system prompt.${NC}`);
184
+ }
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;
192
+ }
193
+
194
+ // /create-agent — interactive agent creation
195
+ if (trimmed === '/create-agent') {
196
+ const readline2 = await import('readline');
197
+ const rl2 = readline2.default.createInterface({ input: process.stdin, output: process.stdout });
198
+ const q = (prompt) => new Promise(resolve => rl2.question(prompt, resolve));
199
+ console.log(`\n ${BOLD}${Y}Create Custom Agent${NC}\n`);
200
+ const name = ((await q(` ${C}Name${NC} (lowercase, no spaces): `)) || '').toLowerCase().replace(/[^a-z0-9_-]/g, '');
201
+ const tagline = (await q(` ${C}Tagline${NC} (short description): `)) || '';
202
+ const sysPrompt = (await q(` ${C}System prompt${NC} (agent personality): `)) || '';
203
+ rl2.close();
204
+
205
+ if (!name || !tagline || !sysPrompt) {
206
+ console.log(` ${R}All fields required.${NC}`);
207
+ return true;
208
+ }
209
+
210
+ const agentFile = path.join(AGENTS_DIR, `${name}.mjs`);
211
+ if (fs.existsSync(agentFile)) {
212
+ console.log(` ${R}Agent "${name}" already exists.${NC}`);
213
+ return true;
214
+ }
215
+
216
+ 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`;
217
+ if (!fs.existsSync(AGENTS_DIR)) fs.mkdirSync(AGENTS_DIR, { recursive: true });
218
+ fs.writeFileSync(agentFile, content, 'utf-8');
219
+ console.log(` ${G}Agent "${name}" created!${NC}`);
220
+ console.log(` ${D}Switch to it: /agent ${name}${NC}`);
221
+ return true;
222
+ }
223
+
224
+ // /agents — list available agents
225
+ if (trimmed === '/agents') {
226
+ const available = [];
227
+ if (fs.existsSync(AGENTS_DIR)) {
228
+ for (const f of fs.readdirSync(AGENTS_DIR)) {
229
+ if (f.endsWith('.mjs')) available.push(f.replace('.mjs', ''));
230
+ }
231
+ }
232
+ console.log(` ${BOLD}Available Agents${NC} (${available.length})`);
233
+ for (const a of available) {
234
+ console.log(` ${C}${a}${NC}`);
235
+ }
236
+ console.log(`\n ${D}Switch: /agent <name> | Create: /create-agent${NC}`);
237
+ return true;
238
+ }
239
+
163
240
  if (trimmed === '/help') {
164
241
  console.log(`
165
242
  ${BOLD}Chat Commands${NC}
166
243
 
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
244
+ ${C}/tasks${NC} Show today's tasks
245
+ ${C}/plan${NC} Run daily planner
246
+ ${C}/agents${NC} List available agents
247
+ ${C}/agent <name>${NC} Switch to chatting with a specific agent
248
+ ${C}/agent off${NC} Return to NHA Chat
249
+ ${C}/create-agent${NC} Create a new custom agent interactively
250
+ ${C}/clear${NC} Clear conversation history
251
+ ${C}/help${NC} Show this help
252
+ ${C}/quit${NC} Exit chat
172
253
 
173
- ${D}Otherwise, just type naturally the AI understands
254
+ ${D}You can also type @agent in any message to route it to that agent.
255
+ Example: "@saber audit this function for SQL injection"
256
+
257
+ Otherwise, just type naturally — the AI understands
174
258
  requests like "show my unread emails", "add a task to review PR #42",
175
259
  "what's on my calendar tomorrow?", "list GitHub issues", etc.${NC}
176
260
  `);
@@ -256,10 +340,32 @@ export async function cmdChat(args) {
256
340
  }
257
341
 
258
342
  try {
259
- const userMessage = serializeHistory(history, input);
343
+ // Handle @agent inline routing
344
+ let effectiveSystemPrompt = systemPrompt;
345
+ let effectiveInput = input;
346
+ const atMatch = input.match(/^@(\w+)\s+(.*)/s);
347
+ if (atMatch) {
348
+ const inlineAgent = atMatch[1].toLowerCase();
349
+ const inlinePrompt = atMatch[2];
350
+ const agentFile = path.join(AGENTS_DIR, `${inlineAgent}.mjs`);
351
+ if (fs.existsSync(agentFile)) {
352
+ const agentSource = fs.readFileSync(agentFile, 'utf-8');
353
+ const { card, systemPrompt: agentSysPrompt } = parseAgentFile(agentSource, inlineAgent);
354
+ if (agentSysPrompt) {
355
+ effectiveSystemPrompt = agentSysPrompt;
356
+ effectiveInput = inlinePrompt;
357
+ process.stdout.write(` ${D}Routing to ${card?.displayName || inlineAgent.toUpperCase()}...${NC}\n`);
358
+ }
359
+ }
360
+ } else if (config._chatAgent) {
361
+ // Persistent agent mode via /agent <name>
362
+ effectiveSystemPrompt = config._chatAgent.systemPrompt;
363
+ }
364
+
365
+ const userMessage = serializeHistory(history, effectiveInput);
260
366
 
261
367
  process.stdout.write(`\n ${D}Thinking...${NC}`);
262
- const response = await callLLM(config, systemPrompt, userMessage);
368
+ const response = await callLLM(config, effectiveSystemPrompt, userMessage);
263
369
  process.stdout.write('\r' + ' '.repeat(40) + '\r');
264
370
 
265
371
  const { textParts, actions } = parseActions(response);