nothumanallowed 6.7.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "6.7.0",
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": {
@@ -691,6 +691,34 @@ export async function cmdUI(args) {
691
691
  return;
692
692
  }
693
693
 
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
+
694
722
  // GET /api/emails?filter=unread|all (default: all inbox)
695
723
  if (method === 'GET' && pathname === '/api/emails') {
696
724
  try {
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.7.0';
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
 
@@ -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?' &middot; '+esc(f.size):'')+(f.shared?' &middot; Shared':'')+(f.starred?' &#9733;':'')+'</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:'&#128193;',doc:'&#128196;',sheet:'&#128202;',slides:'&#127916;',pdf:'&#128213;',image:'&#127748;',video:'&#127910;',audio:'&#127925;',archive:'&#128230;',text:'&#128221;',file:'&#128196;'};
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">&#9993;</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">&#128197;</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">&#128193;</span> Drive</div>
1064
1139
  </div>
1065
1140
  <div class="sidebar__section">
1066
1141
  <div class="sidebar__label">AI</div>