nothumanallowed 1.1.0 → 2.1.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.
@@ -0,0 +1,29 @@
1
+ /** nha google auth|status|revoke — Google account management */
2
+
3
+ import { runAuthFlow, showStatus, revokeAuth } from '../services/google-oauth.mjs';
4
+ import { loadConfig } from '../config.mjs';
5
+ import { fail, info } from '../ui.mjs';
6
+
7
+ export async function cmdGoogle(args) {
8
+ const sub = args[0] || 'auth';
9
+ const config = loadConfig();
10
+
11
+ switch (sub) {
12
+ case 'auth':
13
+ case 'login':
14
+ case 'connect':
15
+ return runAuthFlow(config);
16
+
17
+ case 'status':
18
+ return showStatus();
19
+
20
+ case 'revoke':
21
+ case 'disconnect':
22
+ case 'logout':
23
+ return revokeAuth();
24
+
25
+ default:
26
+ fail(`Unknown: nha google ${sub}`);
27
+ info('Commands: auth, status, revoke');
28
+ }
29
+ }
@@ -0,0 +1,77 @@
1
+ /** nha ops — Daemon control for Personal Agent Operations */
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { startDaemon, stopDaemon, getDaemonStatus, isRunning } from '../services/ops-daemon.mjs';
6
+ import { runPlanningPipeline } from '../services/ops-pipeline.mjs';
7
+ import { loadConfig } from '../config.mjs';
8
+ import { NHA_DIR } from '../constants.mjs';
9
+ import { info, ok, fail, warn, C, G, Y, D, W, BOLD, NC, R } from '../ui.mjs';
10
+
11
+ export async function cmdOps(args) {
12
+ const sub = args[0] || 'status';
13
+
14
+ switch (sub) {
15
+ case 'start': {
16
+ const result = startDaemon();
17
+ if (result.ok) {
18
+ ok(`PAO daemon started (PID ${result.pid})`);
19
+ info('Monitoring Gmail + Calendar. Notifications enabled.');
20
+ info('Run "nha ops status" to check. "nha ops stop" to halt.');
21
+ } else {
22
+ warn(result.message);
23
+ }
24
+ return;
25
+ }
26
+
27
+ case 'stop': {
28
+ const result = stopDaemon();
29
+ if (result.ok) {
30
+ ok(`Daemon stopped (PID ${result.pid})`);
31
+ } else {
32
+ warn(result.message);
33
+ }
34
+ return;
35
+ }
36
+
37
+ case 'status': {
38
+ const status = getDaemonStatus();
39
+ console.log(`\n ${BOLD}PAO Daemon Status${NC}\n`);
40
+ console.log(` Running: ${status.running ? G + 'yes' + NC + ` (PID ${status.pid})` : R + 'no' + NC}`);
41
+ if (status.startedAt) console.log(` Started: ${D}${status.startedAt}${NC}`);
42
+ if (status.lastMailCheck) console.log(` Last mail check: ${D}${status.lastMailCheck}${NC}`);
43
+ if (status.lastCalendarCheck) console.log(` Last cal check: ${D}${status.lastCalendarCheck}${NC}`);
44
+ if (status.lastPlanGenerated) console.log(` Last plan: ${D}${status.lastPlanGenerated}${NC}`);
45
+ if (status.errors > 0) console.log(` Errors: ${Y}${status.errors}${NC}`);
46
+ console.log('');
47
+ return;
48
+ }
49
+
50
+ case 'logs': {
51
+ const logFile = path.join(NHA_DIR, 'ops', 'daemon', 'daemon.log');
52
+ if (!fs.existsSync(logFile)) {
53
+ info('No daemon logs. Start with: nha ops start');
54
+ return;
55
+ }
56
+ const content = fs.readFileSync(logFile, 'utf-8');
57
+ const lines = content.split('\n').filter(Boolean);
58
+ const last50 = lines.slice(-50);
59
+ for (const line of last50) {
60
+ console.log(` ${D}${line}${NC}`);
61
+ }
62
+ return;
63
+ }
64
+
65
+ case 'run': {
66
+ // One-shot: sync + plan + exit
67
+ const config = loadConfig();
68
+ info('Running one-shot PAO pipeline...');
69
+ await runPlanningPipeline(config, { refresh: true });
70
+ return;
71
+ }
72
+
73
+ default:
74
+ fail(`Unknown: nha ops ${sub}`);
75
+ info('Commands: start, stop, status, logs, run');
76
+ }
77
+ }
@@ -0,0 +1,45 @@
1
+ /** nha plan — Generate daily plan using 5 specialist agents */
2
+
3
+ import { runPlanningPipeline } from '../services/ops-pipeline.mjs';
4
+ import { loadConfig } from '../config.mjs';
5
+ import { fail } from '../ui.mjs';
6
+
7
+ export async function cmdPlan(args) {
8
+ const config = loadConfig();
9
+
10
+ if (!config.llm.apiKey) {
11
+ fail('No API key configured. Run: nha config set key YOUR_KEY');
12
+ process.exit(1);
13
+ }
14
+
15
+ let date = null;
16
+ let refresh = false;
17
+ let showOnly = false;
18
+
19
+ for (const arg of args) {
20
+ if (arg === '--refresh') { refresh = true; continue; }
21
+ if (arg === '--show') { showOnly = true; continue; }
22
+ if (arg === 'tomorrow') {
23
+ const d = new Date();
24
+ d.setDate(d.getDate() + 1);
25
+ date = d.toISOString().split('T')[0];
26
+ continue;
27
+ }
28
+ if (arg === 'yesterday') {
29
+ const d = new Date();
30
+ d.setDate(d.getDate() - 1);
31
+ date = d.toISOString().split('T')[0];
32
+ continue;
33
+ }
34
+ if (arg.startsWith('--date=')) {
35
+ date = arg.split('=')[1];
36
+ continue;
37
+ }
38
+ if (/^\d{4}-\d{2}-\d{2}$/.test(arg)) {
39
+ date = arg;
40
+ continue;
41
+ }
42
+ }
43
+
44
+ await runPlanningPipeline(config, { date, refresh, showOnly });
45
+ }
@@ -0,0 +1,132 @@
1
+ /** nha tasks — Local task management */
2
+
3
+ import { getTasks, addTask, completeTask, editTask, moveTask, getWeekTasks, getDayStats } from '../services/task-store.mjs';
4
+ import { fail, info, ok, C, G, Y, D, W, BOLD, NC, R } from '../ui.mjs';
5
+
6
+ export async function cmdTasks(args) {
7
+ const sub = args[0];
8
+
9
+ // nha tasks (list today)
10
+ if (!sub || sub === 'list' || sub === 'today') {
11
+ const date = args[1] || undefined;
12
+ return showTasks(date);
13
+ }
14
+
15
+ // nha tasks add "description" [--priority high] [--due 14:00]
16
+ if (sub === 'add') {
17
+ const parts = args.slice(1);
18
+ let description = '';
19
+ let priority = 'medium';
20
+ let due = null;
21
+
22
+ for (let i = 0; i < parts.length; i++) {
23
+ if (parts[i] === '--priority' && parts[i + 1]) { priority = parts[++i]; continue; }
24
+ if (parts[i] === '--due' && parts[i + 1]) { due = parts[++i]; continue; }
25
+ if (parts[i] === '-p' && parts[i + 1]) { priority = parts[++i]; continue; }
26
+ description += (description ? ' ' : '') + parts[i];
27
+ }
28
+
29
+ if (!description) {
30
+ fail('Usage: nha tasks add "task description" [--priority high] [--due 14:00]');
31
+ return;
32
+ }
33
+
34
+ const task = addTask({ description, priority, due });
35
+ ok(`Task #${task.id} added: ${description}`);
36
+ return;
37
+ }
38
+
39
+ // nha tasks done <id>
40
+ if (sub === 'done' || sub === 'complete') {
41
+ const id = parseInt(args[1]);
42
+ if (!id) { fail('Usage: nha tasks done <task-id>'); return; }
43
+ if (completeTask(id)) {
44
+ ok(`Task #${id} completed`);
45
+ } else {
46
+ fail(`Task #${id} not found`);
47
+ }
48
+ return;
49
+ }
50
+
51
+ // nha tasks edit <id> "new description"
52
+ if (sub === 'edit') {
53
+ const id = parseInt(args[1]);
54
+ const desc = args.slice(2).join(' ');
55
+ if (!id || !desc) { fail('Usage: nha tasks edit <id> "new description"'); return; }
56
+ if (editTask(id, desc)) {
57
+ ok(`Task #${id} updated`);
58
+ } else {
59
+ fail(`Task #${id} not found`);
60
+ }
61
+ return;
62
+ }
63
+
64
+ // nha tasks move <id> tomorrow|YYYY-MM-DD
65
+ if (sub === 'move') {
66
+ const id = parseInt(args[1]);
67
+ let toDate = args[2];
68
+ if (!id || !toDate) { fail('Usage: nha tasks move <id> tomorrow'); return; }
69
+
70
+ if (toDate === 'tomorrow') {
71
+ const d = new Date();
72
+ d.setDate(d.getDate() + 1);
73
+ toDate = d.toISOString().split('T')[0];
74
+ }
75
+
76
+ const today = new Date().toISOString().split('T')[0];
77
+ if (moveTask(id, today, toDate)) {
78
+ ok(`Task #${id} moved to ${toDate}`);
79
+ } else {
80
+ fail(`Task #${id} not found`);
81
+ }
82
+ return;
83
+ }
84
+
85
+ // nha tasks week
86
+ if (sub === 'week') {
87
+ const week = getWeekTasks();
88
+ console.log(`\n ${BOLD}Week Overview${NC}\n`);
89
+ for (const day of week) {
90
+ const stats = getDayStats(day.date);
91
+ const isToday = day.date === new Date().toISOString().split('T')[0];
92
+ const label = isToday ? `${BOLD}${day.day} ${day.date} (today)${NC}` : `${D}${day.day} ${day.date}${NC}`;
93
+ console.log(` ${label} ${stats.total > 0 ? `${G}${stats.done}${NC}/${stats.total} done` : D + 'empty' + NC}`);
94
+ for (const t of day.tasks.slice(0, 5)) {
95
+ const icon = t.status === 'done' ? `${G}✓${NC}` : t.priority === 'high' || t.priority === 'critical' ? `${Y}●${NC}` : `${D}○${NC}`;
96
+ console.log(` ${icon} ${t.description}${t.due ? D + ' due ' + t.due + NC : ''}`);
97
+ }
98
+ if (day.tasks.length > 5) console.log(` ${D}+${day.tasks.length - 5} more${NC}`);
99
+ }
100
+ console.log('');
101
+ return;
102
+ }
103
+
104
+ fail(`Unknown: nha tasks ${sub}`);
105
+ info('Commands: list, add, done, edit, move, week');
106
+ }
107
+
108
+ function showTasks(date) {
109
+ const tasks = getTasks(date);
110
+ const dateStr = date || new Date().toISOString().split('T')[0];
111
+ const stats = getDayStats(date);
112
+
113
+ console.log(`\n ${BOLD}Tasks — ${dateStr}${NC} ${D}(${stats.done}/${stats.total} done, ${stats.completionRate}%)${NC}\n`);
114
+
115
+ if (tasks.length === 0) {
116
+ info('No tasks for today. Add one: nha tasks add "your task"');
117
+ console.log('');
118
+ return;
119
+ }
120
+
121
+ for (const t of tasks) {
122
+ const statusIcon = t.status === 'done' ? `${G}✓${NC}` : `${D}○${NC}`;
123
+ const priorityBadge = t.priority === 'critical' ? `${R}[!!]${NC}` :
124
+ t.priority === 'high' ? `${Y}[!]${NC}` : '';
125
+ const dueStr = t.due ? ` ${D}due ${t.due}${NC}` : '';
126
+ const sourceStr = t.source !== 'manual' ? ` ${D}(${t.source})${NC}` : '';
127
+ const strikethrough = t.status === 'done' ? D : '';
128
+
129
+ console.log(` ${statusIcon} ${strikethrough}#${t.id} ${t.description}${NC} ${priorityBadge}${dueStr}${sourceStr}`);
130
+ }
131
+ console.log('');
132
+ }
package/src/config.mjs CHANGED
@@ -48,6 +48,26 @@ const DEFAULT_CONFIG = {
48
48
  promptEvolutionEnabled: true,
49
49
  metaIntelligenceEnabled: true,
50
50
  },
51
+ google: {
52
+ clientId: '',
53
+ clientSecret: '',
54
+ },
55
+ ops: {
56
+ enabled: false,
57
+ planTime: '07:00',
58
+ summaryTime: '18:00',
59
+ pollIntervalMail: 300000,
60
+ pollIntervalCalendar: 900000,
61
+ meetingAlertMinutes: 30,
62
+ webhooks: {
63
+ telegram: '',
64
+ discord: '',
65
+ },
66
+ notifications: {
67
+ desktop: true,
68
+ terminal: true,
69
+ },
70
+ },
51
71
  };
52
72
 
53
73
  /**
@@ -152,6 +172,13 @@ export function setConfigValue(key, value) {
152
172
  'convergence': 'deliberation.convergence',
153
173
  'tribunal': 'deliberation.tribunalEnabled',
154
174
  'knowledge': 'features.knowledgeEnabled',
175
+ 'google-client-id': 'google.clientId',
176
+ 'google-client-secret': 'google.clientSecret',
177
+ 'plan-time': 'ops.planTime',
178
+ 'summary-time': 'ops.summaryTime',
179
+ 'meeting-alert': 'ops.meetingAlertMinutes',
180
+ 'telegram-webhook': 'ops.webhooks.telegram',
181
+ 'discord-webhook': 'ops.webhooks.discord',
155
182
  };
156
183
 
157
184
  const resolved = aliases[key] || key;
package/src/constants.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import os from 'os';
2
2
  import path from 'path';
3
3
 
4
- export const VERSION = '1.1.0';
4
+ export const VERSION = '2.1.0';
5
5
  export const BASE_URL = 'https://nothumanallowed.com/cli';
6
6
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
7
7
 
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Google Calendar API wrapper — zero dependencies.
3
+ * All calls via native fetch to Calendar REST API.
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { getAccessToken } from './token-store.mjs';
9
+ import { NHA_DIR } from '../constants.mjs';
10
+
11
+ const CAL_BASE = 'https://www.googleapis.com/calendar/v3';
12
+ const CAL_DIR = path.join(NHA_DIR, 'ops', 'calendar');
13
+
14
+ /** Authenticated fetch */
15
+ async function calFetch(config, urlPath, options = {}) {
16
+ const token = await getAccessToken(config);
17
+ const url = urlPath.startsWith('http') ? urlPath : `${CAL_BASE}${urlPath}`;
18
+
19
+ let res = await fetch(url, {
20
+ ...options,
21
+ headers: {
22
+ ...options.headers,
23
+ 'Authorization': `Bearer ${token}`,
24
+ },
25
+ });
26
+
27
+ if (res.status === 401) {
28
+ const newToken = await getAccessToken(config);
29
+ res = await fetch(url, {
30
+ ...options,
31
+ headers: { ...options.headers, 'Authorization': `Bearer ${newToken}` },
32
+ });
33
+ }
34
+
35
+ if (!res.ok) {
36
+ const err = await res.text();
37
+ throw new Error(`Calendar API ${res.status}: ${err}`);
38
+ }
39
+
40
+ return res.json();
41
+ }
42
+
43
+ /**
44
+ * List all calendars the user has access to.
45
+ */
46
+ export async function listCalendars(config) {
47
+ const data = await calFetch(config, '/users/me/calendarList');
48
+ return (data.items || []).map(c => ({
49
+ id: c.id,
50
+ summary: c.summary,
51
+ primary: c.primary || false,
52
+ timeZone: c.timeZone,
53
+ accessRole: c.accessRole,
54
+ }));
55
+ }
56
+
57
+ /**
58
+ * List events for a date range.
59
+ * @param {object} config
60
+ * @param {string} calendarId — 'primary' or specific ID
61
+ * @param {Date} timeMin
62
+ * @param {Date} timeMax
63
+ * @returns {Promise<Array>} parsed events
64
+ */
65
+ export async function listEvents(config, calendarId = 'primary', timeMin, timeMax) {
66
+ const params = new URLSearchParams({
67
+ timeMin: timeMin.toISOString(),
68
+ timeMax: timeMax.toISOString(),
69
+ singleEvents: 'true',
70
+ orderBy: 'startTime',
71
+ maxResults: '50',
72
+ });
73
+
74
+ const data = await calFetch(config, `/calendars/${encodeURIComponent(calendarId)}/events?${params}`);
75
+ const events = (data.items || []).map(parseEvent);
76
+
77
+ // Cache events
78
+ cacheEvents(timeMin, events);
79
+ return events;
80
+ }
81
+
82
+ /**
83
+ * Get today's events from all calendars.
84
+ */
85
+ export async function getTodayEvents(config) {
86
+ const now = new Date();
87
+ const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
88
+ const endOfDay = new Date(startOfDay.getTime() + 86400000);
89
+
90
+ const calendars = await listCalendars(config);
91
+ const allEvents = [];
92
+
93
+ for (const cal of calendars) {
94
+ if (cal.accessRole === 'freeBusyReader') continue; // skip minimal access
95
+ try {
96
+ const events = await listEvents(config, cal.id, startOfDay, endOfDay);
97
+ for (const e of events) {
98
+ e.calendarName = cal.summary;
99
+ allEvents.push(e);
100
+ }
101
+ } catch { /* skip failed calendars */ }
102
+ }
103
+
104
+ // Sort by start time
105
+ allEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
106
+ return allEvents;
107
+ }
108
+
109
+ /**
110
+ * Get events for a specific date.
111
+ */
112
+ export async function getEventsForDate(config, date) {
113
+ const d = new Date(date);
114
+ const startOfDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
115
+ const endOfDay = new Date(startOfDay.getTime() + 86400000);
116
+ return listEvents(config, 'primary', startOfDay, endOfDay);
117
+ }
118
+
119
+ /**
120
+ * Get upcoming events (next N hours).
121
+ * @param {number} hours — look-ahead hours (default 2)
122
+ */
123
+ export async function getUpcomingEvents(config, hours = 2) {
124
+ const now = new Date();
125
+ const end = new Date(now.getTime() + hours * 3600000);
126
+ return listEvents(config, 'primary', now, end);
127
+ }
128
+
129
+ /**
130
+ * Create a new event.
131
+ * @param {object} config
132
+ * @param {object} event — { summary, start, end, description, location, attendees }
133
+ * @param {string} calendarId
134
+ */
135
+ export async function createEvent(config, event, calendarId = 'primary') {
136
+ const body = {
137
+ summary: event.summary,
138
+ description: event.description || '',
139
+ location: event.location || '',
140
+ start: {
141
+ dateTime: new Date(event.start).toISOString(),
142
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
143
+ },
144
+ end: {
145
+ dateTime: new Date(event.end).toISOString(),
146
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
147
+ },
148
+ };
149
+
150
+ if (event.attendees?.length) {
151
+ body.attendees = event.attendees.map(email => ({ email }));
152
+ }
153
+
154
+ return calFetch(config, `/calendars/${encodeURIComponent(calendarId)}/events`, {
155
+ method: 'POST',
156
+ headers: { 'Content-Type': 'application/json' },
157
+ body: JSON.stringify(body),
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Update an existing event (partial).
163
+ */
164
+ export async function updateEvent(config, calendarId, eventId, patch) {
165
+ return calFetch(config, `/calendars/${encodeURIComponent(calendarId)}/events/${eventId}`, {
166
+ method: 'PATCH',
167
+ headers: { 'Content-Type': 'application/json' },
168
+ body: JSON.stringify(patch),
169
+ });
170
+ }
171
+
172
+ // ── Event Parser ───────────────────────────────────────────────────────────
173
+
174
+ function parseEvent(raw) {
175
+ const isAllDay = !!raw.start?.date;
176
+ return {
177
+ id: raw.id,
178
+ summary: raw.summary || '(no title)',
179
+ description: raw.description || '',
180
+ location: raw.location || '',
181
+ start: isAllDay ? raw.start.date : raw.start?.dateTime || '',
182
+ end: isAllDay ? raw.end.date : raw.end?.dateTime || '',
183
+ isAllDay,
184
+ status: raw.status || 'confirmed',
185
+ attendees: (raw.attendees || []).map(a => ({
186
+ email: a.email,
187
+ name: a.displayName || '',
188
+ responseStatus: a.responseStatus || 'needsAction',
189
+ self: a.self || false,
190
+ })),
191
+ organizer: raw.organizer?.email || '',
192
+ hangoutLink: raw.hangoutLink || '',
193
+ htmlLink: raw.htmlLink || '',
194
+ calendarName: '',
195
+ };
196
+ }
197
+
198
+ // ── Local Cache ────────────────────────────────────────────────────────────
199
+
200
+ function cacheEvents(date, events) {
201
+ fs.mkdirSync(CAL_DIR, { recursive: true });
202
+ const dateStr = date instanceof Date ? date.toISOString().split('T')[0] : String(date);
203
+ const file = path.join(CAL_DIR, `events-${dateStr}.json`);
204
+ fs.writeFileSync(file, JSON.stringify(events, null, 2), { mode: 0o600 });
205
+ }
206
+
207
+ /**
208
+ * Load cached events for a date.
209
+ */
210
+ export function loadCachedEvents(date) {
211
+ const dateStr = date instanceof Date ? date.toISOString().split('T')[0] : String(date);
212
+ const file = path.join(CAL_DIR, `events-${dateStr}.json`);
213
+ if (!fs.existsSync(file)) return null;
214
+ try { return JSON.parse(fs.readFileSync(file, 'utf-8')); }
215
+ catch { return null; }
216
+ }