nothumanallowed 5.0.0 → 6.0.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 +10 -3
- package/src/cli.mjs +111 -3
- package/src/commands/autostart.mjs +342 -0
- package/src/commands/chat.mjs +12 -2
- package/src/commands/ops.mjs +37 -0
- package/src/commands/ui.mjs +22 -5
- package/src/config.mjs +38 -0
- package/src/constants.mjs +8 -1
- package/src/services/llm.mjs +22 -1
- package/src/services/memory.mjs +627 -0
- package/src/services/message-responder.mjs +778 -0
- package/src/services/ops-daemon.mjs +463 -9
- package/src/services/tool-executor.mjs +392 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool executor and action parser — used by chat.mjs, ui.mjs, and voice.mjs.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes the JSON action block parsing and tool execution logic
|
|
5
|
+
* to eliminate code duplication across the three PAO interfaces.
|
|
6
|
+
*
|
|
7
|
+
* Zero dependencies — pure Node.js 22.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
listMessages,
|
|
12
|
+
getMessage,
|
|
13
|
+
getUnreadImportant,
|
|
14
|
+
sendEmail,
|
|
15
|
+
createDraft,
|
|
16
|
+
getTodayEvents,
|
|
17
|
+
getUpcomingEvents,
|
|
18
|
+
getEventsForDate,
|
|
19
|
+
createEvent,
|
|
20
|
+
updateEvent,
|
|
21
|
+
} from './mail-router.mjs';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
getTasks,
|
|
25
|
+
addTask,
|
|
26
|
+
completeTask,
|
|
27
|
+
moveTask,
|
|
28
|
+
} from './task-store.mjs';
|
|
29
|
+
|
|
30
|
+
import { notify } from './notification.mjs';
|
|
31
|
+
|
|
32
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/** Actions that mutate external state and require user confirmation. */
|
|
35
|
+
export const DESTRUCTIVE_ACTIONS = new Set([
|
|
36
|
+
'gmail_send',
|
|
37
|
+
'gmail_reply',
|
|
38
|
+
'calendar_create',
|
|
39
|
+
'calendar_move',
|
|
40
|
+
'task_done',
|
|
41
|
+
'notify_remind',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// ── System Prompt (tool definitions) ─────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the tool definitions system prompt block.
|
|
48
|
+
* Callers should replace {{TODAY}} and {{TIMEZONE}} and append their own persona.
|
|
49
|
+
*/
|
|
50
|
+
export const TOOL_DEFINITIONS = `
|
|
51
|
+
You have access to the following tools. When the user's message requires an action,
|
|
52
|
+
output EXACTLY ONE fenced JSON block per action:
|
|
53
|
+
|
|
54
|
+
\`\`\`json
|
|
55
|
+
{"action": "<tool_name>", "params": { ... }}
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
You may include conversational text BEFORE or AFTER the JSON block. If no action
|
|
59
|
+
is needed, respond normally without any JSON block.
|
|
60
|
+
|
|
61
|
+
TOOLS:
|
|
62
|
+
|
|
63
|
+
1. gmail_list(query: string, maxResults?: number)
|
|
64
|
+
Search emails. query uses Gmail search syntax (e.g. "from:boss@co.com", "is:unread subject:invoice").
|
|
65
|
+
Default maxResults = 10.
|
|
66
|
+
|
|
67
|
+
2. gmail_read(messageId: string)
|
|
68
|
+
Read the full body of an email by its ID (returned by gmail_list).
|
|
69
|
+
|
|
70
|
+
3. gmail_send(to: string, subject: string, body: string)
|
|
71
|
+
Send an email. ALWAYS confirm with the user before sending.
|
|
72
|
+
|
|
73
|
+
4. gmail_draft(to: string, subject: string, body: string)
|
|
74
|
+
Create a draft email (safe — does not send).
|
|
75
|
+
|
|
76
|
+
5. gmail_reply(messageId: string, body: string)
|
|
77
|
+
Reply to an existing email thread. ALWAYS confirm before sending.
|
|
78
|
+
|
|
79
|
+
6. calendar_today()
|
|
80
|
+
List all events for today.
|
|
81
|
+
|
|
82
|
+
7. calendar_tomorrow()
|
|
83
|
+
List all events for tomorrow.
|
|
84
|
+
|
|
85
|
+
8. calendar_upcoming(hours?: number)
|
|
86
|
+
List upcoming events in the next N hours (default 2).
|
|
87
|
+
|
|
88
|
+
9. calendar_create(summary: string, start: string, end: string, attendees?: string[], description?: string)
|
|
89
|
+
Create a calendar event. start/end are ISO 8601 datetime strings.
|
|
90
|
+
ALWAYS confirm with the user before creating.
|
|
91
|
+
|
|
92
|
+
10. calendar_move(eventId: string, newStart: string, newEnd: string)
|
|
93
|
+
Reschedule an event. ALWAYS confirm before moving.
|
|
94
|
+
|
|
95
|
+
11. task_list()
|
|
96
|
+
List today's tasks.
|
|
97
|
+
|
|
98
|
+
12. task_add(description: string, priority?: "low"|"medium"|"high"|"critical", due?: string)
|
|
99
|
+
Add a new task for today.
|
|
100
|
+
|
|
101
|
+
13. task_done(id: number)
|
|
102
|
+
Mark a task as completed. Confirm before completing.
|
|
103
|
+
|
|
104
|
+
14. task_move(id: number, toDate: string)
|
|
105
|
+
Move a task to another date (YYYY-MM-DD).
|
|
106
|
+
|
|
107
|
+
15. notify_remind(message: string, atTime: string)
|
|
108
|
+
Set a desktop reminder. atTime is ISO 8601 or relative like "in 30 minutes".
|
|
109
|
+
|
|
110
|
+
RULES:
|
|
111
|
+
- For search/read operations, execute immediately and present results conversationally.
|
|
112
|
+
- For write/send/delete operations (gmail_send, gmail_reply, calendar_create, calendar_move, task_done, notify_remind), DESCRIBE what you're about to do and include the JSON block so the system can ask the user for confirmation.
|
|
113
|
+
- When presenting email results, show From, Subject, Date, and a brief snippet. Never dump raw JSON.
|
|
114
|
+
- When presenting calendar events, show Time, Title, Location/Link. Format times in a human-readable way.
|
|
115
|
+
- When presenting tasks, show ID, Description, Priority, Status.
|
|
116
|
+
- If you need multiple actions in sequence (e.g., read an email then reply), do them ONE AT A TIME — wait for the result of each before proceeding.
|
|
117
|
+
- Dates: today is {{TODAY}}. Infer relative dates from this.
|
|
118
|
+
- The user's timezone is {{TIMEZONE}}.
|
|
119
|
+
- CRITICAL: when creating calendar events, always use LOCAL time in format "YYYY-MM-DDTHH:MM:SS" WITHOUT any Z suffix or timezone offset.
|
|
120
|
+
`.trim();
|
|
121
|
+
|
|
122
|
+
// ── Action Parser ────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract JSON action blocks from LLM response text.
|
|
126
|
+
* Supports ```json ... ``` fenced blocks.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} text
|
|
129
|
+
* @returns {{ textParts: string[], actions: Array<{action: string, params: object}> }}
|
|
130
|
+
*/
|
|
131
|
+
export function parseActions(text) {
|
|
132
|
+
const actions = [];
|
|
133
|
+
const textParts = [];
|
|
134
|
+
const fenceRegex = /```json\s*\n?([\s\S]*?)```/g;
|
|
135
|
+
let lastIndex = 0;
|
|
136
|
+
let match;
|
|
137
|
+
|
|
138
|
+
while ((match = fenceRegex.exec(text)) !== null) {
|
|
139
|
+
const before = text.slice(lastIndex, match.index).trim();
|
|
140
|
+
if (before) textParts.push(before);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(match[1].trim());
|
|
144
|
+
if (parsed.action && typeof parsed.action === 'string') {
|
|
145
|
+
actions.push({ action: parsed.action, params: parsed.params || {} });
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
textParts.push(match[0]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
lastIndex = match.index + match[0].length;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const trailing = text.slice(lastIndex).trim();
|
|
155
|
+
if (trailing) textParts.push(trailing);
|
|
156
|
+
|
|
157
|
+
return { textParts, actions };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Formatting Helpers ───────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Format an ISO timestamp into a human-readable time string.
|
|
164
|
+
*/
|
|
165
|
+
export function formatTime(isoStr) {
|
|
166
|
+
try {
|
|
167
|
+
const d = new Date(isoStr);
|
|
168
|
+
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true });
|
|
169
|
+
} catch {
|
|
170
|
+
return isoStr;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Format an array of calendar events into human-readable text.
|
|
176
|
+
*/
|
|
177
|
+
export function formatEvents(events) {
|
|
178
|
+
return events.map((e, i) => {
|
|
179
|
+
const time = e.isAllDay ? 'All day' : `${formatTime(e.start)} - ${formatTime(e.end)}`;
|
|
180
|
+
const location = e.location ? ` | ${e.location}` : '';
|
|
181
|
+
const link = e.hangoutLink ? ` | ${e.hangoutLink}` : '';
|
|
182
|
+
const cal = e.calendarName ? ` (${e.calendarName})` : '';
|
|
183
|
+
return `${i + 1}. ${time} — ${e.summary}${location}${link}${cal}`;
|
|
184
|
+
}).join('\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve a time string into a Date. Supports ISO 8601 and relative formats
|
|
189
|
+
* like "in 30 minutes", "in 2 hours".
|
|
190
|
+
*/
|
|
191
|
+
export function resolveTime(timeStr) {
|
|
192
|
+
const direct = new Date(timeStr);
|
|
193
|
+
if (!isNaN(direct.getTime()) && timeStr.includes('T')) return direct;
|
|
194
|
+
|
|
195
|
+
const relMatch = timeStr.match(/in\s+(\d+)\s+(minute|min|hour|hr)s?/i);
|
|
196
|
+
if (relMatch) {
|
|
197
|
+
const amount = parseInt(relMatch[1], 10);
|
|
198
|
+
const unit = relMatch[2].toLowerCase();
|
|
199
|
+
const ms = unit.startsWith('h') ? amount * 3600000 : amount * 60000;
|
|
200
|
+
return new Date(Date.now() + ms);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const today = new Date();
|
|
204
|
+
const timeOnly = new Date(`${today.toISOString().split('T')[0]}T${timeStr}`);
|
|
205
|
+
if (!isNaN(timeOnly.getTime())) return timeOnly;
|
|
206
|
+
|
|
207
|
+
return new Date(Date.now() + 1800000);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build a human-readable description of an action (for confirmation prompts).
|
|
212
|
+
*/
|
|
213
|
+
export function describeAction(action, params) {
|
|
214
|
+
switch (action) {
|
|
215
|
+
case 'gmail_send':
|
|
216
|
+
return `Send email to ${params.to} — Subject: "${params.subject}"`;
|
|
217
|
+
case 'gmail_reply':
|
|
218
|
+
return `Reply to message ${params.messageId}`;
|
|
219
|
+
case 'calendar_create':
|
|
220
|
+
return `Create event "${params.summary}" at ${formatTime(params.start)}`;
|
|
221
|
+
case 'calendar_move':
|
|
222
|
+
return `Reschedule event ${params.eventId} to ${formatTime(params.newStart)}`;
|
|
223
|
+
case 'task_done':
|
|
224
|
+
return `Mark task #${params.id} as completed`;
|
|
225
|
+
case 'notify_remind':
|
|
226
|
+
return `Set reminder: "${params.message}" at ${params.atTime}`;
|
|
227
|
+
default:
|
|
228
|
+
return `Execute ${action}`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Tool Executor ────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Execute a parsed tool action and return a human-readable result string.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} action — tool name
|
|
238
|
+
* @param {object} params — tool parameters
|
|
239
|
+
* @param {object} config — NHA config
|
|
240
|
+
* @returns {Promise<string>} result description
|
|
241
|
+
*/
|
|
242
|
+
export async function executeTool(action, params, config) {
|
|
243
|
+
switch (action) {
|
|
244
|
+
// ── Gmail ──────────────────────────────────────────────────────────────
|
|
245
|
+
case 'gmail_list': {
|
|
246
|
+
const query = params.query || 'is:unread';
|
|
247
|
+
const max = params.maxResults || 10;
|
|
248
|
+
const refs = await listMessages(config, query, max);
|
|
249
|
+
if (refs.length === 0) return 'No emails found matching that query.';
|
|
250
|
+
|
|
251
|
+
const messages = [];
|
|
252
|
+
for (const ref of refs.slice(0, max)) {
|
|
253
|
+
try {
|
|
254
|
+
const msg = await getMessage(config, ref.id);
|
|
255
|
+
messages.push(msg);
|
|
256
|
+
} catch { /* skip failed */ }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return messages.map((m, i) =>
|
|
260
|
+
`${i + 1}. [${m.id}] From: ${m.from} | Subject: ${m.subject} | Date: ${m.date}\n ${m.snippet.slice(0, 120)}`
|
|
261
|
+
).join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case 'gmail_read': {
|
|
265
|
+
const msg = await getMessage(config, params.messageId);
|
|
266
|
+
return [
|
|
267
|
+
`From: ${msg.from}`,
|
|
268
|
+
`To: ${msg.to}`,
|
|
269
|
+
`Subject: ${msg.subject}`,
|
|
270
|
+
`Date: ${msg.date}`,
|
|
271
|
+
`---`,
|
|
272
|
+
msg.body.slice(0, 4000),
|
|
273
|
+
].join('\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case 'gmail_send': {
|
|
277
|
+
await sendEmail(config, params.to, params.subject, params.body);
|
|
278
|
+
return `Email sent to ${params.to} with subject "${params.subject}".`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
case 'gmail_draft': {
|
|
282
|
+
await createDraft(config, params.to, params.subject, params.body);
|
|
283
|
+
return `Draft created for ${params.to} with subject "${params.subject}".`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
case 'gmail_reply': {
|
|
287
|
+
const original = await getMessage(config, params.messageId);
|
|
288
|
+
await sendEmail(config, original.from, `Re: ${original.subject}`, params.body, {
|
|
289
|
+
replyToMessageId: original.id,
|
|
290
|
+
threadId: original.threadId,
|
|
291
|
+
});
|
|
292
|
+
return `Reply sent to ${original.from} on thread "${original.subject}".`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Calendar ──────────────────────────────────────────────────────────
|
|
296
|
+
case 'calendar_today': {
|
|
297
|
+
const events = await getTodayEvents(config);
|
|
298
|
+
if (events.length === 0) return 'No events scheduled for today.';
|
|
299
|
+
return formatEvents(events);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'calendar_tomorrow': {
|
|
303
|
+
const tomorrow = new Date();
|
|
304
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
305
|
+
const dateStr = tomorrow.toISOString().split('T')[0];
|
|
306
|
+
const events = await getEventsForDate(config, dateStr);
|
|
307
|
+
if (events.length === 0) return 'No events scheduled for tomorrow.';
|
|
308
|
+
return formatEvents(events);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
case 'calendar_upcoming': {
|
|
312
|
+
const hours = params.hours || 2;
|
|
313
|
+
const events = await getUpcomingEvents(config, hours);
|
|
314
|
+
if (events.length === 0) return `No events in the next ${hours} hour(s).`;
|
|
315
|
+
return formatEvents(events);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case 'calendar_create': {
|
|
319
|
+
await createEvent(config, {
|
|
320
|
+
summary: params.summary,
|
|
321
|
+
start: params.start,
|
|
322
|
+
end: params.end,
|
|
323
|
+
description: params.description || '',
|
|
324
|
+
attendees: params.attendees || [],
|
|
325
|
+
});
|
|
326
|
+
return `Event "${params.summary}" created for ${formatTime(params.start)} - ${formatTime(params.end)}.`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
case 'calendar_move': {
|
|
330
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
331
|
+
await updateEvent(config, 'primary', params.eventId, {
|
|
332
|
+
start: { dateTime: new Date(params.newStart).toISOString(), timeZone: tz },
|
|
333
|
+
end: { dateTime: new Date(params.newEnd).toISOString(), timeZone: tz },
|
|
334
|
+
});
|
|
335
|
+
return `Event rescheduled to ${formatTime(params.newStart)} - ${formatTime(params.newEnd)}.`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── Tasks ─────────────────────────────────────────────────────────────
|
|
339
|
+
case 'task_list': {
|
|
340
|
+
const tasks = getTasks();
|
|
341
|
+
if (tasks.length === 0) return 'No tasks for today.';
|
|
342
|
+
return tasks.map(t =>
|
|
343
|
+
`#${t.id} [${t.priority}] ${t.status === 'done' ? '[DONE] ' : ''}${t.description}${t.due ? ' (due: ' + t.due + ')' : ''}`
|
|
344
|
+
).join('\n');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
case 'task_add': {
|
|
348
|
+
const task = addTask({
|
|
349
|
+
description: params.description,
|
|
350
|
+
priority: params.priority || 'medium',
|
|
351
|
+
due: params.due || null,
|
|
352
|
+
source: 'tool',
|
|
353
|
+
});
|
|
354
|
+
return `Task #${task.id} added: "${task.description}" [${task.priority}]`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
case 'task_done': {
|
|
358
|
+
const success = completeTask(params.id);
|
|
359
|
+
return success ? `Task #${params.id} marked as done.` : `Task #${params.id} not found.`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
case 'task_move': {
|
|
363
|
+
const todayStr = new Date().toISOString().split('T')[0];
|
|
364
|
+
const toDate = params.toDate === 'tomorrow'
|
|
365
|
+
? new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().split('T')[0]
|
|
366
|
+
: params.toDate;
|
|
367
|
+
const success = moveTask(params.id, todayStr, toDate);
|
|
368
|
+
return success ? `Task #${params.id} moved to ${toDate}.` : `Task #${params.id} not found.`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Notifications ─────────────────────────────────────────────────────
|
|
372
|
+
case 'notify_remind': {
|
|
373
|
+
const atTime = resolveTime(params.atTime);
|
|
374
|
+
const delayMs = atTime.getTime() - Date.now();
|
|
375
|
+
|
|
376
|
+
if (delayMs <= 0) {
|
|
377
|
+
await notify('Reminder', params.message, config);
|
|
378
|
+
return `Reminder sent now: "${params.message}"`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
setTimeout(async () => {
|
|
382
|
+
try { await notify('Reminder', params.message, config); } catch { /* best effort */ }
|
|
383
|
+
}, Math.min(delayMs, 86400000));
|
|
384
|
+
|
|
385
|
+
const minutes = Math.round(delayMs / 60000);
|
|
386
|
+
return `Reminder set for ${formatTime(atTime.toISOString())} (in ~${minutes} min): "${params.message}"`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
default:
|
|
390
|
+
return `Unknown action: ${action}`;
|
|
391
|
+
}
|
|
392
|
+
}
|