@clavon/clav-agent 1.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.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ const { execSync } = require('child_process');
3
+ const path = require('path');
4
+
5
+ try {
6
+ const electronPath = require('electron');
7
+ const appPath = path.join(__dirname, '..');
8
+ execSync(`"${electronPath}" "${appPath}"`, { stdio: 'inherit' });
9
+ } catch (error) {
10
+ console.error('Failed to start Clav Agent:', error.message);
11
+ process.exit(1);
12
+ }
@@ -0,0 +1,521 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const electron = require("electron");
4
+ const path = require("path");
5
+ const utils = require("@electron-toolkit/utils");
6
+ const fs = require("fs");
7
+ const child_process = require("child_process");
8
+ let tray = null;
9
+ let isConnected = false;
10
+ function setupTray(mainWindow) {
11
+ const iconPath = path.join(__dirname, "../../assets/icons/tray-icon.png");
12
+ let icon;
13
+ try {
14
+ icon = electron.nativeImage.createFromPath(iconPath);
15
+ if (icon.isEmpty()) {
16
+ icon = createDefaultIcon();
17
+ }
18
+ } catch {
19
+ icon = createDefaultIcon();
20
+ }
21
+ tray = new electron.Tray(icon.resize({ width: 16, height: 16 }));
22
+ tray.setToolTip("Clav Agent — Disconnected");
23
+ const updateMenu = () => {
24
+ const contextMenu = electron.Menu.buildFromTemplate([
25
+ {
26
+ label: `Clav Agent ${isConnected ? "(Connected)" : "(Disconnected)"}`,
27
+ enabled: false
28
+ },
29
+ { type: "separator" },
30
+ {
31
+ label: "Open",
32
+ click: () => {
33
+ mainWindow.show();
34
+ mainWindow.focus();
35
+ }
36
+ },
37
+ {
38
+ label: "Settings",
39
+ click: () => {
40
+ mainWindow.show();
41
+ mainWindow.focus();
42
+ mainWindow.webContents.send("navigate", "/settings");
43
+ }
44
+ },
45
+ { type: "separator" },
46
+ {
47
+ label: "Quit",
48
+ click: () => {
49
+ electron.app.isQuitting = true;
50
+ electron.app.quit();
51
+ }
52
+ }
53
+ ]);
54
+ tray?.setContextMenu(contextMenu);
55
+ };
56
+ updateMenu();
57
+ tray.on("double-click", () => {
58
+ mainWindow.show();
59
+ mainWindow.focus();
60
+ });
61
+ global.__updateTrayConnection = (connected) => {
62
+ isConnected = connected;
63
+ tray?.setToolTip(`Clav Agent — ${connected ? "Connected" : "Disconnected"}`);
64
+ updateMenu();
65
+ };
66
+ }
67
+ function createDefaultIcon() {
68
+ return electron.nativeImage.createFromDataURL(
69
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAARElEQVQ4T2NkYPj/n4EBCBgZGf8zMDL+Z2Bk+M/IwPCfkZHxPwMj438GRsb/jIyM/xkZGf8zMjL+Z2AAqmFkZPwPAFUOEBH0sOZ3AAAAAElFTkSuQmCC"
70
+ );
71
+ }
72
+ const BLOCKED_PATTERNS$1 = [
73
+ /[/\\]\.ssh[/\\]/i,
74
+ /[/\\]\.aws[/\\]/i,
75
+ /[/\\]\.gnupg[/\\]/i,
76
+ /[/\\]\.env$/i,
77
+ /[/\\]\.env\./i,
78
+ /[/\\]credentials/i,
79
+ /^[A-Z]:[/\\]Windows[/\\]/i,
80
+ /^[A-Z]:[/\\]Program Files/i,
81
+ /^\/etc\//i,
82
+ /^\/usr\//i,
83
+ /^\/bin\//i,
84
+ /^\/sbin\//i
85
+ ];
86
+ function isPathBlocked(filePath) {
87
+ const normalized = path.resolve(filePath);
88
+ return BLOCKED_PATTERNS$1.some((p) => p.test(normalized));
89
+ }
90
+ async function executeFileTool(toolName, params) {
91
+ const filePath = params.path;
92
+ if (!filePath) {
93
+ return { success: false, error: "Missing required parameter: path" };
94
+ }
95
+ if (isPathBlocked(filePath)) {
96
+ return { success: false, error: `Access denied: "${filePath}" is in a restricted location.` };
97
+ }
98
+ switch (toolName) {
99
+ case "desktop_file_read":
100
+ return fileRead(filePath, params.encoding);
101
+ case "desktop_file_write":
102
+ return fileWrite(filePath, params.content);
103
+ case "desktop_file_list":
104
+ return fileList(filePath);
105
+ case "desktop_file_delete":
106
+ return fileDelete(filePath);
107
+ case "desktop_file_search":
108
+ return fileSearch(filePath, params.pattern);
109
+ default:
110
+ return { success: false, error: `Unknown file tool: ${toolName}` };
111
+ }
112
+ }
113
+ async function fileRead(filePath, encoding) {
114
+ try {
115
+ const content = await fs.promises.readFile(path.resolve(filePath), {
116
+ encoding: encoding || "utf-8"
117
+ });
118
+ const stats = await fs.promises.stat(path.resolve(filePath));
119
+ return {
120
+ success: true,
121
+ data: {
122
+ content,
123
+ size: stats.size,
124
+ modified: stats.mtime.toISOString()
125
+ }
126
+ };
127
+ } catch (error) {
128
+ if (error.code === "ENOENT") {
129
+ return { success: false, error: `File not found: ${filePath}` };
130
+ }
131
+ return { success: false, error: error.message };
132
+ }
133
+ }
134
+ async function fileWrite(filePath, content) {
135
+ if (content === void 0 || content === null) {
136
+ return { success: false, error: "Missing required parameter: content" };
137
+ }
138
+ try {
139
+ const resolved = path.resolve(filePath);
140
+ await fs.promises.mkdir(path.join(resolved, ".."), { recursive: true });
141
+ await fs.promises.writeFile(resolved, content, "utf-8");
142
+ const stats = await fs.promises.stat(resolved);
143
+ return {
144
+ success: true,
145
+ data: {
146
+ path: resolved,
147
+ size: stats.size,
148
+ message: `File written successfully (${stats.size} bytes)`
149
+ }
150
+ };
151
+ } catch (error) {
152
+ return { success: false, error: error.message };
153
+ }
154
+ }
155
+ async function fileList(dirPath) {
156
+ try {
157
+ const resolved = path.resolve(dirPath);
158
+ const entries = await fs.promises.readdir(resolved, { withFileTypes: true });
159
+ const items = await Promise.all(
160
+ entries.slice(0, 200).map(async (entry) => {
161
+ const fullPath = path.join(resolved, entry.name);
162
+ try {
163
+ const stats = await fs.promises.stat(fullPath);
164
+ return {
165
+ name: entry.name,
166
+ type: entry.isDirectory() ? "directory" : "file",
167
+ size: stats.size,
168
+ modified: stats.mtime.toISOString()
169
+ };
170
+ } catch {
171
+ return {
172
+ name: entry.name,
173
+ type: entry.isDirectory() ? "directory" : "file"
174
+ };
175
+ }
176
+ })
177
+ );
178
+ return {
179
+ success: true,
180
+ data: {
181
+ path: resolved,
182
+ count: entries.length,
183
+ items,
184
+ truncated: entries.length > 200
185
+ }
186
+ };
187
+ } catch (error) {
188
+ if (error.code === "ENOENT") {
189
+ return { success: false, error: `Directory not found: ${dirPath}` };
190
+ }
191
+ return { success: false, error: error.message };
192
+ }
193
+ }
194
+ async function fileDelete(filePath) {
195
+ try {
196
+ const resolved = path.resolve(filePath);
197
+ const stats = await fs.promises.stat(resolved);
198
+ if (stats.isDirectory()) {
199
+ return { success: false, error: "Cannot delete directories. Use shell commands for that." };
200
+ }
201
+ await fs.promises.unlink(resolved);
202
+ return {
203
+ success: true,
204
+ data: {
205
+ path: resolved,
206
+ message: `Deleted: ${path.basename(resolved)}`
207
+ }
208
+ };
209
+ } catch (error) {
210
+ if (error.code === "ENOENT") {
211
+ return { success: false, error: `File not found: ${filePath}` };
212
+ }
213
+ return { success: false, error: error.message };
214
+ }
215
+ }
216
+ async function fileSearch(dirPath, pattern) {
217
+ if (!pattern) {
218
+ return { success: false, error: "Missing required parameter: pattern" };
219
+ }
220
+ try {
221
+ const resolved = path.resolve(dirPath);
222
+ const regex = new RegExp(pattern, "i");
223
+ const matches = [];
224
+ async function walk(dir, depth) {
225
+ if (depth > 5 || matches.length >= 50) return;
226
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
227
+ for (const entry of entries) {
228
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
229
+ const fullPath = path.join(dir, entry.name);
230
+ if (regex.test(entry.name)) {
231
+ matches.push(fullPath);
232
+ }
233
+ if (entry.isDirectory()) {
234
+ await walk(fullPath, depth + 1);
235
+ }
236
+ }
237
+ }
238
+ await walk(resolved, 0);
239
+ return {
240
+ success: true,
241
+ data: {
242
+ pattern,
243
+ searchDir: resolved,
244
+ matches,
245
+ count: matches.length,
246
+ truncated: matches.length >= 50
247
+ }
248
+ };
249
+ } catch (error) {
250
+ return { success: false, error: error.message };
251
+ }
252
+ }
253
+ const SAFE_COMMANDS = [
254
+ "ls",
255
+ "dir",
256
+ "pwd",
257
+ "cd",
258
+ "git status",
259
+ "git log",
260
+ "git branch",
261
+ "git diff",
262
+ "git remote -v",
263
+ "node --version",
264
+ "npm --version",
265
+ "python --version",
266
+ "pip --version",
267
+ "whoami",
268
+ "hostname",
269
+ "date",
270
+ "echo",
271
+ "type",
272
+ "cat"
273
+ ];
274
+ const BLOCKED_PATTERNS = [
275
+ /rm\s+-rf\s+[/\\]/i,
276
+ /del\s+\/s\s+\/q/i,
277
+ /format\s+[A-Z]:/i,
278
+ /mkfs/i,
279
+ /dd\s+if=/i,
280
+ />\s*\/dev\/sd/i,
281
+ /shutdown/i,
282
+ /reboot/i,
283
+ /reg\s+delete/i,
284
+ /net\s+user.*\/delete/i
285
+ ];
286
+ function isSafeCommand(command) {
287
+ const trimmed = command.trim().toLowerCase();
288
+ return SAFE_COMMANDS.some((safe) => trimmed.startsWith(safe));
289
+ }
290
+ function isBlockedCommand(command) {
291
+ return BLOCKED_PATTERNS.some((p) => p.test(command));
292
+ }
293
+ const MAX_OUTPUT = 5e4;
294
+ const TIMEOUT_MS = 3e4;
295
+ async function executeShellTool(toolName, params) {
296
+ const command = params.command;
297
+ const cwd = params.cwd;
298
+ if (!command) {
299
+ return { success: false, error: "Missing required parameter: command" };
300
+ }
301
+ if (isBlockedCommand(command)) {
302
+ return {
303
+ success: false,
304
+ error: "This command is blocked for safety reasons."
305
+ };
306
+ }
307
+ if (toolName === "desktop_shell_exec_safe" && !isSafeCommand(command)) {
308
+ return {
309
+ success: false,
310
+ error: `Command "${command}" is not in the safe command whitelist. Use desktop_shell_exec instead (requires approval).`
311
+ };
312
+ }
313
+ const workingDir = cwd ? path.resolve(cwd) : void 0;
314
+ return new Promise((resolveResult) => {
315
+ child_process.exec(
316
+ command,
317
+ {
318
+ cwd: workingDir,
319
+ timeout: TIMEOUT_MS,
320
+ maxBuffer: MAX_OUTPUT,
321
+ windowsHide: true
322
+ },
323
+ (error, stdout, stderr) => {
324
+ if (error) {
325
+ if (error.killed) {
326
+ resolveResult({
327
+ success: false,
328
+ error: `Command timed out after ${TIMEOUT_MS / 1e3}s`
329
+ });
330
+ return;
331
+ }
332
+ resolveResult({
333
+ success: false,
334
+ error: stderr || error.message,
335
+ data: {
336
+ stdout: stdout?.substring(0, MAX_OUTPUT),
337
+ stderr: stderr?.substring(0, MAX_OUTPUT),
338
+ exitCode: error.code
339
+ }
340
+ });
341
+ return;
342
+ }
343
+ resolveResult({
344
+ success: true,
345
+ data: {
346
+ stdout: stdout.substring(0, MAX_OUTPUT),
347
+ stderr: stderr?.substring(0, MAX_OUTPUT) || "",
348
+ exitCode: 0
349
+ }
350
+ });
351
+ }
352
+ );
353
+ });
354
+ }
355
+ async function executeClipboardTool(toolName, params) {
356
+ switch (toolName) {
357
+ case "desktop_clipboard_read": {
358
+ const text = electron.clipboard.readText();
359
+ return {
360
+ success: true,
361
+ data: {
362
+ content: text,
363
+ length: text.length
364
+ }
365
+ };
366
+ }
367
+ case "desktop_clipboard_write": {
368
+ const text = params.text;
369
+ if (!text && text !== "") {
370
+ return { success: false, error: "Missing required parameter: text" };
371
+ }
372
+ electron.clipboard.writeText(text);
373
+ return {
374
+ success: true,
375
+ data: {
376
+ message: `Copied ${text.length} characters to clipboard`,
377
+ length: text.length
378
+ }
379
+ };
380
+ }
381
+ default:
382
+ return { success: false, error: `Unknown clipboard tool: ${toolName}` };
383
+ }
384
+ }
385
+ async function executeLocalTool(toolName, params) {
386
+ const startTime = Date.now();
387
+ try {
388
+ let result;
389
+ switch (toolName) {
390
+ case "desktop_file_read":
391
+ case "desktop_file_write":
392
+ case "desktop_file_list":
393
+ case "desktop_file_delete":
394
+ case "desktop_file_search":
395
+ result = await executeFileTool(toolName, params);
396
+ break;
397
+ case "desktop_shell_exec":
398
+ case "desktop_shell_exec_safe":
399
+ result = await executeShellTool(toolName, params);
400
+ break;
401
+ case "desktop_clipboard_read":
402
+ case "desktop_clipboard_write":
403
+ result = await executeClipboardTool(toolName, params);
404
+ break;
405
+ default:
406
+ result = { success: false, error: `Unknown tool: ${toolName}` };
407
+ }
408
+ result.executionTimeMs = Date.now() - startTime;
409
+ return result;
410
+ } catch (error) {
411
+ return {
412
+ success: false,
413
+ error: error instanceof Error ? error.message : "Unknown error",
414
+ executionTimeMs: Date.now() - startTime
415
+ };
416
+ }
417
+ }
418
+ function registerIpcHandlers() {
419
+ electron.ipcMain.on("window:minimize", (event) => {
420
+ electron.BrowserWindow.fromWebContents(event.sender)?.minimize();
421
+ });
422
+ electron.ipcMain.on("window:maximize", (event) => {
423
+ const win = electron.BrowserWindow.fromWebContents(event.sender);
424
+ if (win?.isMaximized()) {
425
+ win.unmaximize();
426
+ } else {
427
+ win?.maximize();
428
+ }
429
+ });
430
+ electron.ipcMain.on("window:close", (event) => {
431
+ electron.BrowserWindow.fromWebContents(event.sender)?.hide();
432
+ });
433
+ electron.ipcMain.handle("window:isMaximized", (event) => {
434
+ return electron.BrowserWindow.fromWebContents(event.sender)?.isMaximized() ?? false;
435
+ });
436
+ electron.ipcMain.handle("tools:execute", async (_event, toolName, params) => {
437
+ return executeLocalTool(toolName, params);
438
+ });
439
+ electron.ipcMain.handle("clipboard:read", () => {
440
+ return electron.clipboard.readText();
441
+ });
442
+ electron.ipcMain.handle("clipboard:write", (_event, text) => {
443
+ electron.clipboard.writeText(text);
444
+ return { success: true };
445
+ });
446
+ electron.ipcMain.handle("app:version", () => {
447
+ const { app } = require("electron");
448
+ return app.getVersion();
449
+ });
450
+ electron.ipcMain.on("tray:setConnected", (_event, connected) => {
451
+ const updater = global.__updateTrayConnection;
452
+ if (updater) updater(connected);
453
+ });
454
+ }
455
+ exports.mainWindow = null;
456
+ const gotTheLock = electron.app.requestSingleInstanceLock();
457
+ if (!gotTheLock) {
458
+ electron.app.quit();
459
+ } else {
460
+ electron.app.on("second-instance", () => {
461
+ if (exports.mainWindow) {
462
+ if (exports.mainWindow.isMinimized()) exports.mainWindow.restore();
463
+ exports.mainWindow.focus();
464
+ }
465
+ });
466
+ }
467
+ function createWindow() {
468
+ exports.mainWindow = new electron.BrowserWindow({
469
+ width: 420,
470
+ height: 700,
471
+ minWidth: 360,
472
+ minHeight: 500,
473
+ frame: false,
474
+ titleBarStyle: "hidden",
475
+ backgroundColor: "#0a0a1a",
476
+ icon: path.join(__dirname, "../../assets/icons/icon.ico"),
477
+ webPreferences: {
478
+ preload: path.join(__dirname, "../preload/index.js"),
479
+ contextIsolation: true,
480
+ nodeIntegration: false,
481
+ sandbox: true
482
+ },
483
+ show: false
484
+ });
485
+ exports.mainWindow.on("ready-to-show", () => {
486
+ exports.mainWindow?.show();
487
+ });
488
+ exports.mainWindow.webContents.setWindowOpenHandler(({ url }) => {
489
+ electron.shell.openExternal(url);
490
+ return { action: "deny" };
491
+ });
492
+ if (utils.is.dev && process.env["ELECTRON_RENDERER_URL"]) {
493
+ exports.mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
494
+ } else {
495
+ exports.mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
496
+ }
497
+ exports.mainWindow.on("close", (event) => {
498
+ if (!electron.app.isQuitting) {
499
+ event.preventDefault();
500
+ exports.mainWindow?.hide();
501
+ }
502
+ });
503
+ }
504
+ electron.app.whenReady().then(() => {
505
+ utils.electronApp.setAppUserModelId("com.clavon.desktop");
506
+ electron.app.on("browser-window-created", (_, window) => {
507
+ utils.optimizer.watchWindowShortcuts(window);
508
+ });
509
+ registerIpcHandlers();
510
+ createWindow();
511
+ setupTray(exports.mainWindow);
512
+ });
513
+ electron.app.on("window-all-closed", () => {
514
+ });
515
+ electron.app.on("activate", () => {
516
+ if (electron.BrowserWindow.getAllWindows().length === 0) {
517
+ createWindow();
518
+ } else {
519
+ exports.mainWindow?.show();
520
+ }
521
+ });
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ const electron = require("electron");
3
+ electron.contextBridge.exposeInMainWorld("clavAPI", {
4
+ // Window controls
5
+ minimize: () => electron.ipcRenderer.send("window:minimize"),
6
+ maximize: () => electron.ipcRenderer.send("window:maximize"),
7
+ close: () => electron.ipcRenderer.send("window:close"),
8
+ isMaximized: () => electron.ipcRenderer.invoke("window:isMaximized"),
9
+ // Local tool execution
10
+ executeTool: (toolName, params) => electron.ipcRenderer.invoke("tools:execute", toolName, params),
11
+ // Clipboard
12
+ readClipboard: () => electron.ipcRenderer.invoke("clipboard:read"),
13
+ writeClipboard: (text) => electron.ipcRenderer.invoke("clipboard:write", text),
14
+ // App info
15
+ getVersion: () => electron.ipcRenderer.invoke("app:version"),
16
+ // Tray state
17
+ setTrayConnected: (connected) => electron.ipcRenderer.send("tray:setConnected", connected),
18
+ // Navigation listener (from tray menu)
19
+ onNavigate: (callback) => {
20
+ const handler = (_event, path) => callback(path);
21
+ electron.ipcRenderer.on("navigate", handler);
22
+ return () => electron.ipcRenderer.removeListener("navigate", handler);
23
+ },
24
+ // Platform info
25
+ platform: process.platform
26
+ });