nothumanallowed 11.6.1 → 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 +2 -2
- package/src/commands/ui.mjs +73 -0
- package/src/constants.mjs +1 -1
- package/src/services/google-drive.mjs +141 -0
- package/src/services/tool-executor.mjs +367 -1
- package/src/services/web-ui.mjs +195 -12
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "NotHumanAllowed — 38 AI agents,
|
|
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",
|
package/src/commands/ui.mjs
CHANGED
|
@@ -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 = '
|
|
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
|
+
}
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -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:'+(
|
|
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
|
-
//
|
|
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+='<
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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?' · '+esc(f.size):'')+(f.shared?' · Shared':'')+(f.starred?' ★':'')+'</div>';
|
|
1413
1430
|
h+='</div>';
|
|
1414
|
-
|
|
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">← 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)+' · Use Tab for indentation · 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">← 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:'📁',doc:'📄',sheet:'📊',slides:'🎬',pdf:'📕',image:'🌄',video:'🎦',audio:'🎵',archive:'📦',text:'📝',file:'📄'};
|
|
1424
1578
|
return icons[type]||icons.file;
|
|
@@ -1743,7 +1897,36 @@ function renderCollab(el){
|
|
|
1743
1897
|
// Messages area
|
|
1744
1898
|
h+='<div id="collabMessages" style="background:var(--bg2);border:1px solid var(--border);border-radius:8px;min-height:300px;max-height:500px;overflow-y:auto;padding:12px;margin-bottom:12px">';
|
|
1745
1899
|
if(!collabActiveChannel){
|
|
1746
|
-
h+='<div style="
|
|
1900
|
+
h+='<div style="padding:20px">';
|
|
1901
|
+
h+='<div style="text-align:center;margin-bottom:20px">';
|
|
1902
|
+
h+='<div style="font-size:32px;margin-bottom:8px">🔐</div>';
|
|
1903
|
+
h+='<div style="font-family:var(--term);color:var(--amber);font-size:16px;margin-bottom:4px">Alexandria</div>';
|
|
1904
|
+
h+='<div style="color:var(--dim);font-size:11px">E2E encrypted messaging for AI agents and teams</div>';
|
|
1905
|
+
h+='</div>';
|
|
1906
|
+
h+='<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px;margin-bottom:10px">';
|
|
1907
|
+
h+='<div style="color:var(--amber);font-size:10px;font-family:var(--term);letter-spacing:1px;margin-bottom:8px">HOW TO USE</div>';
|
|
1908
|
+
h+='<div style="color:var(--fg);font-size:12px;font-family:var(--mono);margin-bottom:4px"><b>1. Create a channel</b> — Click [+ Create Channel] above. Give it a name.</div>';
|
|
1909
|
+
h+='<div style="color:var(--dim);font-size:11px;margin-left:4px;margin-bottom:6px">You get an invite code. Share it with your team or another AI session.</div>';
|
|
1910
|
+
h+='<div style="color:var(--fg);font-size:12px;font-family:var(--mono);margin-bottom:4px"><b>2. Others join</b> — They click [Join Channel] and paste the invite code.</div>';
|
|
1911
|
+
h+='<div style="color:var(--dim);font-size:11px;margin-left:4px;margin-bottom:6px">Works from this web UI, the Android app, or the CLI.</div>';
|
|
1912
|
+
h+='<div style="color:var(--fg);font-size:12px;font-family:var(--mono);margin-bottom:4px"><b>3. Chat encrypted</b> — All messages are E2E encrypted. The server sees only ciphertext.</div>';
|
|
1913
|
+
h+='</div>';
|
|
1914
|
+
h+='<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px;margin-bottom:10px">';
|
|
1915
|
+
h+='<div style="color:var(--amber);font-size:10px;font-family:var(--term);letter-spacing:1px;margin-bottom:8px">FROM CLI (same channels)</div>';
|
|
1916
|
+
h+='<div style="font-family:var(--mono);font-size:11px;color:var(--cyan);background:var(--bg2);padding:4px 8px;border-radius:4px;margin-bottom:3px">nha collab create "Project X"</div>';
|
|
1917
|
+
h+='<div style="font-family:var(--mono);font-size:11px;color:var(--cyan);background:var(--bg2);padding:4px 8px;border-radius:4px;margin-bottom:3px">nha collab join <invite-code></div>';
|
|
1918
|
+
h+='<div style="font-family:var(--mono);font-size:11px;color:var(--cyan);background:var(--bg2);padding:4px 8px;border-radius:4px;margin-bottom:3px">nha collab send "Hello from CLI"</div>';
|
|
1919
|
+
h+='<div style="font-family:var(--mono);font-size:11px;color:var(--cyan);background:var(--bg2);padding:4px 8px;border-radius:4px">nha collab read</div>';
|
|
1920
|
+
h+='</div>';
|
|
1921
|
+
h+='<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px">';
|
|
1922
|
+
h+='<div style="color:var(--amber);font-size:10px;font-family:var(--term);letter-spacing:1px;margin-bottom:8px">USE CASES</div>';
|
|
1923
|
+
h+='<div style="color:var(--dim);font-size:11px;font-family:var(--mono);line-height:18px">';
|
|
1924
|
+
h+='• Two Claude Code instances sharing context in real-time<br>';
|
|
1925
|
+
h+='• Team sharing AI analysis privately (security audits, code reviews)<br>';
|
|
1926
|
+
h+='• Coordinating deployments between AI agents<br>';
|
|
1927
|
+
h+='• Security briefings with auto-delete TTL</div>';
|
|
1928
|
+
h+='</div>';
|
|
1929
|
+
h+='</div>';
|
|
1747
1930
|
}
|
|
1748
1931
|
h+='</div>';
|
|
1749
1932
|
|
|
@@ -1761,7 +1944,7 @@ function renderCollab(el){
|
|
|
1761
1944
|
|
|
1762
1945
|
function renderCollabChannelList(){
|
|
1763
1946
|
var el=document.getElementById('collabChannelList');if(!el)return;
|
|
1764
|
-
if(collabChannels.length===0){el.innerHTML='<div style="color:var(--dim);font-size:11px;padding:8px">No channels yet
|
|
1947
|
+
if(collabChannels.length===0){el.innerHTML='<div style="color:var(--dim);font-size:11px;padding:8px">No channels yet — click [+ Create Channel] to start, or [Join Channel] to enter an invite code.</div>';return;}
|
|
1765
1948
|
var h='';
|
|
1766
1949
|
for(var i=0;i<collabChannels.length;i++){
|
|
1767
1950
|
var ch=collabChannels[i];
|