nothumanallowed 6.6.3 → 6.8.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/package.json +1 -1
- package/src/commands/chat.mjs +27 -13
- package/src/commands/ui.mjs +74 -15
- package/src/constants.mjs +1 -1
- package/src/services/google-drive.mjs +168 -0
- package/src/services/google-oauth.mjs +2 -0
- package/src/services/web-ui.mjs +76 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents for security, code, DevOps, data & daily ops. Per-agent memory, Telegram + Discord auto-responder, proactive intelligence daemon, voice chat, plugin system.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/chat.mjs
CHANGED
|
@@ -133,11 +133,11 @@ TOOLS:
|
|
|
133
133
|
Update ANY field of an existing calendar event: title, location, description, start time, end time.
|
|
134
134
|
You MUST call calendar_find first to get the eventId. Only include fields that need to change. ALWAYS confirm before updating.
|
|
135
135
|
|
|
136
|
-
21. gmail_mark_read(
|
|
137
|
-
Mark
|
|
136
|
+
21. gmail_mark_read(all?: boolean, count?: number, messageId?: string)
|
|
137
|
+
Mark emails as read. Options: all=true marks ALL unread. count=5 marks the last 5 unread. messageId marks one specific email.
|
|
138
138
|
|
|
139
|
-
22. gmail_mark_unread(messageId
|
|
140
|
-
Mark
|
|
139
|
+
22. gmail_mark_unread(count?: number, messageId?: string)
|
|
140
|
+
Mark emails as unread. count=5 marks the last 5 read emails as unread. messageId marks one specific email.
|
|
141
141
|
|
|
142
142
|
23. gmail_archive(messageId: string)
|
|
143
143
|
Archive a specific email (removes from inbox).
|
|
@@ -434,23 +434,37 @@ async function executeTool(action, params, config) {
|
|
|
434
434
|
|
|
435
435
|
// ── Gmail Mark Read/Unread/Archive ──────────────────────────────────
|
|
436
436
|
case 'gmail_mark_read': {
|
|
437
|
+
const gm = await import('../services/google-gmail.mjs');
|
|
437
438
|
if (params.all) {
|
|
438
|
-
const
|
|
439
|
-
const result = await markAllAsRead(config);
|
|
439
|
+
const result = await gm.markAllAsRead(config);
|
|
440
440
|
return `Done! ${result.count} email${result.count !== 1 ? 's' : ''} marked as read.`;
|
|
441
441
|
}
|
|
442
|
+
if (params.count) {
|
|
443
|
+
const msgs = await gm.listMessages(config, 'is:unread', params.count);
|
|
444
|
+
if (msgs.length === 0) return 'No unread emails found.';
|
|
445
|
+
for (const m of msgs) { await gm.markAsRead(config, m.id); }
|
|
446
|
+
return `Done! ${msgs.length} email${msgs.length !== 1 ? 's' : ''} marked as read.`;
|
|
447
|
+
}
|
|
442
448
|
if (params.messageId) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return `Email ${params.messageId} marked as read.`;
|
|
449
|
+
await gm.markAsRead(config, params.messageId);
|
|
450
|
+
return 'Email marked as read.';
|
|
446
451
|
}
|
|
447
|
-
return 'Specify
|
|
452
|
+
return 'Specify all=true, count=N, or a messageId.';
|
|
448
453
|
}
|
|
449
454
|
|
|
450
455
|
case 'gmail_mark_unread': {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
456
|
+
const gm = await import('../services/google-gmail.mjs');
|
|
457
|
+
if (params.count) {
|
|
458
|
+
const msgs = await gm.listMessages(config, 'in:inbox -is:unread', params.count);
|
|
459
|
+
if (msgs.length === 0) return 'No read emails found to mark as unread.';
|
|
460
|
+
for (const m of msgs) { await gm.markAsUnread(config, m.id); }
|
|
461
|
+
return `Done! ${msgs.length} email${msgs.length !== 1 ? 's' : ''} marked as unread.`;
|
|
462
|
+
}
|
|
463
|
+
if (params.messageId) {
|
|
464
|
+
await gm.markAsUnread(config, params.messageId);
|
|
465
|
+
return 'Email marked as unread.';
|
|
466
|
+
}
|
|
467
|
+
return 'Specify count=N or a messageId.';
|
|
454
468
|
}
|
|
455
469
|
|
|
456
470
|
case 'gmail_archive': {
|
package/src/commands/ui.mjs
CHANGED
|
@@ -98,11 +98,11 @@ TOOLS:
|
|
|
98
98
|
16. calendar_update(eventId: string, summary?: string, location?: string, description?: string, start?: string, end?: string)
|
|
99
99
|
Update ANY field of an existing calendar event. Only include fields that need to change. Confirm with the user first.
|
|
100
100
|
|
|
101
|
-
17. gmail_mark_read(
|
|
102
|
-
Mark
|
|
101
|
+
17. gmail_mark_read(all?: boolean, count?: number, messageId?: string)
|
|
102
|
+
Mark emails as read. Options: all=true marks ALL unread. count=5 marks the last 5. messageId marks one specific email.
|
|
103
103
|
|
|
104
|
-
18. gmail_mark_unread(messageId
|
|
105
|
-
Mark
|
|
104
|
+
18. gmail_mark_unread(count?: number, messageId?: string)
|
|
105
|
+
Mark emails as unread. count=5 marks the last 5 read emails as unread. messageId marks one specific email.
|
|
106
106
|
|
|
107
107
|
19. gmail_archive(messageId: string)
|
|
108
108
|
Archive a specific email (removes from inbox).
|
|
@@ -361,22 +361,38 @@ async function executeTool(action, params, config) {
|
|
|
361
361
|
return `Event updated successfully (${changes}). ${params.location ? 'New location: ' + params.location : ''}`;
|
|
362
362
|
}
|
|
363
363
|
case 'gmail_mark_read': {
|
|
364
|
+
const gm = await import('../services/google-gmail.mjs');
|
|
365
|
+
const cfg = (await import('../config.mjs')).loadConfig();
|
|
364
366
|
if (params.all) {
|
|
365
|
-
const
|
|
366
|
-
const result = await markAllAsRead(config);
|
|
367
|
+
const result = await gm.markAllAsRead(cfg);
|
|
367
368
|
return `Done! ${result.count} email${result.count !== 1 ? 's' : ''} marked as read.`;
|
|
368
369
|
}
|
|
370
|
+
if (params.count) {
|
|
371
|
+
const msgs = await gm.listMessages(cfg, 'is:unread', params.count);
|
|
372
|
+
if (msgs.length === 0) return 'No unread emails found.';
|
|
373
|
+
for (const m of msgs) { await gm.markAsRead(cfg, m.id); }
|
|
374
|
+
return `Done! ${msgs.length} email${msgs.length !== 1 ? 's' : ''} marked as read.`;
|
|
375
|
+
}
|
|
369
376
|
if (params.messageId) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return `Email marked as read.`;
|
|
377
|
+
await gm.markAsRead(cfg, params.messageId);
|
|
378
|
+
return 'Email marked as read.';
|
|
373
379
|
}
|
|
374
|
-
return 'Specify
|
|
380
|
+
return 'Specify all=true, count=N, or a messageId.';
|
|
375
381
|
}
|
|
376
382
|
case 'gmail_mark_unread': {
|
|
377
|
-
const
|
|
378
|
-
await
|
|
379
|
-
|
|
383
|
+
const gm = await import('../services/google-gmail.mjs');
|
|
384
|
+
const cfg = (await import('../config.mjs')).loadConfig();
|
|
385
|
+
if (params.count) {
|
|
386
|
+
const msgs = await gm.listMessages(cfg, 'in:inbox -is:unread', params.count);
|
|
387
|
+
if (msgs.length === 0) return 'No read emails found to mark as unread.';
|
|
388
|
+
for (const m of msgs) { await gm.markAsUnread(cfg, m.id); }
|
|
389
|
+
return `Done! ${msgs.length} email${msgs.length !== 1 ? 's' : ''} marked as unread.`;
|
|
390
|
+
}
|
|
391
|
+
if (params.messageId) {
|
|
392
|
+
await gm.markAsUnread(cfg, params.messageId);
|
|
393
|
+
return 'Email marked as unread.';
|
|
394
|
+
}
|
|
395
|
+
return 'Specify count=N or a messageId.';
|
|
380
396
|
}
|
|
381
397
|
case 'gmail_archive': {
|
|
382
398
|
const { archiveMessage } = await import('../services/mail-router.mjs');
|
|
@@ -675,10 +691,53 @@ export async function cmdUI(args) {
|
|
|
675
691
|
return;
|
|
676
692
|
}
|
|
677
693
|
|
|
678
|
-
// GET /api/
|
|
694
|
+
// GET /api/drive — list recent Drive files
|
|
695
|
+
if (method === 'GET' && pathname === '/api/drive') {
|
|
696
|
+
try {
|
|
697
|
+
const gd = await import('../services/google-drive.mjs');
|
|
698
|
+
const filter = url.searchParams.get('filter');
|
|
699
|
+
const search = url.searchParams.get('q');
|
|
700
|
+
let files;
|
|
701
|
+
if (search) {
|
|
702
|
+
files = await gd.searchFiles(config, search, 20);
|
|
703
|
+
} else if (filter === 'starred') {
|
|
704
|
+
files = await gd.getStarredFiles(config, 20);
|
|
705
|
+
} else if (filter === 'shared') {
|
|
706
|
+
files = await gd.getSharedFiles(config, 20);
|
|
707
|
+
} else if (filter === 'recent') {
|
|
708
|
+
files = await gd.getRecentFiles(config, 15);
|
|
709
|
+
} else {
|
|
710
|
+
files = await gd.listFiles(config, 30);
|
|
711
|
+
}
|
|
712
|
+
let quota = null;
|
|
713
|
+
try { quota = await gd.getStorageQuota(config); } catch {}
|
|
714
|
+
sendJSON(res, 200, { files, quota });
|
|
715
|
+
} catch (e) {
|
|
716
|
+
sendJSON(res, 200, { files: [], error: e.message });
|
|
717
|
+
}
|
|
718
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// GET /api/emails?filter=unread|all (default: all inbox)
|
|
679
723
|
if (method === 'GET' && pathname === '/api/emails') {
|
|
680
724
|
try {
|
|
681
|
-
const
|
|
725
|
+
const filter = url.searchParams.get('filter');
|
|
726
|
+
let emails;
|
|
727
|
+
if (filter === 'unread') {
|
|
728
|
+
emails = await getUnreadImportant(config, 20);
|
|
729
|
+
} else {
|
|
730
|
+
// Show all recent inbox emails (read + unread)
|
|
731
|
+
const gm = await import('../services/google-gmail.mjs');
|
|
732
|
+
const msgRefs = await gm.listMessages(config, 'in:inbox', 30);
|
|
733
|
+
emails = [];
|
|
734
|
+
for (const ref of msgRefs.slice(0, 30)) {
|
|
735
|
+
try {
|
|
736
|
+
const msg = await gm.getMessage(config, ref.id);
|
|
737
|
+
emails.push(msg);
|
|
738
|
+
} catch { /* skip */ }
|
|
739
|
+
}
|
|
740
|
+
}
|
|
682
741
|
sendJSON(res, 200, { emails });
|
|
683
742
|
} catch (e) {
|
|
684
743
|
sendJSON(res, 200, { emails: [], error: e.message });
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
export const VERSION = '6.
|
|
8
|
+
export const VERSION = '6.8.0';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Drive API wrapper — zero dependencies.
|
|
3
|
+
* All calls via native fetch to Drive REST API v3.
|
|
4
|
+
* Auto-refreshes tokens on 401.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getAccessToken } from './token-store.mjs';
|
|
8
|
+
|
|
9
|
+
const DRIVE_BASE = 'https://www.googleapis.com/drive/v3';
|
|
10
|
+
|
|
11
|
+
/** Authenticated fetch with auto-retry on 401 */
|
|
12
|
+
async function driveFetch(config, urlPath, options = {}) {
|
|
13
|
+
const token = await getAccessToken(config);
|
|
14
|
+
const url = urlPath.startsWith('http') ? urlPath : `${DRIVE_BASE}${urlPath}`;
|
|
15
|
+
|
|
16
|
+
let res = await fetch(url, {
|
|
17
|
+
...options,
|
|
18
|
+
headers: {
|
|
19
|
+
'Authorization': `Bearer ${token}`,
|
|
20
|
+
...options.headers,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (res.status === 401) {
|
|
25
|
+
const newToken = await getAccessToken(config);
|
|
26
|
+
res = await fetch(url, {
|
|
27
|
+
...options,
|
|
28
|
+
headers: {
|
|
29
|
+
...options.headers,
|
|
30
|
+
'Authorization': `Bearer ${newToken}`,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const err = await res.text();
|
|
37
|
+
throw new Error(`Drive API ${res.status}: ${err}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return res.json();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* List recent files.
|
|
45
|
+
* @param {object} config
|
|
46
|
+
* @param {number} maxResults
|
|
47
|
+
* @param {string} query — Drive search query (e.g., "mimeType='application/pdf'")
|
|
48
|
+
* @returns {Promise<Array>}
|
|
49
|
+
*/
|
|
50
|
+
export async function listFiles(config, maxResults = 20, query = '') {
|
|
51
|
+
const params = new URLSearchParams({
|
|
52
|
+
pageSize: String(maxResults),
|
|
53
|
+
fields: 'files(id,name,mimeType,size,modifiedTime,webViewLink,iconLink,owners,shared,starred)',
|
|
54
|
+
orderBy: 'modifiedTime desc',
|
|
55
|
+
});
|
|
56
|
+
if (query) params.set('q', query);
|
|
57
|
+
|
|
58
|
+
const data = await driveFetch(config, `/files?${params}`);
|
|
59
|
+
return (data.files || []).map(parseFile);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Search files by name or content.
|
|
64
|
+
*/
|
|
65
|
+
export async function searchFiles(config, searchTerm, maxResults = 20) {
|
|
66
|
+
const q = `name contains '${searchTerm.replace(/'/g, "\\'")}'`;
|
|
67
|
+
return listFiles(config, maxResults, q);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get file metadata.
|
|
72
|
+
*/
|
|
73
|
+
export async function getFile(config, fileId) {
|
|
74
|
+
const params = new URLSearchParams({
|
|
75
|
+
fields: 'id,name,mimeType,size,modifiedTime,createdTime,webViewLink,webContentLink,iconLink,owners,shared,starred,description,parents',
|
|
76
|
+
});
|
|
77
|
+
const data = await driveFetch(config, `/files/${fileId}?${params}`);
|
|
78
|
+
return parseFile(data);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* List files in a specific folder.
|
|
83
|
+
*/
|
|
84
|
+
export async function listFolder(config, folderId = 'root', maxResults = 30) {
|
|
85
|
+
const q = `'${folderId}' in parents and trashed = false`;
|
|
86
|
+
return listFiles(config, maxResults, q);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get storage quota.
|
|
91
|
+
*/
|
|
92
|
+
export async function getStorageQuota(config) {
|
|
93
|
+
const data = await driveFetch(config, '/about?fields=storageQuota,user');
|
|
94
|
+
const q = data.storageQuota || {};
|
|
95
|
+
return {
|
|
96
|
+
limit: formatBytes(parseInt(q.limit || '0')),
|
|
97
|
+
usage: formatBytes(parseInt(q.usage || '0')),
|
|
98
|
+
usageInDrive: formatBytes(parseInt(q.usageInDrive || '0')),
|
|
99
|
+
usageInTrash: formatBytes(parseInt(q.usageInTrash || '0')),
|
|
100
|
+
percentUsed: q.limit ? Math.round((parseInt(q.usage || '0') / parseInt(q.limit)) * 100) : 0,
|
|
101
|
+
user: data.user?.displayName || '',
|
|
102
|
+
email: data.user?.emailAddress || '',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* List recently modified files.
|
|
108
|
+
*/
|
|
109
|
+
export async function getRecentFiles(config, maxResults = 10) {
|
|
110
|
+
return listFiles(config, maxResults, 'modifiedTime > \'' + new Date(Date.now() - 7 * 86400000).toISOString() + '\'');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* List starred files.
|
|
115
|
+
*/
|
|
116
|
+
export async function getStarredFiles(config, maxResults = 20) {
|
|
117
|
+
return listFiles(config, maxResults, 'starred = true');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* List shared with me.
|
|
122
|
+
*/
|
|
123
|
+
export async function getSharedFiles(config, maxResults = 20) {
|
|
124
|
+
return listFiles(config, maxResults, 'sharedWithMe = true');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
function parseFile(raw) {
|
|
130
|
+
return {
|
|
131
|
+
id: raw.id,
|
|
132
|
+
name: raw.name || '(untitled)',
|
|
133
|
+
mimeType: raw.mimeType || '',
|
|
134
|
+
type: mimeToType(raw.mimeType || ''),
|
|
135
|
+
size: raw.size ? formatBytes(parseInt(raw.size)) : '',
|
|
136
|
+
modifiedTime: raw.modifiedTime || '',
|
|
137
|
+
createdTime: raw.createdTime || '',
|
|
138
|
+
webViewLink: raw.webViewLink || '',
|
|
139
|
+
webContentLink: raw.webContentLink || '',
|
|
140
|
+
iconLink: raw.iconLink || '',
|
|
141
|
+
owner: raw.owners?.[0]?.displayName || '',
|
|
142
|
+
shared: raw.shared || false,
|
|
143
|
+
starred: raw.starred || false,
|
|
144
|
+
description: raw.description || '',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function mimeToType(mime) {
|
|
149
|
+
if (mime.includes('folder')) return 'folder';
|
|
150
|
+
if (mime.includes('document')) return 'doc';
|
|
151
|
+
if (mime.includes('spreadsheet')) return 'sheet';
|
|
152
|
+
if (mime.includes('presentation')) return 'slides';
|
|
153
|
+
if (mime.includes('pdf')) return 'pdf';
|
|
154
|
+
if (mime.includes('image')) return 'image';
|
|
155
|
+
if (mime.includes('video')) return 'video';
|
|
156
|
+
if (mime.includes('audio')) return 'audio';
|
|
157
|
+
if (mime.includes('zip') || mime.includes('compressed')) return 'archive';
|
|
158
|
+
if (mime.includes('text') || mime.includes('json') || mime.includes('xml')) return 'text';
|
|
159
|
+
return 'file';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatBytes(bytes) {
|
|
163
|
+
if (bytes === 0) return '0 B';
|
|
164
|
+
const k = 1024;
|
|
165
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
166
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
167
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
168
|
+
}
|
|
@@ -20,6 +20,8 @@ const SCOPES = [
|
|
|
20
20
|
'https://www.googleapis.com/auth/gmail.labels',
|
|
21
21
|
'https://www.googleapis.com/auth/calendar.readonly',
|
|
22
22
|
'https://www.googleapis.com/auth/calendar.events',
|
|
23
|
+
'https://www.googleapis.com/auth/drive.readonly',
|
|
24
|
+
'https://www.googleapis.com/auth/drive.metadata.readonly',
|
|
23
25
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
24
26
|
].join(' ');
|
|
25
27
|
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -194,7 +194,7 @@ function switchView(v) {
|
|
|
194
194
|
document.querySelectorAll('.nav-item').forEach(function(el){
|
|
195
195
|
if(el.dataset.view===v){el.classList.add('nav-item--active')}else{el.classList.remove('nav-item--active')}
|
|
196
196
|
});
|
|
197
|
-
var titles = {dashboard:'Dashboard',chat:'Chat',plan:'Daily Plan',tasks:'Tasks',emails:'Emails',calendar:'Calendar',agents:'Agents',settings:'Settings'};
|
|
197
|
+
var titles = {dashboard:'Dashboard',chat:'Chat',plan:'Daily Plan',tasks:'Tasks',emails:'Emails',calendar:'Calendar',drive:'Drive',agents:'Agents',settings:'Settings'};
|
|
198
198
|
document.getElementById('headerTitle').textContent = titles[v]||v;
|
|
199
199
|
closeSidebar();
|
|
200
200
|
render();
|
|
@@ -256,6 +256,7 @@ function render(){
|
|
|
256
256
|
case 'tasks':renderTasks(el);break;
|
|
257
257
|
case 'emails':renderEmails(el);break;
|
|
258
258
|
case 'calendar':renderCalendar(el);break;
|
|
259
|
+
case 'drive':renderDrive(el);break;
|
|
259
260
|
case 'agents':renderAgents(el);break;
|
|
260
261
|
case 'settings':renderSettings(el);break;
|
|
261
262
|
}
|
|
@@ -607,6 +608,79 @@ var AGENT_ICONS = {
|
|
|
607
608
|
prometheus:'\\u{1F525}',cassandra:'\\u26A0',athena:'\\u{1F9E0}',sauron:'\\u{1F441}',conductor:'\\u{1F3BC}',
|
|
608
609
|
navi:'\\u{1F9ED}',edi:'\\u{1F4C8}',tempest:'\\u26C8',epicure:'\\u{1F37D}'
|
|
609
610
|
};
|
|
611
|
+
// ---- DRIVE ----
|
|
612
|
+
var driveData=null;
|
|
613
|
+
var driveFilter='';
|
|
614
|
+
function renderDrive(el){
|
|
615
|
+
if(!driveData){
|
|
616
|
+
el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading Drive...</div></div>';
|
|
617
|
+
apiGet('/api/drive').then(function(r){driveData=r||{files:[]};renderDrive(el)}).catch(function(){
|
|
618
|
+
el.innerHTML='<div class="card" style="color:var(--red);padding:20px">Could not load Drive. Run <b>nha google revoke</b> then <b>nha google auth</b> to grant Drive permissions.</div>';
|
|
619
|
+
});
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
var files=driveData.files||[];
|
|
623
|
+
var quota=driveData.quota;
|
|
624
|
+
|
|
625
|
+
var h='';
|
|
626
|
+
|
|
627
|
+
// Quota bar
|
|
628
|
+
if(quota){
|
|
629
|
+
h+='<div class="card" style="margin-bottom:12px;padding:12px"><div style="display:flex;justify-content:space-between;margin-bottom:6px"><span style="color:var(--bright);font-size:12px">'+esc(quota.usage)+' of '+esc(quota.limit)+' used</span><span style="color:var(--dim);font-size:11px">'+quota.percentUsed+'%</span></div>';
|
|
630
|
+
h+='<div style="height:6px;background:var(--bg);border-radius:3px;overflow:hidden"><div style="height:100%;width:'+Math.min(quota.percentUsed,100)+'%;background:'+( quota.percentUsed>90?'var(--red)':quota.percentUsed>70?'var(--amber)':'var(--green)')+';border-radius:3px"></div></div></div>';
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Filter bar
|
|
634
|
+
h+='<div style="display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap">';
|
|
635
|
+
['','recent','starred','shared'].forEach(function(f){
|
|
636
|
+
var label=f||'All Files';
|
|
637
|
+
var active=driveFilter===f;
|
|
638
|
+
h+='<button onclick="filterDrive(\\x27'+f+'\\x27)" style="padding:6px 14px;border-radius:6px;font-size:11px;background:'+(active?'var(--green3)':'var(--bg3)')+';color:'+(active?'var(--bg)':'var(--dim)')+';border:1px solid '+(active?'var(--green)':'var(--border)')+'">'+esc(label.charAt(0).toUpperCase()+label.slice(1))+'</button>';
|
|
639
|
+
});
|
|
640
|
+
h+='<input type="text" id="driveSearch" placeholder="Search files..." style="flex:1;min-width:120px;font-size:11px;padding:6px 10px" onkeydown="if(event.key===\\x27Enter\\x27)searchDrive()">';
|
|
641
|
+
h+='</div>';
|
|
642
|
+
|
|
643
|
+
// File list
|
|
644
|
+
if(files.length===0){
|
|
645
|
+
h+='<div class="card" style="text-align:center;color:var(--dim);padding:30px">No files found</div>';
|
|
646
|
+
} else {
|
|
647
|
+
files.forEach(function(f){
|
|
648
|
+
var icon=driveTypeIcon(f.type);
|
|
649
|
+
var date=f.modifiedTime?new Date(f.modifiedTime).toLocaleDateString():'';
|
|
650
|
+
h+='<div class="card" style="margin-bottom:6px;padding:10px;cursor:pointer" onclick="window.open(\\x27'+esc(f.webViewLink)+'\\x27,\\x27_blank\\x27)">';
|
|
651
|
+
h+='<div style="display:flex;align-items:center;gap:10px">';
|
|
652
|
+
h+='<span style="font-size:20px">'+icon+'</span>';
|
|
653
|
+
h+='<div style="flex:1;min-width:0">';
|
|
654
|
+
h+='<div style="color:var(--bright);font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(f.name)+'</div>';
|
|
655
|
+
h+='<div style="color:var(--dim);font-size:10px">'+esc(date)+(f.size?' · '+esc(f.size):'')+(f.shared?' · Shared':'')+(f.starred?' ★':'')+'</div>';
|
|
656
|
+
h+='</div>';
|
|
657
|
+
h+='<span style="color:var(--dim);font-size:10px">'+esc(f.type)+'</span>';
|
|
658
|
+
h+='</div></div>';
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
el.innerHTML=h;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function driveTypeIcon(type){
|
|
666
|
+
var icons={folder:'📁',doc:'📄',sheet:'📊',slides:'🎬',pdf:'📕',image:'🌄',video:'🎦',audio:'🎵',archive:'📦',text:'📝',file:'📄'};
|
|
667
|
+
return icons[type]||icons.file;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function filterDrive(f){
|
|
671
|
+
driveFilter=f;
|
|
672
|
+
driveData=null;
|
|
673
|
+
var params=f?'?filter='+f:'';
|
|
674
|
+
apiGet('/api/drive'+params).then(function(r){driveData=r||{files:[]};renderDrive(document.getElementById('content'))});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function searchDrive(){
|
|
678
|
+
var inp=document.getElementById('driveSearch');
|
|
679
|
+
if(!inp||!inp.value.trim())return;
|
|
680
|
+
driveData=null;
|
|
681
|
+
apiGet('/api/drive?q='+encodeURIComponent(inp.value.trim())).then(function(r){driveData=r||{files:[]};renderDrive(document.getElementById('content'))});
|
|
682
|
+
}
|
|
683
|
+
|
|
610
684
|
function renderAgents(el){
|
|
611
685
|
if(agentsList.length===0){el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading agents...</div></div>';loadAgents().then(function(){renderAgents(el)});return}
|
|
612
686
|
|
|
@@ -1061,6 +1135,7 @@ init();
|
|
|
1061
1135
|
<div class="sidebar__label">Data</div>
|
|
1062
1136
|
<div class="nav-item" data-view="emails" onclick="switchView('emails')"><span class="nav-item__icon">✉</span> Emails <span class="nav-item__badge" id="emailBadge" style="display:none">0</span></div>
|
|
1063
1137
|
<div class="nav-item" data-view="calendar" onclick="switchView('calendar')"><span class="nav-item__icon">📅</span> Calendar <span class="nav-item__badge" id="calBadge" style="display:none;background:var(--amber)">0</span></div>
|
|
1138
|
+
<div class="nav-item" data-view="drive" onclick="switchView('drive')"><span class="nav-item__icon">📁</span> Drive</div>
|
|
1064
1139
|
</div>
|
|
1065
1140
|
<div class="sidebar__section">
|
|
1066
1141
|
<div class="sidebar__label">AI</div>
|