nothumanallowed 11.6.2 → 12.0.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,7 +1,7 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "11.6.2",
4
- "description": "NotHumanAllowed — 38 AI agents, 70 tools. Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
3
+ "version": "12.0.0",
4
+ "description": "NotHumanAllowed — 38 AI agents, 80 tools. Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "nha": "./bin/nha.mjs",
@@ -800,6 +800,79 @@ export async function cmdUI(args) {
800
800
  return;
801
801
  }
802
802
 
803
+ // GET /api/drive/read/:fileId — read file as text
804
+ if (method === 'GET' && pathname.startsWith('/api/drive/read/')) {
805
+ const fileId = pathname.split('/api/drive/read/')[1];
806
+ try {
807
+ const gd = await import('../services/google-drive.mjs');
808
+ const content = await gd.readFileAsText(config, fileId);
809
+ sendJSON(res, 200, { content });
810
+ } catch (e) {
811
+ sendJSON(res, 500, { error: e.message });
812
+ }
813
+ logRequest(method, pathname, 200, Date.now() - start);
814
+ return;
815
+ }
816
+
817
+ // GET /api/drive/download/:fileId — download file as base64
818
+ if (method === 'GET' && pathname.startsWith('/api/drive/download/')) {
819
+ const fileId = pathname.split('/api/drive/download/')[1];
820
+ try {
821
+ const gd = await import('../services/google-drive.mjs');
822
+ const dl = await gd.downloadFileContent(config, fileId);
823
+ sendJSON(res, 200, dl);
824
+ } catch (e) {
825
+ sendJSON(res, 500, { error: e.message });
826
+ }
827
+ logRequest(method, pathname, 200, Date.now() - start);
828
+ return;
829
+ }
830
+
831
+ // POST /api/drive/update/:fileId — update file content
832
+ if (method === 'POST' && pathname.startsWith('/api/drive/update/')) {
833
+ const fileId = pathname.split('/api/drive/update/')[1];
834
+ try {
835
+ const body = await readBody(req);
836
+ const gd = await import('../services/google-drive.mjs');
837
+ const result = await gd.updateFileContent(config, fileId, body.content || '');
838
+ sendJSON(res, 200, result);
839
+ } catch (e) {
840
+ sendJSON(res, 500, { error: e.message });
841
+ }
842
+ logRequest(method, pathname, 200, Date.now() - start);
843
+ return;
844
+ }
845
+
846
+ // POST /api/drive/upload — upload new file
847
+ if (method === 'POST' && pathname === '/api/drive/upload') {
848
+ try {
849
+ const body = await readBody(req);
850
+ const gd = await import('../services/google-drive.mjs');
851
+ let content = body.content || '';
852
+ if (body.encoding === 'base64') content = Buffer.from(content, 'base64');
853
+ const result = await gd.uploadFile(config, body.name, content, body.mimeType || 'text/plain', body.folderId || 'root');
854
+ sendJSON(res, 200, result);
855
+ } catch (e) {
856
+ sendJSON(res, 500, { error: e.message });
857
+ }
858
+ logRequest(method, pathname, 200, Date.now() - start);
859
+ return;
860
+ }
861
+
862
+ // POST /api/drive/delete/:fileId — trash file
863
+ if (method === 'POST' && pathname.startsWith('/api/drive/delete/')) {
864
+ const fileId = pathname.split('/api/drive/delete/')[1];
865
+ try {
866
+ const gd = await import('../services/google-drive.mjs');
867
+ await gd.trashFile(config, fileId);
868
+ sendJSON(res, 200, { ok: true });
869
+ } catch (e) {
870
+ sendJSON(res, 500, { error: e.message });
871
+ }
872
+ logRequest(method, pathname, 200, Date.now() - start);
873
+ return;
874
+ }
875
+
803
876
  // GET /api/emails?page=0&pageSize=25&filter=unread|all
804
877
  if (method === 'GET' && pathname === '/api/emails') {
805
878
  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 = '11.6.2';
8
+ export const VERSION = '12.0.0';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -165,6 +165,147 @@ export async function downloadFileContent(config, fileId) {
165
165
  };
166
166
  }
167
167
 
168
+ /**
169
+ * Upload a file to Google Drive.
170
+ * @param {object} config
171
+ * @param {string} name — filename
172
+ * @param {string|Buffer} content — file content (string for text, Buffer for binary)
173
+ * @param {string} mimeType — e.g. 'text/plain', 'application/pdf'
174
+ * @param {string} folderId — parent folder ID (default: root)
175
+ * @returns {Promise<object>} uploaded file metadata
176
+ */
177
+ export async function uploadFile(config, name, content, mimeType = 'text/plain', folderId = 'root') {
178
+ const token = await getAccessToken(config);
179
+ const metadata = {
180
+ name,
181
+ parents: [folderId],
182
+ };
183
+
184
+ // Multipart upload
185
+ const boundary = '===NHA_UPLOAD_BOUNDARY===';
186
+ const body = [
187
+ `--${boundary}`,
188
+ 'Content-Type: application/json; charset=UTF-8',
189
+ '',
190
+ JSON.stringify(metadata),
191
+ `--${boundary}`,
192
+ `Content-Type: ${mimeType}`,
193
+ 'Content-Transfer-Encoding: base64',
194
+ '',
195
+ Buffer.from(content).toString('base64'),
196
+ `--${boundary}--`,
197
+ ].join('\r\n');
198
+
199
+ const res = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,mimeType,size,webViewLink', {
200
+ method: 'POST',
201
+ headers: {
202
+ 'Authorization': `Bearer ${token}`,
203
+ 'Content-Type': `multipart/related; boundary=${boundary}`,
204
+ },
205
+ body,
206
+ });
207
+
208
+ if (!res.ok) {
209
+ const err = await res.text();
210
+ throw new Error(`Drive upload ${res.status}: ${err.slice(0, 200)}`);
211
+ }
212
+
213
+ return parseFile(await res.json());
214
+ }
215
+
216
+ /**
217
+ * Update (overwrite) file content on Drive.
218
+ */
219
+ export async function updateFileContent(config, fileId, content, mimeType = 'text/plain') {
220
+ const token = await getAccessToken(config);
221
+ const res = await fetch(`https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=media&fields=id,name,mimeType,size,webViewLink`, {
222
+ method: 'PATCH',
223
+ headers: {
224
+ 'Authorization': `Bearer ${token}`,
225
+ 'Content-Type': mimeType,
226
+ },
227
+ body: content,
228
+ });
229
+
230
+ if (!res.ok) {
231
+ const err = await res.text();
232
+ throw new Error(`Drive update ${res.status}: ${err.slice(0, 200)}`);
233
+ }
234
+
235
+ return parseFile(await res.json());
236
+ }
237
+
238
+ /**
239
+ * Delete a file (move to trash).
240
+ */
241
+ export async function trashFile(config, fileId) {
242
+ const token = await getAccessToken(config);
243
+ const res = await fetch(`${DRIVE_BASE}/files/${fileId}`, {
244
+ method: 'PATCH',
245
+ headers: {
246
+ 'Authorization': `Bearer ${token}`,
247
+ 'Content-Type': 'application/json',
248
+ },
249
+ body: JSON.stringify({ trashed: true }),
250
+ });
251
+
252
+ if (!res.ok) {
253
+ const err = await res.text();
254
+ throw new Error(`Drive trash ${res.status}: ${err.slice(0, 200)}`);
255
+ }
256
+
257
+ return { ok: true };
258
+ }
259
+
260
+ /**
261
+ * Read text content of a file directly (for text files, code, docs).
262
+ * For Google Docs, exports as plain text. For other text files, downloads raw.
263
+ * @returns {Promise<string>} file content as text
264
+ */
265
+ export async function readFileAsText(config, fileId) {
266
+ const token = await getAccessToken(config);
267
+ const meta = await getFile(config, fileId);
268
+
269
+ let downloadUrl;
270
+
271
+ // Google Workspace files need export
272
+ if (meta.mimeType === 'application/vnd.google-apps.document') {
273
+ downloadUrl = `${DRIVE_BASE}/files/${fileId}/export?mimeType=text/plain`;
274
+ } else if (meta.mimeType === 'application/vnd.google-apps.spreadsheet') {
275
+ downloadUrl = `${DRIVE_BASE}/files/${fileId}/export?mimeType=text/csv`;
276
+ } else if (meta.mimeType.startsWith('application/vnd.google-apps.')) {
277
+ downloadUrl = `${DRIVE_BASE}/files/${fileId}/export?mimeType=text/plain`;
278
+ } else {
279
+ downloadUrl = `${DRIVE_BASE}/files/${fileId}?alt=media`;
280
+ }
281
+
282
+ const res = await fetch(downloadUrl, {
283
+ headers: { 'Authorization': `Bearer ${token}` },
284
+ });
285
+
286
+ if (!res.ok) {
287
+ const err = await res.text();
288
+ throw new Error(`Drive read ${res.status}: ${err.slice(0, 200)}`);
289
+ }
290
+
291
+ return res.text();
292
+ }
293
+
294
+ /**
295
+ * Create a folder on Drive.
296
+ */
297
+ export async function createFolder(config, name, parentId = 'root') {
298
+ return driveFetch(config, '/files', {
299
+ method: 'POST',
300
+ headers: { 'Content-Type': 'application/json' },
301
+ body: JSON.stringify({
302
+ name,
303
+ mimeType: 'application/vnd.google-apps.folder',
304
+ parents: [parentId],
305
+ }),
306
+ });
307
+ }
308
+
168
309
  // ── Helpers ─────────────────────────────────────────────────────────────
169
310
 
170
311
  function parseFile(raw) {
@@ -65,6 +65,10 @@ export const DESTRUCTIVE_ACTIONS = new Set([
65
65
  'notify_remind',
66
66
  'slack_send',
67
67
  'github_create_issue',
68
+ 'file_write',
69
+ 'drive_upload',
70
+ 'drive_update',
71
+ 'drive_delete',
68
72
  ]);
69
73
 
70
74
  // ── Tool Definitions (for system prompt) ─────────────────────────────────────
@@ -377,12 +381,74 @@ TOOLS:
377
381
  Use this when the user asks to check messages, read a conversation, or see what others wrote.
378
382
  If channel is not specified, reads from the active channel. Channel can be an ID or a name.
379
383
 
384
+ --- FILE MANAGER ---
385
+
386
+ 68. file_list(path?: string, pattern?: string)
387
+ List files and folders in a directory. Default path is the current working directory.
388
+ pattern filters by glob (e.g. "*.js", "*.csv"). Shows: name, size, type (file/dir), modified date.
389
+ Use when the user asks "show files", "what's in this folder", "list files in ~/Downloads".
390
+
391
+ 69. file_read(path: string, lines?: number)
392
+ Read the content of a text file. Returns up to 500 lines by default (adjustable with lines parameter).
393
+ Supports: .txt, .md, .json, .csv, .js, .ts, .py, .html, .css, .xml, .yaml, .toml, .env, .log, .sh, and similar text formats.
394
+ Use when the user asks "read this file", "show me the contents of", "open file X".
395
+ For binary files (images, PDFs, etc.), returns file info instead of content.
396
+
397
+ 70. file_write(path: string, content: string, append?: boolean)
398
+ Write content to a file. Creates the file if it doesn't exist. Creates parent directories if needed.
399
+ append=true adds to the end instead of overwriting.
400
+ ALWAYS confirm with the user before writing. NEVER overwrite without permission.
401
+ Use when the user asks "create a file", "save this to", "write to".
402
+
403
+ 71. file_info(path: string)
404
+ Get detailed info about a file or folder: size, created date, modified date, permissions, type, extension.
405
+ For directories: also shows total items count and total size.
406
+ Use when the user asks "how big is this file", "when was this modified", "file details".
407
+
408
+ 72. file_search(query: string, path?: string, content?: boolean)
409
+ Search for files by name. Default path is current directory, searches recursively.
410
+ content=true also searches inside file contents (like grep). Max depth: 5 levels. Max results: 50.
411
+ Use when the user asks "find files named", "search for", "where is the file".
412
+
413
+ --- GOOGLE DRIVE ---
414
+
415
+ 73. drive_list(filter?: "recent"|"starred"|"shared", query?: string, maxResults?: number)
416
+ List files from Google Drive. filter="recent" shows last 7 days, "starred" shows starred, "shared" shows shared with me.
417
+ query is a search term to find files by name. Default: all files, 20 results.
418
+
419
+ 74. drive_read(fileId: string)
420
+ Read the text content of a Drive file. Works with Google Docs (exported as plain text), Sheets (as CSV), and any text/code file.
421
+ Returns the content as text. For binary files (images, PDFs), use drive_download instead.
422
+
423
+ 75. drive_upload(name: string, content: string, mimeType?: string, folderId?: string)
424
+ Upload a new file to Google Drive. content is the file text content.
425
+ mimeType defaults to "text/plain". folderId defaults to root.
426
+ ALWAYS confirm before uploading. Use for creating new files on Drive.
427
+
428
+ 76. drive_update(fileId: string, content: string)
429
+ Update (overwrite) the content of an existing Drive file.
430
+ ALWAYS confirm before updating. Use for saving edits to existing files.
431
+
432
+ 77. drive_delete(fileId: string)
433
+ Move a Drive file to trash. ALWAYS confirm before deleting.
434
+
435
+ 78. drive_info(fileId: string)
436
+ Get detailed metadata of a Drive file: size, type, owner, dates, sharing status, link.
437
+
438
+ 79. drive_folder(folderId?: string)
439
+ List files inside a specific Drive folder. Default: root folder.
440
+ Returns files with their IDs, names, types, sizes.
441
+
442
+ 80. drive_download(fileId: string)
443
+ Download a file from Drive. For Google Docs/Sheets/Slides, exports as PDF.
444
+ Returns the file as base64-encoded content. Use for binary files, PDFs, images.
445
+
380
446
  RULES:
381
447
  - ABSOLUTE RULE: NEVER LIE. NEVER fabricate, invent, or guess information. If you do not know, say "I don't know." If a tool fails, say it failed. If you cannot see something, say so. Honesty is MORE important than being helpful.
382
448
  - CRITICAL: For web searches, ALWAYS use web_search — NEVER open Google/Bing/DuckDuckGo in the browser.
383
449
  - CRITICAL: For web searches ("search for X", "find X online", "look up X"), ALWAYS use web_search — NEVER open Google/Bing/DuckDuckGo in the browser. web_search is faster, more reliable, and doesn't get blocked by CAPTCHAs. Only use browser_open for interacting with specific websites (filling forms, clicking buttons, taking screenshots of specific pages).
384
450
  - For search/read operations, execute immediately and present results conversationally.
385
- - For write/send/delete operations (gmail_send, gmail_reply, gmail_delete, calendar_create, calendar_move, calendar_update, contact_delete, 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.
451
+ - For write/send/delete operations (gmail_send, gmail_reply, gmail_delete, calendar_create, calendar_move, calendar_update, contact_delete, task_done, notify_remind, file_write), DESCRIBE what you're about to do and include the JSON block so the system can ask the user for confirmation.
386
452
  - For schedule_meeting and schedule_draft_email, execute immediately — these are read operations that suggest slots.
387
453
  - When presenting email results, show From, Subject, Date, and a brief snippet. Never dump raw JSON.
388
454
  - When presenting calendar events, show Time, Title, Location/Link. Format times in a human-readable way.
@@ -530,11 +596,24 @@ export function describeAction(action, params) {
530
596
  return `Send Slack message to #${params.channel}`;
531
597
  case 'github_create_issue':
532
598
  return `Create GitHub issue on ${params.repo}: "${params.title}"`;
599
+ case 'file_write':
600
+ return `${params.append ? 'Append to' : 'Write'} file: ${params.path} (${params.content?.length || 0} chars)`;
601
+ case 'drive_upload':
602
+ return `Upload to Drive: ${params.name}`;
603
+ case 'drive_update':
604
+ return `Update Drive file: ${params.fileId}`;
605
+ case 'drive_delete':
606
+ return `Delete from Drive: ${params.fileId}`;
533
607
  default:
534
608
  return `Execute ${action}`;
535
609
  }
536
610
  }
537
611
 
612
+ function driveIcon(type) {
613
+ const icons = { folder: '📁', doc: '📄', sheet: '📊', slides: '🎬', pdf: '📕', image: '🖼️', video: '🎥', audio: '🎵', archive: '📦', text: '📝', file: '📄' };
614
+ return icons[type] || icons.file;
615
+ }
616
+
538
617
  /**
539
618
  * Build the system prompt with TOOL_DEFINITIONS + persona + profile + context.
540
619
  * @param {string} persona — e.g. "NHA Chat", "NHA Voice"
@@ -1576,7 +1655,294 @@ export async function executeTool(action, params, config) {
1576
1655
  }
1577
1656
  }
1578
1657
 
1658
+ // ── Google Drive ──────────────────────────────────────────────────────
1659
+ case 'drive_list': {
1660
+ const drv = await import('./google-drive.mjs');
1661
+ let files;
1662
+ if (params.query) {
1663
+ files = await drv.searchFiles(config, params.query, params.maxResults || 20);
1664
+ } else if (params.filter === 'recent') {
1665
+ files = await drv.getRecentFiles(config, params.maxResults || 20);
1666
+ } else if (params.filter === 'starred') {
1667
+ files = await drv.getStarredFiles(config, params.maxResults || 20);
1668
+ } else if (params.filter === 'shared') {
1669
+ files = await drv.getSharedFiles(config, params.maxResults || 20);
1670
+ } else {
1671
+ files = await drv.listFiles(config, params.maxResults || 20);
1672
+ }
1673
+ if (files.length === 0) return 'No files found on Drive.';
1674
+ return files.map((f, i) =>
1675
+ `${i + 1}. [${f.id}] ${driveIcon(f.type)} ${f.name}${f.size ? ' (' + f.size + ')' : ''} — ${f.modifiedTime ? new Date(f.modifiedTime).toLocaleDateString() : ''}${f.shared ? ' [shared]' : ''}${f.starred ? ' ★' : ''}`
1676
+ ).join('\n');
1677
+ }
1678
+
1679
+ case 'drive_read': {
1680
+ if (!params.fileId) return 'Error: fileId required. Use drive_list first to get file IDs.';
1681
+ const drv = await import('./google-drive.mjs');
1682
+ const text = await drv.readFileAsText(config, params.fileId);
1683
+ if (text.length > 10000) return text.slice(0, 10000) + '\n\n[... truncated at 10000 chars]';
1684
+ return text;
1685
+ }
1686
+
1687
+ case 'drive_upload': {
1688
+ if (!params.name || !params.content) return 'Error: name and content required.';
1689
+ const drv = await import('./google-drive.mjs');
1690
+ const uploaded = await drv.uploadFile(config, params.name, params.content, params.mimeType || 'text/plain', params.folderId || 'root');
1691
+ return `Uploaded: ${uploaded.name} (${uploaded.size || ''}) — ${uploaded.webViewLink || uploaded.id}`;
1692
+ }
1693
+
1694
+ case 'drive_update': {
1695
+ if (!params.fileId || !params.content) return 'Error: fileId and content required.';
1696
+ const drv = await import('./google-drive.mjs');
1697
+ const updated = await drv.updateFileContent(config, params.fileId, params.content);
1698
+ return `Updated: ${updated.name} — ${updated.webViewLink || updated.id}`;
1699
+ }
1700
+
1701
+ case 'drive_delete': {
1702
+ if (!params.fileId) return 'Error: fileId required.';
1703
+ const drv = await import('./google-drive.mjs');
1704
+ await drv.trashFile(config, params.fileId);
1705
+ return `File ${params.fileId} moved to trash.`;
1706
+ }
1707
+
1708
+ case 'drive_info': {
1709
+ if (!params.fileId) return 'Error: fileId required.';
1710
+ const drv = await import('./google-drive.mjs');
1711
+ const f = await drv.getFile(config, params.fileId);
1712
+ return [
1713
+ `Name: ${f.name}`,
1714
+ `Type: ${f.type} (${f.mimeType})`,
1715
+ `Size: ${f.size || 'unknown'}`,
1716
+ `Modified: ${f.modifiedTime}`,
1717
+ `Created: ${f.createdTime || 'unknown'}`,
1718
+ `Owner: ${f.owner}`,
1719
+ `Shared: ${f.shared ? 'yes' : 'no'}`,
1720
+ `Starred: ${f.starred ? 'yes' : 'no'}`,
1721
+ `Link: ${f.webViewLink}`,
1722
+ f.description ? `Description: ${f.description}` : '',
1723
+ ].filter(Boolean).join('\n');
1724
+ }
1725
+
1726
+ case 'drive_folder': {
1727
+ const drv = await import('./google-drive.mjs');
1728
+ const files = await drv.listFolder(config, params.folderId || 'root', 30);
1729
+ if (files.length === 0) return 'Empty folder.';
1730
+ return files.map((f, i) =>
1731
+ `${i + 1}. [${f.id}] ${driveIcon(f.type)} ${f.name}${f.size ? ' (' + f.size + ')' : ''}`
1732
+ ).join('\n');
1733
+ }
1734
+
1735
+ case 'drive_download': {
1736
+ if (!params.fileId) return 'Error: fileId required.';
1737
+ const drv = await import('./google-drive.mjs');
1738
+ const dl = await drv.downloadFileContent(config, params.fileId);
1739
+ return `Downloaded: ${dl.name} (${formatFileSize(dl.size)}, ${dl.mimeType})\n[File content available as base64 — ${dl.base64.length} chars]`;
1740
+ }
1741
+
1742
+ // ── File Manager (Local) ────────────────────────────────────────────
1743
+ case 'file_list': {
1744
+ const fsM = await import('fs');
1745
+ const pathM = await import('path');
1746
+ const osM = await import('os');
1747
+ let dir = params.path || process.cwd();
1748
+ dir = dir.replace(/^~/, osM.default.homedir());
1749
+ dir = pathM.default.resolve(dir);
1750
+
1751
+ if (!fsM.default.existsSync(dir)) return `Directory not found: ${dir}`;
1752
+ if (!fsM.default.statSync(dir).isDirectory()) return `Not a directory: ${dir}`;
1753
+
1754
+ // Security: block sensitive paths
1755
+ const blocked = ['/etc/shadow', '/etc/passwd', '.ssh/id_', '.gnupg/', 'Keychain'];
1756
+ if (blocked.some(b => dir.includes(b))) return 'Access denied — sensitive path.';
1757
+
1758
+ let entries = fsM.default.readdirSync(dir, { withFileTypes: true });
1759
+
1760
+ // Pattern filter
1761
+ if (params.pattern) {
1762
+ const pat = params.pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.');
1763
+ const re = new RegExp(`^${pat}$`, 'i');
1764
+ entries = entries.filter(e => re.test(e.name));
1765
+ }
1766
+
1767
+ if (entries.length === 0) return `Empty directory: ${dir}`;
1768
+
1769
+ const lines = entries.slice(0, 100).map(e => {
1770
+ const full = pathM.default.join(dir, e.name);
1771
+ try {
1772
+ const st = fsM.default.statSync(full);
1773
+ const size = e.isDirectory() ? '<DIR>' : formatFileSize(st.size);
1774
+ const mod = st.mtime.toISOString().split('T')[0];
1775
+ return `${e.isDirectory() ? 'd' : '-'} ${size.padStart(10)} ${mod} ${e.name}`;
1776
+ } catch {
1777
+ return `? ? ${e.name}`;
1778
+ }
1779
+ });
1780
+
1781
+ return `${dir}/ (${entries.length} items${entries.length > 100 ? ', showing first 100' : ''})\n\n${lines.join('\n')}`;
1782
+ }
1783
+
1784
+ case 'file_read': {
1785
+ const fsR = await import('fs');
1786
+ const pathR = await import('path');
1787
+ const osR = await import('os');
1788
+ let filePath = params.path;
1789
+ filePath = filePath.replace(/^~/, osR.default.homedir());
1790
+ filePath = pathR.default.resolve(filePath);
1791
+
1792
+ if (!fsR.default.existsSync(filePath)) return `File not found: ${filePath}`;
1793
+ const stat = fsR.default.statSync(filePath);
1794
+
1795
+ if (stat.isDirectory()) return `"${filePath}" is a directory. Use file_list to browse it.`;
1796
+
1797
+ // Block sensitive files
1798
+ const sensitivePatterns = ['.ssh/id_', '.gnupg/', 'Keychain', '.env', 'credentials', 'secret', '.npmrc', '.pypirc'];
1799
+ if (sensitivePatterns.some(p => filePath.includes(p))) return 'Access denied — sensitive file.';
1800
+
1801
+ // Binary check
1802
+ const ext = pathR.default.extname(filePath).toLowerCase();
1803
+ const binaryExts = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.mp3', '.mp4', '.avi', '.mov', '.zip', '.tar', '.gz', '.rar', '.7z', '.exe', '.dll', '.so', '.dylib', '.bin', '.dat', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.woff', '.woff2', '.ttf', '.otf', '.eot']);
1804
+ if (binaryExts.has(ext)) {
1805
+ return `Binary file: ${pathR.default.basename(filePath)} (${formatFileSize(stat.size)}, ${ext} format). Cannot display content.`;
1806
+ }
1807
+
1808
+ // Size check
1809
+ if (stat.size > 1024 * 1024) return `File too large to display: ${formatFileSize(stat.size)}. Use file_info for details.`;
1810
+
1811
+ const maxLines = params.lines || 500;
1812
+ const content = fsR.default.readFileSync(filePath, 'utf-8');
1813
+ const lines = content.split('\n');
1814
+ const truncated = lines.length > maxLines;
1815
+ const display = lines.slice(0, maxLines).join('\n');
1816
+
1817
+ return `${pathR.default.basename(filePath)} (${formatFileSize(stat.size)}, ${lines.length} lines)${truncated ? ` [showing first ${maxLines}]` : ''}\n\n${display}`;
1818
+ }
1819
+
1820
+ case 'file_write': {
1821
+ const fsW = await import('fs');
1822
+ const pathW = await import('path');
1823
+ const osW = await import('os');
1824
+ let writePath = params.path;
1825
+ writePath = writePath.replace(/^~/, osW.default.homedir());
1826
+ writePath = pathW.default.resolve(writePath);
1827
+
1828
+ // Block writing to sensitive locations
1829
+ const blockedWrite = ['/etc/', '/usr/', '/bin/', '/sbin/', '/System/', '/Library/', '.ssh/', '.gnupg/', 'node_modules/'];
1830
+ if (blockedWrite.some(b => writePath.includes(b))) return 'Access denied — cannot write to system/sensitive paths.';
1831
+
1832
+ // Create parent directories
1833
+ const dir = pathW.default.dirname(writePath);
1834
+ if (!fsW.default.existsSync(dir)) {
1835
+ fsW.default.mkdirSync(dir, { recursive: true });
1836
+ }
1837
+
1838
+ if (params.append) {
1839
+ fsW.default.appendFileSync(writePath, params.content, 'utf-8');
1840
+ return `Appended ${params.content.length} chars to ${writePath}`;
1841
+ } else {
1842
+ fsW.default.writeFileSync(writePath, params.content, 'utf-8');
1843
+ return `Written ${params.content.length} chars to ${writePath}`;
1844
+ }
1845
+ }
1846
+
1847
+ case 'file_info': {
1848
+ const fsI = await import('fs');
1849
+ const pathI = await import('path');
1850
+ const osI = await import('os');
1851
+ let infoPath = params.path;
1852
+ infoPath = infoPath.replace(/^~/, osI.default.homedir());
1853
+ infoPath = pathI.default.resolve(infoPath);
1854
+
1855
+ if (!fsI.default.existsSync(infoPath)) return `Not found: ${infoPath}`;
1856
+ const st = fsI.default.statSync(infoPath);
1857
+
1858
+ const info = [
1859
+ `Path: ${infoPath}`,
1860
+ `Type: ${st.isDirectory() ? 'directory' : 'file'}`,
1861
+ `Size: ${formatFileSize(st.size)}`,
1862
+ `Created: ${st.birthtime.toISOString()}`,
1863
+ `Modified: ${st.mtime.toISOString()}`,
1864
+ `Permissions: ${(st.mode & 0o777).toString(8)}`,
1865
+ ];
1866
+
1867
+ if (!st.isDirectory()) {
1868
+ info.push(`Extension: ${pathI.default.extname(infoPath) || '(none)'}`);
1869
+ } else {
1870
+ try {
1871
+ const items = fsI.default.readdirSync(infoPath);
1872
+ info.push(`Items: ${items.length}`);
1873
+ } catch { /* permission denied */ }
1874
+ }
1875
+
1876
+ return info.join('\n');
1877
+ }
1878
+
1879
+ case 'file_search': {
1880
+ const fsS = await import('fs');
1881
+ const pathS = await import('path');
1882
+ const osS = await import('os');
1883
+ let searchDir = params.path || process.cwd();
1884
+ searchDir = searchDir.replace(/^~/, osS.default.homedir());
1885
+ searchDir = pathS.default.resolve(searchDir);
1886
+
1887
+ if (!fsS.default.existsSync(searchDir)) return `Directory not found: ${searchDir}`;
1888
+
1889
+ const query = params.query.toLowerCase();
1890
+ const searchContent = params.content || false;
1891
+ const results = [];
1892
+ const maxDepth = 5;
1893
+ const maxResults = 50;
1894
+
1895
+ function searchRecursive(dir, depth) {
1896
+ if (depth > maxDepth || results.length >= maxResults) return;
1897
+ try {
1898
+ const entries = fsS.default.readdirSync(dir, { withFileTypes: true });
1899
+ for (const entry of entries) {
1900
+ if (results.length >= maxResults) break;
1901
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
1902
+
1903
+ const full = pathS.default.join(dir, entry.name);
1904
+
1905
+ // Name match
1906
+ if (entry.name.toLowerCase().includes(query)) {
1907
+ const st = fsS.default.statSync(full);
1908
+ results.push(`${entry.isDirectory() ? 'd' : '-'} ${formatFileSize(st.size).padStart(10)} ${full}`);
1909
+ }
1910
+
1911
+ // Content search
1912
+ if (searchContent && entry.isFile() && !entry.name.match(/\.(png|jpg|gif|mp4|zip|tar|gz|exe|bin|pdf|woff|ttf)$/i)) {
1913
+ try {
1914
+ const st = fsS.default.statSync(full);
1915
+ if (st.size < 512 * 1024) { // Max 512KB per file
1916
+ const content = fsS.default.readFileSync(full, 'utf-8');
1917
+ if (content.toLowerCase().includes(query)) {
1918
+ const lineNum = content.split('\n').findIndex(l => l.toLowerCase().includes(query)) + 1;
1919
+ if (!results.some(r => r.includes(full))) {
1920
+ results.push(`- ${formatFileSize(st.size).padStart(10)} ${full}:${lineNum} (content match)`);
1921
+ }
1922
+ }
1923
+ }
1924
+ } catch { /* skip unreadable */ }
1925
+ }
1926
+
1927
+ if (entry.isDirectory()) searchRecursive(full, depth + 1);
1928
+ }
1929
+ } catch { /* permission denied */ }
1930
+ }
1931
+
1932
+ searchRecursive(searchDir, 0);
1933
+
1934
+ if (results.length === 0) return `No files matching "${params.query}" in ${searchDir}`;
1935
+ return `Found ${results.length} match${results.length > 1 ? 'es' : ''} for "${params.query}":\n\n${results.join('\n')}`;
1936
+ }
1937
+
1579
1938
  default:
1580
1939
  return `Unknown action: ${action}`;
1581
1940
  }
1582
1941
  }
1942
+
1943
+ function formatFileSize(bytes) {
1944
+ if (bytes === 0) return '0 B';
1945
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1946
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
1947
+ return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
1948
+ }
@@ -1365,9 +1365,12 @@ var AGENT_ICONS = {
1365
1365
  prometheus:'\\u{1F525}',cassandra:'\\u26A0',athena:'\\u{1F9E0}',sauron:'\\u{1F441}',conductor:'\\u{1F3BC}',
1366
1366
  navi:'\\u{1F9ED}',edi:'\\u{1F4C8}',tempest:'\\u26C8',epicure:'\\u{1F37D}'
1367
1367
  };
1368
- // ---- DRIVE ----
1368
+ // ---- DRIVE (Full File Manager) ----
1369
1369
  var driveData=null;
1370
1370
  var driveFilter='';
1371
+ var driveEditorFile=null; // {id,name,content,mimeType} when editing
1372
+ var driveViewerFile=null; // {id,name,base64,mimeType} when viewing image/pdf
1373
+
1371
1374
  function renderDrive(el){
1372
1375
  if(!driveData){
1373
1376
  el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading Drive...</div></div>';
@@ -1376,27 +1379,37 @@ function renderDrive(el){
1376
1379
  });
1377
1380
  return;
1378
1381
  }
1382
+
1383
+ // If editor is open, render editor instead
1384
+ if(driveEditorFile){renderDriveEditor(el);return;}
1385
+ // If viewer is open, render viewer
1386
+ if(driveViewerFile){renderDriveViewer(el);return;}
1387
+
1379
1388
  var files=driveData.files||[];
1380
1389
  var quota=driveData.quota;
1381
-
1382
1390
  var h='';
1383
1391
 
1384
1392
  // Quota bar
1385
1393
  if(quota){
1386
1394
  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>';
1387
- 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>';
1395
+ 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>';
1388
1396
  }
1389
1397
 
1390
- // Filter bar
1398
+ // Action bar
1391
1399
  h+='<div style="display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap">';
1392
1400
  ['','recent','starred','shared'].forEach(function(f){
1393
1401
  var label=f||'All Files';
1394
1402
  var active=driveFilter===f;
1395
- 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>';
1403
+ 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)')+';cursor:pointer">'+esc(label.charAt(0).toUpperCase()+label.slice(1))+'</button>';
1396
1404
  });
1397
- 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()">';
1405
+ h+='<div style="flex:1"></div>';
1406
+ h+='<button onclick="driveNewFile()" style="padding:6px 14px;border-radius:6px;font-size:11px;background:var(--green3);color:var(--bg);border:1px solid var(--green);cursor:pointer">+ New File</button>';
1407
+ h+='<button onclick="driveUploadFile()" style="padding:6px 14px;border-radius:6px;font-size:11px;background:var(--amberdim);color:var(--amber);border:1px solid var(--amber3);cursor:pointer">Upload</button>';
1398
1408
  h+='</div>';
1399
1409
 
1410
+ // Search bar
1411
+ h+='<div style="margin-bottom:12px"><input type="text" id="driveSearch" placeholder="Search files..." style="width:100%;font-size:11px;padding:8px 12px" onkeydown="if(event.key===\\x27Enter\\x27)searchDrive()"></div>';
1412
+
1400
1413
  // File list
1401
1414
  if(files.length===0){
1402
1415
  h+='<div class="card" style="text-align:center;color:var(--dim);padding:30px">No files found</div>';
@@ -1404,14 +1417,31 @@ function renderDrive(el){
1404
1417
  files.forEach(function(f){
1405
1418
  var icon=driveTypeIcon(f.type);
1406
1419
  var date=f.modifiedTime?new Date(f.modifiedTime).toLocaleDateString():'';
1407
- h+='<div class="card" style="margin-bottom:6px;padding:10px;cursor:pointer" onclick="window.open(\\x27'+esc(f.webViewLink)+'\\x27,\\x27_blank\\x27)">';
1420
+ var isText=f.type==='text'||f.type==='doc'||f.mimeType.includes('text')||f.mimeType.includes('json')||f.mimeType.includes('javascript')||f.mimeType.includes('xml')||f.mimeType.includes('csv')||f.mimeType.includes('yaml')||f.mimeType.includes('markdown')||f.mimeType.includes('html')||f.mimeType.includes('css')||f.mimeType.includes('python')||f.mimeType.includes('php')||f.mimeType.includes('vnd.google-apps.document');
1421
+ var isImage=f.type==='image';
1422
+ var isPdf=f.type==='pdf'||f.mimeType.includes('pdf');
1423
+
1424
+ h+='<div class="card" style="margin-bottom:4px;padding:10px">';
1408
1425
  h+='<div style="display:flex;align-items:center;gap:10px">';
1409
- h+='<span style="font-size:20px">'+icon+'</span>';
1426
+ h+='<span style="font-size:18px">'+icon+'</span>';
1410
1427
  h+='<div style="flex:1;min-width:0">';
1411
- h+='<div style="color:var(--bright);font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(f.name)+'</div>';
1428
+ h+='<div style="color:var(--bright);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(f.name)+'</div>';
1412
1429
  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>';
1413
1430
  h+='</div>';
1414
- h+='<span style="color:var(--dim);font-size:10px">'+esc(f.type)+'</span>';
1431
+ // Action buttons
1432
+ h+='<div style="display:flex;gap:4px;flex-shrink:0">';
1433
+ if(isText){
1434
+ h+='<button onclick="event.stopPropagation();driveOpenEditor(\\x27'+f.id+'\\x27,\\x27'+esc(f.name).replace(/'/g,'\\x27')+'\\x27)" style="padding:3px 8px;font-size:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;color:var(--cyan);cursor:pointer" title="Open in editor">Edit</button>';
1435
+ }
1436
+ if(isImage){
1437
+ h+='<button onclick="event.stopPropagation();driveViewImage(\\x27'+f.id+'\\x27,\\x27'+esc(f.name).replace(/'/g,'\\x27')+'\\x27)" style="padding:3px 8px;font-size:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;color:var(--green);cursor:pointer" title="View image">View</button>';
1438
+ }
1439
+ if(isPdf){
1440
+ h+='<button onclick="event.stopPropagation();driveViewPdf(\\x27'+f.id+'\\x27,\\x27'+esc(f.name).replace(/'/g,'\\x27')+'\\x27)" style="padding:3px 8px;font-size:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;color:var(--amber);cursor:pointer" title="View PDF">PDF</button>';
1441
+ }
1442
+ h+='<button onclick="event.stopPropagation();window.open(\\x27'+esc(f.webViewLink)+'\\x27,\\x27_blank\\x27)" style="padding:3px 8px;font-size:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;color:var(--dim);cursor:pointer" title="Open in Google Drive">Open</button>';
1443
+ h+='<button onclick="event.stopPropagation();driveDeleteFile(\\x27'+f.id+'\\x27,\\x27'+esc(f.name).replace(/'/g,'\\x27')+'\\x27)" style="padding:3px 8px;font-size:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;color:var(--red);cursor:pointer;opacity:0.6" title="Delete">Del</button>';
1444
+ h+='</div>';
1415
1445
  h+='</div></div>';
1416
1446
  });
1417
1447
  }
@@ -1419,6 +1449,130 @@ function renderDrive(el){
1419
1449
  el.innerHTML=h;
1420
1450
  }
1421
1451
 
1452
+ // ---- Drive Editor (Notepad) ----
1453
+ function renderDriveEditor(el){
1454
+ var f=driveEditorFile;
1455
+ var h='<div style="max-width:900px;margin:0 auto">';
1456
+ h+='<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">';
1457
+ h+='<button onclick="driveCloseEditor()" style="padding:4px 10px;font-size:11px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;color:var(--dim);cursor:pointer">&larr; Back</button>';
1458
+ h+='<span style="flex:1;font-size:14px;color:var(--bright);font-weight:bold">'+esc(f.name)+'</span>';
1459
+ h+='<button onclick="driveSaveEditor()" style="padding:6px 16px;font-size:11px;background:var(--green3);border:1px solid var(--green);border-radius:6px;color:var(--bg);cursor:pointer;font-weight:bold">Save to Drive</button>';
1460
+ h+='</div>';
1461
+ h+='<textarea id="driveEditorContent" style="width:100%;min-height:500px;background:var(--bg);border:1px solid var(--border2);border-radius:8px;padding:14px;color:var(--fg);font-family:var(--mono);font-size:12px;line-height:1.6;resize:vertical;tab-size:2" spellcheck="false">'+esc(f.content||'')+'</textarea>';
1462
+ h+='<div style="color:var(--dim);font-size:10px;margin-top:6px">File ID: '+esc(f.id)+' &middot; Use Tab for indentation &middot; Changes are NOT auto-saved</div>';
1463
+ h+='</div>';
1464
+ el.innerHTML=h;
1465
+ // Enable Tab key in textarea
1466
+ var ta=document.getElementById('driveEditorContent');
1467
+ if(ta)ta.addEventListener('keydown',function(e){if(e.key==='Tab'){e.preventDefault();var s=this.selectionStart,end=this.selectionEnd;this.value=this.value.substring(0,s)+' '+this.value.substring(end);this.selectionStart=this.selectionEnd=s+2;}});
1468
+ }
1469
+
1470
+ function driveOpenEditor(fileId,fileName){
1471
+ var el=document.getElementById('content');
1472
+ el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading file...</div></div>';
1473
+ apiGet('/api/drive/read/'+fileId).then(function(r){
1474
+ driveEditorFile={id:fileId,name:fileName,content:r.content||''};
1475
+ renderDrive(el);
1476
+ }).catch(function(e){
1477
+ el.innerHTML='<div class="card" style="color:var(--red);padding:20px">Error reading file: '+esc(e.message||'unknown')+'</div>';
1478
+ });
1479
+ }
1480
+
1481
+ function driveSaveEditor(){
1482
+ var ta=document.getElementById('driveEditorContent');
1483
+ if(!ta||!driveEditorFile)return;
1484
+ var content=ta.value;
1485
+ if(!confirm('Save changes to "'+driveEditorFile.name+'" on Drive?'))return;
1486
+ apiPost('/api/drive/update/'+driveEditorFile.id,{content:content}).then(function(){
1487
+ driveEditorFile.content=content;
1488
+ alert('Saved!');
1489
+ }).catch(function(e){alert('Save failed: '+(e.message||'unknown'));});
1490
+ }
1491
+
1492
+ function driveCloseEditor(){
1493
+ driveEditorFile=null;
1494
+ renderDrive(document.getElementById('content'));
1495
+ }
1496
+
1497
+ // ---- Drive Image/PDF Viewer ----
1498
+ function renderDriveViewer(el){
1499
+ var f=driveViewerFile;
1500
+ var h='<div style="max-width:900px;margin:0 auto">';
1501
+ h+='<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">';
1502
+ h+='<button onclick="driveCloseViewer()" style="padding:4px 10px;font-size:11px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;color:var(--dim);cursor:pointer">&larr; Back</button>';
1503
+ h+='<span style="flex:1;font-size:14px;color:var(--bright);font-weight:bold">'+esc(f.name)+'</span>';
1504
+ h+='</div>';
1505
+ if(f.mimeType&&f.mimeType.includes('image')){
1506
+ h+='<img src="data:'+esc(f.mimeType)+';base64,'+f.base64+'" style="max-width:100%;border-radius:8px;border:1px solid var(--border)" alt="'+esc(f.name)+'">';
1507
+ } else {
1508
+ h+='<iframe src="data:application/pdf;base64,'+f.base64+'" style="width:100%;height:600px;border:1px solid var(--border);border-radius:8px" title="'+esc(f.name)+'"></iframe>';
1509
+ }
1510
+ h+='</div>';
1511
+ el.innerHTML=h;
1512
+ }
1513
+
1514
+ function driveViewImage(fileId,fileName){
1515
+ var el=document.getElementById('content');
1516
+ el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading image...</div></div>';
1517
+ apiGet('/api/drive/download/'+fileId).then(function(r){
1518
+ driveViewerFile={id:fileId,name:fileName,base64:r.base64,mimeType:r.mimeType};
1519
+ renderDrive(el);
1520
+ }).catch(function(e){el.innerHTML='<div class="card" style="color:var(--red);padding:20px">Error: '+esc(e.message)+'</div>';});
1521
+ }
1522
+
1523
+ function driveViewPdf(fileId,fileName){
1524
+ var el=document.getElementById('content');
1525
+ el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading PDF...</div></div>';
1526
+ apiGet('/api/drive/download/'+fileId).then(function(r){
1527
+ driveViewerFile={id:fileId,name:fileName,base64:r.base64,mimeType:'application/pdf'};
1528
+ renderDrive(el);
1529
+ }).catch(function(e){el.innerHTML='<div class="card" style="color:var(--red);padding:20px">Error: '+esc(e.message)+'</div>';});
1530
+ }
1531
+
1532
+ function driveCloseViewer(){
1533
+ driveViewerFile=null;
1534
+ renderDrive(document.getElementById('content'));
1535
+ }
1536
+
1537
+ // ---- Drive Actions ----
1538
+ function driveNewFile(){
1539
+ var name=prompt('File name (e.g. notes.txt, script.py):');
1540
+ if(!name)return;
1541
+ apiPost('/api/drive/upload',{name:name,content:'',mimeType:'text/plain'}).then(function(r){
1542
+ driveData=null;
1543
+ driveEditorFile={id:r.id,name:name,content:''};
1544
+ renderDrive(document.getElementById('content'));
1545
+ }).catch(function(e){alert('Error: '+(e.message||'unknown'));});
1546
+ }
1547
+
1548
+ function driveUploadFile(){
1549
+ var inp=document.createElement('input');
1550
+ inp.type='file';
1551
+ inp.accept='*/*';
1552
+ inp.onchange=function(){
1553
+ var file=inp.files[0];
1554
+ if(!file)return;
1555
+ var reader=new FileReader();
1556
+ reader.onload=function(){
1557
+ var base64=reader.result.split(',')[1]||'';
1558
+ apiPost('/api/drive/upload',{name:file.name,content:base64,mimeType:file.type||'application/octet-stream',encoding:'base64'}).then(function(){
1559
+ driveData=null;
1560
+ renderDrive(document.getElementById('content'));
1561
+ }).catch(function(e){alert('Upload error: '+(e.message||'unknown'));});
1562
+ };
1563
+ reader.readAsDataURL(file);
1564
+ };
1565
+ inp.click();
1566
+ }
1567
+
1568
+ function driveDeleteFile(fileId,fileName){
1569
+ if(!confirm('Delete "'+fileName+'" from Drive? (moved to trash)'))return;
1570
+ apiPost('/api/drive/delete/'+fileId,{}).then(function(){
1571
+ driveData=null;
1572
+ renderDrive(document.getElementById('content'));
1573
+ }).catch(function(e){alert('Delete error: '+(e.message||'unknown'));});
1574
+ }
1575
+
1422
1576
  function driveTypeIcon(type){
1423
1577
  var icons={folder:'&#128193;',doc:'&#128196;',sheet:'&#128202;',slides:'&#127916;',pdf:'&#128213;',image:'&#127748;',video:'&#127910;',audio:'&#127925;',archive:'&#128230;',text:'&#128221;',file:'&#128196;'};
1424
1578
  return icons[type]||icons.file;